题目:https://blog.csdn.net/weixin_44584560/article/details/86599565
https://blog.csdn.net/dcx2001/article/details/78269908@##@@@
这类问题主要是统计子树和,通过加减一些子树满足题目中要求的某些性质。
例如:
1.codeforces 767C Garland
这道题是让你把树分成3部分,使每部分点权和相等,这就是通过算子树的size[]实现的。
2.洛谷1122最大子树和
这道题让你剪去一些子树,让剩下的子树点权和最大。这题就要维护子树的点权和,f[i]表示i这颗子树的点权和最大值。
1、codeforce garland
分割一棵树
分成三部分,每部分的和一样
//codeforce 的题,超时了????
//分割一棵树
//分成三部分,每部分的和一样
int summ=0,tot=0;
int w[maxn]; //暖度
int to[maxn*2],fi[maxn],ne[maxn*2];
void link(int x,int y){
to[++tot]=y;
ne[tot]=fi[x];
fi[x]=tot;
}
int n,root,sz[maxn],ans[5],cnt;
void dfs(int x,int fa){ //开始分割,要有fa是因为这是一棵无根树
sz[x]=w[x];
for(int i=fi[x];i;i=ne[i]){
int v=to[i];
if(v!=fa){
dfs(v,x);
sz[x]+=sz[v]; //加上子树的值
}
}
if(sz[x]==summ) {
ans[++cnt]=x;
sz[x]=0;
}
}
int main(){
int x,y;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d %d",&x,&y);
if(x!=0){
link(x,i);
link(i,x);
}
else root=i;
w[i]=y;
summ+=y;
}
if(summ%3!=0){
printf("-1\n");
return 0;
}
summ/=3;
dfs(root,0);
if(cnt<=2) {
printf("-1\n");
}
else printf("%d %d\n",ans[2],ans[1]);
return 0;
}
2、P1122 洛谷 最大子树和

是一个很标准的树形DP,上面的第二类:求在树上选一些点满足价值最大的问题
这题是一个比较常见的树型dp的模型(稍有变形):子树型,给一棵 n 个点的树,以 1 号点为根,求以每个点为根的子树大小
先设立状态: f[u] 表示以u为根且包含u的最大权联通块
状态转移方程:f[u]+=max(0,f[v]); (v为u的孩子) 有些儿子比较丑,美丽指数小于0,可能不选,所以与0作个比较
状态转移比较容易,主要基于dfs实现,还是要多做题才能熟练
int fi[maxn],to[maxn*2],nex[maxn*2];
int n,tot;
void link(int x,int y){
to[++tot]=y;
nex[tot]=fi[x];
fi[x]=tot;
}
int dp[maxn],w[maxn];
int cut[maxn]; //记录要不要剪掉,如果这支树为负的话,就剪掉
int maxx=-1;
void dfs(int x,int fa){
dp[x]=w[x]; //先赋值
for(int i=fi[x];i;i=nex[i]){
int t=to[i];
if(t!=fa) dfs(t,x);
}
for(int i=fi[x];i;i=nex[i]){
int t=to[i];
if(t!=fa&&!cut[t]) dp[x]+=dp[t]; //因为后面递归的时候会自己判断
}
if(dp[x]<0) cut[x]=1;
maxx=max(maxx,dp[x]);
}
int main(){
int x,y;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
for(int i=1;i<=n-1;i++){
scanf("%d %d",&x,&y);
link(x,y);
link(y,x);
}
dfs(1,1);
printf("%d\n",maxx);
return 0;
}
这类问题就是让你求在树上选一些点满足价值最大的问题,一般都可以设f[i][j]表示i这颗子树选j个点的最优解。
1.洛谷1272重建道路
这道题是让你剪去最少的边实现最后剩P个点。所以P个点就是背包,剪去的边数就是价值。这题需要转化一下把剪边变成加边。
2.洛谷1273有线电视网
这道题是让你在保证不亏损的情况下满足更多的客户看到转播。此时用户的个数就是容量,f[i][j]表示i这颗子树选j个用户能赚的最多的钱。
1、P1272 洛谷 道路重建

讲解:https://www.luogu.com.cn/problemnew/solution/P1272
int n,num,p;
/*
每个状态代表一棵子树,这个子树与父节点相连。 那么初始化的话dp[i][1]=son[i],一开始都是不连儿子只连父亲
那么转移的那个就变成了dp[u][j]=max(dp[u][j-k]+dp[v][k]-1)
为什么减1呢,因为注意到我们一开始点都是不与儿子相连的,我们通过dp的过程一步一步把他们连起来。
那么对于u->v这个边,一开始在初始化u的时候已经被减掉了,因为他是连接儿子的边,而v->u这个边并没有,因为这个连向父亲的边,
我们要通过v转移,就得用u->v这个边,所以就得补回来。
而且最终算的时候,除了根节点无父亲外,其他的都要和父节点断开,也就是边数+1
第二种就是dp[i][j]表示以i为根的子树,保留j个节点,且当前子树不与父节点相连,需要拆掉的最小边数。
那么我们的初始化就变成了dp[i][1]=deg[i]
也就是把与i相连的所有边全部拆掉。
*/
struct node{
int to;
int next;
}edge[maxn*2];
int head[maxn];
void link(int x,int y){
edge[++num].next=head[x];
edge[num].to=y;
head[x]=num;
}
int dp[maxn][maxn]; //这个的含义是以i为根的子树,保留j个节点,且当前子树不与父节点相连,需要拆掉的最小边数。
int in[maxn]; //给个结点的度
void dfs(int x,int fa){
dp[x][1]=in[x];
for(int i=head[x];i;i=edge[i].next){
if(edge[i].to!=fa){
dfs(edge[i].to,x);
for(int j=p;j>0;j--){
for(int k=1;k<j;k++)
dp[x][j]=min(dp[x][j],dp[x][k]+dp[edge[i].to][j-k]-2); //为甚么减2
//因为再两个in[]里面都加了
}
}
}
}
int main(){
scanf("%d %d",&n,&p);
int x,y;
for(int i=1;i<n;i++){
scanf("%d %d",&x,&y);
in[x]++;
in[y]++;
link(x,y);
link(y,x);
}
memset(dp,0x3f,sizeof(dp));
dfs(1,0);
int ans=INF;
for(int i=1;i<=n;i++) ans=min(ans,dp[i][p]);
printf("%d\n",ans);
return 0;
}

1.明确dp[i][j]含义,表示i节点,选j个用户,能得到的钱的最大值,然后对每个节点做分组背包。
2.怎么看出是分组背包?首先,背包的总容量相当于该点为根节点的子树中所有的用户数量(dp[i][j]的 j 不可能超过它连接的所有用户数)。然后,把该节点的每个儿子看成一组,每组中的元素为选一个,选两个...选n个用户。
3.转移方程 dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[v][k]-这条边的花费) i,j不解释了,v表示枚举到这一组(即i的儿子),k表示枚举到这组中的元素:选k个用户。
4.最后输出dp[1][i]>=0的i的最大值,所以反向枚举。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m;//n为整个有线电视网的结点总数,m为用户终端的数量
//第一个转播站即树的根结点编号为1,其他的转播站编号为2到n-m,用户终端编号为n-m+1到n
int head[3010],k;
int val[3010];//用户为观看比赛而准备支付的钱数
int dp[3010][3010];//dp[i][j]表示i节点,选j个用户,能得到的钱的最大值
struct node
{
int to,next,w;
}edge[10000010];
void adde(int u,int v,int w)
{
edge[++k].to=v; edge[k].next=head[u];
edge[k].w=w; head[u]=k;
}
int dfs(int u)
{
if(u>n-m)//u为用户终端
{
dp[u][1]=val[u];
return 1;
}
int sum=0,t;
for(int k=head[u];k;k=edge[k].next)//该点连接了几个节点意为有几组,遍历每一组
{
int v=edge[k].to;
t=dfs(v); sum+=t; //t为该组的元素个数,或是说这个儿子为根的子树大小(这里的大小指子树中用户的个数),sum为该节点最多可以选多少个用户,即背包容量
for(int j=sum;j>0;j--)
{
for(int i=1;i<=t;i++)//遍历组中的元素,选多少个用户相当于一个元素
{
if(j-i>=0) dp[u][j]=max(dp[u][j],dp[u][j-i]+dp[v][i]-edge[k].w);
}
}
}
return sum;
}
int main()
{
memset(dp,~0x3f,sizeof(dp));//初始化一个极大负值,因为dp可能为负
scanf("%d%d",&n,&m);
for(int u=1;u<=n-m;u++)
{
int size,v,w;
scanf("%d",&size);
for(int j=1;j<=size;j++)
{
scanf("%d%d",&v,&w);
adde(u,v,w);
}
}
for(int i=n-m+1;i<=n;i++) scanf("%d",&val[i]);
for (int i=1;i<=n;i++) dp[i][0]=0;//选0个用户的花费肯定是0啦
dfs(1);
for (int i=m;i>=1;i--)
if (dp[1][i]>=0)
{
printf("%d",i);
break;
}
return 0;
}
这类问题是父亲与孩子有联系的题。基本有两种出法:1.选父亲必须不能选孩子(强制)2.选父亲可以不用选孩子(不强制)。
1.洛谷2458 [SDOI2006]保安站岗
这题就属于类型2.这就需要预估,要不要选这个节点的父亲。f[i][0]表示选自己,f[i][1]表示选孩子,f[i][2]表示选父亲。
2.UVA 1220 Party at Hali-Bula(这是最大独立集问题,用做和上面题区分)
这题就是强制要求父亲和孩子不能同时选,但这题没有要求必须把所有点完全覆盖,只是让选尽可能多的点,所以这样就可以用f[i][0]表示选自己,f[i][1]表示选孩子,省去f[i][2]表示选父亲了,因为没有都覆盖的强制要求,他的父亲选不选可以到他父亲再判断。
1、hdu Anniversary party 1520
选父不选子 不选父则都可以
using namespace std;
const int maxn=1010;
//没过
const int INF=0x3fffffff;
//树形dp
//选父不选子 不选父则都可以
int dp[6010][2]; //0位不选,为选
vector<int> tree[6010];
int w[6010];
int n;
int fa[6010];
void dfs(int t){
dp[t][0]=0;
dp[t][1]=w[t]; //先初始化
for(int i=0;i<tree[t].size();i++){
int son=tree[t][i];
dfs(son); //先递归
dp[t][0]+=max(dp[son][0],dp[son][1]);
dp[t][1]+=dp[son][0];
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&w[i]);
fa[i]=-1;
tree[i].clear();
}
int x,y;
// memset(dp,0,sizeof(dp));
while(scanf("%d %d",&x,&y)){
if(x==0&&y==0) break;
fa[x]=y;
tree[y].push_back(x);
}
int root=1;
while(fa[root]!=-1){
root=fa[root];
}
dfs(root);
printf("%d\n",max(dp[root][0],dp[root][1]));
return 0;
}
这类问题就是给你一个条件,问你有多少个点的集合满足这样的条件。这类题主要运用乘法原理,控制一个点不动,看他能做多少贡献。
1. 51nod1588幸运树。 问有多少个三元组满足幸运数字, 可以控制一个点不动,分成子树内,子树外两个部分,分别相乘就行了。
这种题只能根据题目分析,然后随机应变了。
1.洛谷3621 [APIO2007]风铃
把题目中的要求变成公式就行了。
2. 51nod1673树有几多愁
这道题是非常强的综合题,用到了虚树+状压dp+树形dp。
3.51nod1531树上的博弈
非常强的一道博弈题。需要分情况的讨论
2、hdu Computer 2196
对每个点,求距离它最远的点
/*
while(scanf("%d",&n)){
inti();
dfs1(1);
dp[1][2]=0; //根到上面的距离是0
dfs2(1);
for(int i=1;i<=n;i++) printf("%d\n",max(dp[i][0],dp[i][2])); //两个方向,要么是
}
*/
//下面的会超时。。。虽然好理解
/*
struct node{
int id;
int cost; //到爸爸的距离
};
vector<node> tree[maxn];
int dp[maxn][3];
//0代表到子树的最长距离,1代表到子树的次长距离,2代表从i往上走的最短距离
int n;
void inti(){
for(int i=1;i<=n;i++) tree[i].clear();
int x,co;
memset(dp,0,sizeof(dp));
for(int i=2;i<=n;i++){
scanf("%d %d",&x,&co);
node temp;
temp.id=i;
temp.cost=co;
tree[x].push_back(temp);
}
}
void dfs1(int fa){
//最长距离,次长距离
int one=0,two=0;
for(int i=0;i<tree[fa].size();i++){
node child=tree[fa][i];
dfs1(child.id); //先递归
int cost=child.cost+dp[child.id][0];
if(cost>=one){
two=one;
one=+cost;
}
else if(cost>two) two=cost;
}
dp[fa][0]=one;
dp[fa][1]=two;
}
void dfs2(int fa){ //先处理父节点,在处理子节点,所以处理顺序是从上到下,所以最后递归
for(int i=0;i<tree[fa].size();i++){
node temp=tree[fa][i];
if(dp[temp.id][0]+temp.cost==dp[fa][0]){
dp[temp.id][2]=max(dp[fa][2],dp[fa][1])+temp.cost; //如果再最长的路上,那么就加上次长的距离
}
else
dp[temp.id][2]=max(dp[fa][2],dp[fa][0])+temp.cost;
dfs2(temp.id);
}
}
*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=10000+200;
struct edge
{
int to;//终端点
int next;//下一条同样起点的边号
int w;//权值
} edges[MAXN*2];
int tot;//总边数
int head[MAXN];//head[u]=i表示以u为起点的所有边中的第一条边是 i号边
void add_edge(int u,int v,int w)//添加从u->v,权值为w的边
{
edges[tot].to=v;
edges[tot].w=w;
edges[tot].next = head[u];
head[u] = tot++;
}
int dist[MAXN][3];//dist[i][0,1,2]分别为正向最大距离,正向次大距离,反向最大距离
int longest[MAXN];
int dfs1(int u,int fa)//返回u的正向最大距离
{
if(dist[u][0]>=0)return dist[u][0]; //记忆化
dist[u][0]=dist[u][1]=dist[u][2]=longest[u]=0; //递归边界
for(int e=head[u]; e!=-1; e=edges[e].next)
{
int v= edges[e].to;
if(v==fa)continue;
if(dist[u][0]<dfs1(v,u)+edges[e].w) //更新最长
{
longest[u]=v;
dist[u][1] = max(dist[u][1] , dist[u][0]);
dist[u][0]=dfs1(v,u)+edges[e].w;
}
//更新次长
else if( dist[u][1]< dfs1(v,u)+edges[e].w )//这里一定要记得,要不然无情WA
dist[u][1] = max(dist[u][1] , dfs1(v,u)+edges[e].w);
}
return dist[u][0];
}
void dfs2(int u,int fa)
{
for(int e=head[u];e!=-1;e=edges[e].next)
{
int v = edges[e].to;
if(v==fa)continue;
if(v==longest[u]) dist[v][2] = max(dist[u][2],dist[u][1])+edges[e].w; //如果在最长的一条上
else dist[v][2] = max(dist[u][2],dist[u][0])+edges[e].w; //不在最长的
dfs2(v,u); //后递归
}
}
int main()
{
int n;
while(scanf("%d",&n)==1&&n)
{
tot=0;
memset(dist,-1,sizeof(dist));
memset(head,-1,sizeof(head));
memset(longest,-1,sizeof(longest));
for(int i=2; i<=n; i++)
{
int v,w;
scanf("%d%d",&v,&w);
add_edge(i,v,w);//加边
add_edge(v,i,w);//加边
}
dfs1(1,-1);
dfs2(1,-1);
for(int i=1;i<=n;i++)
printf("%d\n",max(dist[i][0],dist[i][2]));
}
return 0;
}
原文:https://www.cnblogs.com/shirlybaby/p/12319687.html