树形DP

题目:https://blog.csdn.net/weixin_44584560/article/details/86599565

https://blog.csdn.net/dcx2001/article/details/78269908@##@@@

1.子树和计数。

这类问题主要是统计子树和,通过加减一些子树满足题目中要求的某些性质。(具有能够用DP解决的性质)

例如:

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
");
		return 0;
	}
	summ/=3;
	dfs(root,0);
	if(cnt<=2) {
		printf("-1
");
	}
	else printf("%d %d
",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
",maxx);
return 0;
}

 

2.树上背包问题

这类问题就是让你求在树上选一些点满足价值最大的问题,一般都可以设f[i][j]表示i这颗子树选j个点的最优解

1.洛谷1272重建道路

这道题是让你剪去最少的边实现最后剩P个点。所以P个点就是背包,剪去的边数就是价值。这题需要转化一下把剪边变成加边。

2.洛谷1273有线电视网

这道题是让你在保证不亏损的情况下满足更多的客户看到转播。此时用户的个数就是容量,f[i][j]表示i这颗子树选j个用户能赚的最多的钱。

1、P1272 洛谷 道路重建

剪去最少的边实现最后剩P个点。所以P个点就是背包,剪去的边数就是价值。这题需要转化一下把剪边变成加边。

dp[i][j]表示以i为根的子树,保留j个节点,且当前子树不与父节点相连,需要拆掉的最小边数

那么我们的初始化就变成了dp[i][1]=deg[i],也就是把与i相连的所有边全部拆掉。

 讲解: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
",ans);
return 0;
}

2、P1273 有线电视网

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;
}

3.花费最少的费用覆盖所有点

这类问题是父亲与孩子有联系的题。基本有两种出法: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、P2458 [SDOI2006]保安站岗

这题是父亲与孩子有限制的树形dp。

由于这个点放不放保安与儿子放不放保安和父亲放不放保安都有关系,所以假设f[x][0] 自己选,f[x][1] 自己不选,儿子选,f[x][2] 自己不选,父亲选。dp不是没有后效性吗?
为什么这个节点可以看他的父亲呢?其实是可以的,这个叫做未来计算,可以事先把还没发生但是肯定会发生的费用累加到答案中。那么这题就非常简单了。
dp方程:f[x][0]+=min(f[vv][1],min(f[vv][2],f[vv][0]));f[x][2]+=min(f[vv][0],f[vv][1]);dp[x][1]有一些特殊的地方因为自己不选儿子并不一定所有的儿子都选,
所以我们要选一个最优的儿子选。可以记一个f[vv][0]-f[vv][1]的最小值,最后把这个加进去就行了。
原文链接:https://blog.csdn.net/dcx2001/article/details/78269908

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=2010;
const int INF=0x3fffffff;
typedef long long LL;
//这个也是不用于最大独立集的那种题,每个点选择的代价是不一样的
//所以要开三维
/*
这题是父亲与孩子有限制的树形dp。

由于这个点放不放保安与儿子放不放保安和父亲放不放保安都有关系,所以假设f[x][0] 自己选,f[x][1] 自己不选,儿子选,f[x][2] 自己不选,父亲选。dp不是没有后效性吗?
为什么这个节点可以看他的父亲呢?其实是可以的,这个叫做未来计算,可以事先把还没发生但是肯定会发生的费用累加到答案中。那么这题就非常简单了。
dp方程:f[x][0]+=min(f[vv][1],min(f[vv][2],f[vv][0]));f[x][2]+=min(f[vv][0],f[vv][1]);dp[x][1]有一些特殊的地方因为自己不选儿子并不一定所有的儿子都选,
所以我们要选一个最优的儿子选。可以记一个f[vv][0]-f[vv][1]的最小值,最后把这个加进去就行了。
原文链接:https://blog.csdn.net/dcx2001/article/details/78269908
*/ 
int f[maxn][4],tot,n;
//f[x][0] 自己选,f[x][1] 自己不选,儿子选,f[x][2] 自己不选,父亲选
int in[maxn],v[maxn];
int to[maxn*2],head[maxn],next[2*maxn];
void add(int x,int y){
	to[++tot]=y;
	next[tot]=head[x];
	head[x]=tot;
}
void dfs(int x,int fa){
	f[x][0]=v[x]; //  自己的代价
	for(int i=head[x];i;i=next[i]){
		int vv=to[i];
		if(vv!=fa){
			dfs(vv,x);
		}
	} 
	int minn=INF;
	bool yes=0,have=0;
	for(int i=head[x];i;i=next[i]){
		int vv=to[i];
		have=true;
		f[x][0]+=min(f[vv][0],min(f[vv][1],f[vv][2]));
		f[x][2]+=min(f[vv][0],f[vv][1]);
		if(f[vv][0]<=f[vv][1]){
			f[x][1]+=f[vv][0];  //儿子选,要挑选 
			yes=1;
		}
		else{
			f[x][1]+=f[vv][1];
			minn=min(minn,f[vv][0]-f[vv][1]);
		}
	}
	if(!yes) f[x][1]+=minn;
	if(!have) f[x][1]=INF; 
}
int main(){
	scanf("%d",&n);
	int x,y,z,x1;
	for(int i=1;i<=n;i++){
		scanf("%d %d %d",&x,&y,&z);
		v[x]=y;
		while(z--){
			scanf("%d",&x1);
			add(x,x1);add(x1,x);
		}
	}
	memset(f,0,sizeof(f));
	dfs(1,0);
	printf("%d
",min(f[1][1],f[1][0]));
return 0;
}

2、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
",max(dp[root][0],dp[root][1]));
return 0;
}

poj 1463 Strategic game  和上面是一样的

在一个节点放士兵,就可以监视与这个点相连的所有点,求监视所有的路,至少需要多少士兵

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1510;
const int INF=0x3fffffff;
typedef long long LL;
//树形dp
//在一个节点放士兵,就可以监视与这个点相连的所有点,求监视所有的路,至少需要多少士兵
int n;
int pre[maxn],dp[maxn][2],child[maxn],root;
void DP(int root){
	int dp0=0,dp1=0;
	if(child[root]==0){  //是叶子节点 
		dp[root][0]=0;
		dp[root][1]=1;
		return;
	}
	for(int i=0;i<n;i++){
		if(pre[i]==root) {
			DP(i); //对孩子进行递归,最终到达叶子节点 
			dp1+=min(dp[i][0],dp[i][1]);
			dp0+=dp[i][1]; 
		}
	}
	dp[root][1]=dp1+1;
	dp[root][0]=dp0;
} 
 
int main(){
	while(~scanf("%d",&n)){
		memset(dp,0,sizeof(dp));
		memset(pre,-1,sizeof(pre));
		root=-1;
		int fa,m,x;
		memset(child,-1,sizeof(child));
		for(int i=0;i<n;i++){
			scanf("%d:(%d)",&fa,&m);
			child[fa]=m;
			if(root==-1) root=fa;
			while(m--){
				scanf("%d",&x);
				pre[x]=fa; 
				if(x==root) root=fa;  //找到根节点 
			}
		}
		DP(root);
		printf("%d
",min(dp[root][1],dp[root][0])); 
	}
return 0;
}

 

 

 

 

 

4.树上统计方案数问题

这类问题就是给你一个条件,问你有多少个点的集合满足这样的条件。这类题主要运用乘法原理,控制一个点不动,看他能做多少贡献

1. 51nod1588幸运树。 问有多少个三元组满足幸运数字, 可以控制一个点不动,分成子树内,子树外两个部分,分别相乘就行了。

与多种算法结合&&大模拟

这种题只能根据题目分析,然后随机应变了。
1.洛谷3621 [APIO2007]风铃

把题目中的要求变成公式就行了。

一个树形dp的变形。

首先发现这个树一定是二叉树,根据邻接表的性质所以可以把左边的点最后加,这样边就是先左后右了。
设mindep[i]表示i这个点的子树中铃的最小深度,maxdep[i]表示最大深度。dp[i]表示使i这颗子树变成满足条件的需要几步,转移就是两个子树相加就行了,
需要注意的是不满足的情况,除了深度差大于1,还有这个条件不要丢掉。maxdep[c[1]]>mindep[c[2]]&&maxdep[c[2]]>mindep[c[1]]。

首先对于所有玩具如果有深度差超过1的就是无解(显然),所以dfs一遍记录最小最大的深度即可。然后如果有一个点的两颗子树中都含有最小、最大深度,那么这种情况也是无解,因为无论你怎么交换都不能使深度小的全部到右边去。

然后考虑最少交换次数:其实对于每一个节点的左右子树,三种情况需要交换,

  1. 左边全是小深度的,右边全是大深度的

  2. 左边全是小深度的,右边大小深度都有

  3. 左边大小深度都有,右边全是大深度的

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e5+10;
const int INF=0x3fffffff;
typedef long long LL;
int n,cnt,tot;
int dep[maxn],mindep[maxn],maxdep[maxn];
//深度 , 最小、最大深度 
int head[maxn*2],next[2*maxn],to[maxn],dp[maxn],w[maxn];
void add(int x,int y){
	to[++cnt]=y;
	next[cnt]=head[x];
	head[x]=cnt;
}
/*
一个树形dp的变形。

首先发现这个树一定是二叉树,根据邻接表的性质所以可以把左边的点最后加,这样边就是先左后右了。
设mindep[i]表示i这个点的子树中铃的最小深度,maxdep[i]表示最大深度。dp[i]表示使i这颗子树变成满足条件的需要几步,转移就是两个子树相加就行了,
需要注意的是不满足的情况,除了深度差大于1,还有这个条件不要丢掉。maxdep[c[1]]>mindep[c[2]]&&maxdep[c[2]]>mindep[c[1]]。

原文链接:https://blog.csdn.net/dcx2001/article/details/78269908
*/
int c[10];
void dfs(int x,int fa){
	for(int i=head[x];i;i=next[i]){
		int v=to[i];
		if(v==fa) continue;
		dep[v]=dep[x]+1;
		dfs(v,x);
		//先递归在更新
		mindep[x]=min(mindep[x],mindep[v]);
		maxdep[x]=max(maxdep[x],maxdep[v]); 
	}
	if(w[x]==-1)
	 mindep[x]=maxdep[x]=dep[x];  //这个就只有一种情况
	else{
		int cnt1=0;
		for(int i=head[x];i;i=next[i]){
	 	int v=to[i];
	 	if(v==fa) continue;
	 	c[++cnt1]=v;
	 } 
	  if((abs(maxdep[c[1]]-mindep[c[2]])>1)||(abs(mindep[c[1]]-maxdep[c[2]])>1) || (maxdep[c[1]]>mindep[c[2]]&&maxdep[c[2]]>mindep[c[1]])) {
	 	printf("-1");
	 	exit(0);
	 }
	 dp[x]=dp[c[1]]+dp[c[2]]+((mindep[c[1]]<maxdep[c[2]])?1:0);
	}
	 //三种情况是不可能有结果的
	 //第一种是两边差距>1或者两边都有最大最小 
	 
}
int main(){
	scanf("%d ",&n);
	int x,y;
	tot=n;
	for(int i=1;i<=n;i++){
		scanf("%d %d",&x,&y);
		if(y==-1) {
			y=++tot;w[tot]=-1;  //先弄右子树,这样左子树就是在前面,这样保证上面的c[1]是左子树
 		}
 		if(x==-1){
 			x=++tot;w[tot]=-1;
		 }
		 add(i,y);add(y,i);
		 add(i,x);add(x,i);
	}
	memset(dp,0x3f,sizeof(dp));
	dfs(1,0);
	printf("%d
",dp[1]);
return 0;
}

//错了错了,landegaile
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
//#include<cmath>
using namespace std;
const int N=300010;
int pre[N*2],last[N],other[N*2],num,w[N];
int dep[N],mindep[N],maxdep[N],c[10],dp[N];
inline void add(int x,int y){
    num++;
    pre[num]=last[x];
    last[x]=num;
    other[num]=y;
}
void dfs(int x,int fa){
    for(int i=last[x];i;i=pre[i]){
        int v=other[i];
        if(v!=fa){
            dep[v]=dep[x]+1;
            dfs(v,x);
            mindep[x]=min(mindep[x],mindep[v]);
            maxdep[x]=max(maxdep[x],maxdep[v]);
        }
    }
    if(w[x]==-1){
        mindep[x]=maxdep[x]=dep[x];
    }
    else{
        int cnt1=0;
        for(int i=last[x];i;i=pre[i]){
            int v=other[i];
            if(v!=fa)
            c[++cnt1]=v;
        }
        if((abs(maxdep[c[1]]-mindep[c[2]])>1)||(abs(mindep[c[1]]-maxdep[c[2]])>1) || (maxdep[c[1]]>mindep[c[2]]&&maxdep[c[2]]>mindep[c[1]])){
            puts("-1");exit(0);
        }
        dp[x]=dp[c[1]]+dp[c[2]]+((mindep[c[1]]<maxdep[c[2]])?1:0);
    }
}
void dfs1(int x,int fa){
    printf("%d ",x);
    for(int i=last[x];i;i=pre[i]){
        int v=other[i];
        if(v!=fa)
        dfs1(v,x);
    }
}
int main(){
    int n;
    int x,y;
    scanf("%d",&n);
    int cnt=n;
    for(int i=1;i<=n;i++){
        scanf("%d%d",&x,&y);
        if(y==-1) y=++cnt,w[cnt]=-1;
        add(i,y);add(y,i);
        if(x==-1) x=++cnt,w[cnt]=-1;
        add(i,x);add(x,i);    
    }
    //cout<<other[last[2]]<<endl;
    memset(mindep,0x3f,sizeof(mindep));
    dfs(1,0);
    //if(maxdep[1]-mindep[1]>1) puts("-1"); 
    printf("%d
",dp[1]);
    return 0;
} 

  

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
",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 ",max(dist[i][0],dist[i][2])); } return 0; }

1575:【例 1】二叉苹果树   ---->第二类树上背包

dp[u][i]为u节点上保留i根树枝的至多保留的苹果数,那么转态转移方程便是dp[u][i]=max(dp[u][i],dp[u][i-k-1]+g[u][i].w+dp[v][k])

dp[u][j]=max(dp[u][j],dp[u][j-k-1]+dp[t][k]+adj[u][i].wei);
//t是u的儿子,其实是在所有的儿子里面挑选,挑选出保留m条边能够获得的最大值。最后printf("%d ",dp[1][m]);  //以1为根,保留m 

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1010;
const int INF=0x3fffffff;
typedef long long LL;
//设dp[u][i]为u节点上保留i根树枝的至多保留的苹果数,那么转态转移方程便是dp[u][i]=max(dp[u][i],dp[u][i-k-1]+g[u][i].w+dp[v][k])
struct node{
	int to,wei;
};
int n,m;
int dp[maxn][maxn];
vector<node> adj[maxn];
void dfs(int u,int root){
	for(int i=0;i<adj[u].size();i++){
		int t=adj[u][i].to;
		if(t==root) continue;  //不能往回搜 
		dfs(t,u);
		for(int j=m;j>=1;j--){  //遍历所有要保留的边的数量 
			for(int k=j-1;k>=0;k--){  //分离的树枝 
				dp[u][j]=max(dp[u][j],dp[u][j-k-1]+dp[t][k]+adj[u][i].wei);
				//t是u的儿子,其实是在所有的儿子里面挑选,挑选出保留m条边能够获得的最大值 
			}
		}
	}
	
}
int main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n-1;i++){
		int u,v,w;
		scanf("%d %d %d",&u,&v,&w);
		adj[u].push_back(node{v,w});
		adj[v].push_back(node{u,w});
	} 
	dfs(1,0);
	printf("%d
",dp[1][m]);  //以1为根,保留m 
return 0;
}

  

1576:【例 2】选课  --->第二类树上背包

设f[now][j][k]表示以now为根节点的子树考虑前j个节点选k门课的方案数((这道题没有用

和上一题很像

dp[i][j]表示以i为根节点选j个的最优值    dp[now][j]=max(dp[now][j],dp[now][j-k]+dp[t][k]);

这道题还建立了虚拟源点,一般在森林里面,可以建立一个这样的超级源点。

这样只用在最后输出时调用dp[0][m+1]即可

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1010;
const int INF=0x3fffffff;
typedef long long LL;
//树形背包、DAG 
//设f[now][j][k]表示以now为根节点的子树考虑前j个节点选k门课的方案数
//因为1号节点是根节点,显然递推起点f[now][1][1]=val[now]
//然后又发现每门课有最多有一个先修课
//所以这一定是一个森林
//为了方便处理也是为了迎合输入数据 
//就把0号节点看做根节点即可    j建立超级源点 
int n,m;
struct node{
	int to,next;
}ed[maxn];
int dp[maxn][maxn];
int head[maxn],cnt; 
void add(int x,int y){
	ed[++cnt].to=y;
	ed[cnt].next=head[x];
	head[x]=cnt;
}
void DP(int now){
	for(int i=head[now];i;i=ed[i].next){
		int t=ed[i].to;
		DP(t);  //需要不断递归到底层 
		//可选的课程,跟上一题要保留的树枝一样 
		for(int j=m+1;j>=1;j--){
			for(int k=0;k<j;k++){  //注意这个递归顺序 
				dp[now][j]=max(dp[now][j],dp[now][j-k]+dp[t][k]);
			}
		}
	}
}

int main(){
	scanf("%d %d",&n,&m);
	int u,wei;
	for(int i=1;i<=n;i++){
		scanf("%d %d",&u,&wei);
		add(u,i);
		dp[i][1]=wei; //初始化在这里 选1个 
	}
	DP(0); //建立的虚拟源点0 !!! 
	printf("%d
",dp[0][m+1]);
return 0;
}  

1577:【例 3】数字转换

其实这道题要仔细分析,分析以后就是知道怎样连边?只要比它小,就可以连边

连边之后,知道要求什么,求最长变换,就是求最长链,其实就是求树的直径

于每一个数,把它和它能够转移到的数之间连一条边。
由于不存在多元环,这个图本质上是一棵树。
然后在树上求最长链的长度就可以了。
具体实现就是dfs遍历整棵树,对于以每个点i为根的子树上的最长链长
形成一个无向图,然后求最长链就可以了
!!!!问题其实就是求树的直径,

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=5e4+10;
const int INF=0x3fffffff;
typedef long long LL;
/*
对于每一个数,把它和它能够转移到的数之间连一条边。 
由于不存在多元环,这个图本质上是一棵树。 
然后在树上求最长链的长度就可以了。 
具体实现就是dfs遍历整棵树,对于以每个点i为根的子树上的最长链长
 形成一个无向图,然后求最长链就可以了 
 //!!!!问题其实就是求树的直径, 
*/
int n,ans,mm;
int in[maxn],summ[maxn];
bool vis[maxn];
int cnt,head[maxn];
struct node{
	int to;
	int next;  //建图 
}ed[maxn<<1];
void adde(int u,int v){
	ed[++cnt].to=v;
	ed[cnt].next=head[u];
	head[u]=cnt;
}
void dfs(int u,int step){
	vis[u]=1;
	if(step>mm){
		mm=step;
		ans=u;  //最长链 
	} 
	for(int i=head[u];i;i=ed[i].next){
		if(!vis[ed[i].to]) dfs(ed[i].to,step+1);
	}
}


int main(){
	summ[1]=0;
	summ[2]=1;
	summ[3]=1;
	for(int i=4;i<=50000;i++){
		for(int j=2;j*j<=i;j++){
			if(i%j==0){
				summ[i]+=j;
				if(j*j!=i) summ[i]+=i/j; //加上另一个家伙 
			}
		}
		summ[i]++; //加上1 
	}
	scanf("%d",&n);
	if(n==1) {
		printf("0
");return 0;
	} 
	for(int i=2;i<=n;i++){
		if(summ[i]<i){  //从小向大连边 
			adde(summ[i],i);
			adde(i,summ[i]);
		}
	}
	dfs(1,1);
	memset(vis,0,sizeof(vis));
	mm=0;
	dfs(ans,1);
	printf("%d
",mm-1);
	
return 0;
}

1578:【例 4】战略游戏  --->第三类儿子爸爸有限制   树的最大独立集

和上面的一样的,dp[maxn][2],开二维就好了,0表示不放,1表示放

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1510;
const int INF=0x3fffffff;
typedef long long LL;
int n;
int dp[maxn][2];
int pre[maxn];
int child[maxn];  //孩子数量 
int root=-1;
void dfs(int root){
	int dp0=0,dp1=0;
	if(child[root]==0){
		//直接赋值
		dp[root][0]=0;
		dp[root][1]=1;
		return; 
	}
	for(int i=0;i<n;i++){
		if(pre[i]==root){
			dfs(i);
			dp0+=dp[i][1];
			dp1+=min(dp[i][0],dp[i][1]);
		}
	}
	dp[root][0]=dp0;
	dp[root][1]=dp1+1;  //在这里加1 
}
int main(){
	scanf("%d",&n);
	int fa,x,num;
	memset(child,-1,sizeof(child));
	memset(pre,-1,sizeof(pre));   //从0开始,所以必须赋值为-1 
	for(int i=0;i<n;i++){
		scanf("%d %d",&fa,&num);
		child[fa]=num;
		if(root==-1) root=fa;
		while(num--){
			scanf("%d",&x);
			pre[x]=fa;
			if(x==root) root=fa;
		}
	}
	//cout<<root<<endl;
	dfs(root);
	printf("%d",min(dp[root][0],dp[root][1])); 
return 0;
}

  

1579: 【例 5】皇宫看守    --->第三类儿子爸爸有限制

这道题和上面不一样,要开三维,和Party那道题也不一样,

本题与战略游戏最大的区别,战略游戏是求树的最大独立集
最大独立集,选取每一个节点的代价是一样的,选取的点不相接得到的一定是最优的解
而这题的选取节点的代价是不相同的,故选取的点相接是有可能获得最优解的
f[u][0]表示i的父亲节点放了一个守卫,以i ii为根的子树最小需要的代价
f[u][1]表示i的子节点放一个守卫,以…(同上)
f[u][2]表示i自身放一个守卫,以…(同上)

在递归中不断更新,这个更新比较复杂,记住吧

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e6+10;  //!!!嘿嘿嘿嘿 
const int INF=0x3fffffff;
typedef long long LL;
//这道题与战略游戏不一样
/*
本题与战略游戏最大的区别,战略游戏是求树的最大独立集
最大独立集,选取每一个节点的代价是一样的,选取的点不相接得到的一定是最优的解
而这题的选取节点的代价是不相同的,故选取的点相接是有可能获得最优解的
f[u][0]表示i ii的父亲节点放了一个守卫,以i ii为根的子树最小需要的代价
f[u][1] f[u][1]f[u][1]表示i ii的子节点放一个守卫,以…(同上)
f[u][2] f[u][2]f[u][2]表示i ii自身放一个守卫,以…(同上)
*/ 
int n,root,cnt,m;
int head[maxn],wei[maxn],vis[maxn];
int dp[maxn][3]; //改为3维 
struct node{
	int to,next;
}ed[maxn];
void adde(int u,int v){
	ed[++cnt].to=v;
	ed[cnt].next=head[u];
	head[u]=cnt;
}
void readd(){
	scanf("%d",&n);
	int fa,num,x;
	root=-1;
	for(int i=1;i<=n;i++){
		scanf("%d",&fa);
		scanf("%d %d",&wei[fa],&num);
		while(num--){
			scanf("%d",&x);
			adde(fa,x);  //建立爸爸指向儿子的边 
			vis[x]++;  //入度++,入度为0的是root 
		}
	}
	for(int i=1;i<=n;i++){
		if(vis[i]==0) root=i;
	} 
}
/*
long long dp[N][2][2];
    //第一维[0,1]表示这个点是否有士兵
    //第二维[0,1]表示这个点是否安全 
    inline void dfs(int x,int fa)
    {
        int i;
        long long Min=inf;
        bool bo=0;
        for(i=head[x];i;i=Next[i]) if(to[i]!=fa)
        {
            dfs(to[i],x);
            dp[x][0][0]+=dp[to[i]][0][1];
            if(dp[to[i]][0][1]>dp[to[i]][1][1]) bo=1;
            else Min=min(Min,dp[to[i]][1][1]-dp[to[i]][0][1]);
            dp[x][0][1]+=min(dp[to[i]][0][1],dp[to[i]][1][1]);
            dp[x][1][1]+=min(min(dp[to[i]][0][0],dp[to[i]][0][1]),dp[to[i]][1][1]);
        }
        if(!bo)dp[x][0][1]+=Min;
        dp[x][1][1]+=Cost[x];
        return;
    }
*/ 
void dfs(int u,int fa){
	int d=INF;
	for(int i=head[u];i;i=ed[i].next){
		int v=ed[i].to;
		if(v==fa) continue;
		dfs(v,u);
		dp[u][0]+=min(dp[v][2],dp[v][1]);  //爸爸没放,那么儿子要么自己放,要么儿子的儿子放
		dp[u][1]+=min(dp[v][2],dp[v][1]); //爸爸的子节点放,那么相当于爸爸还是没有放
		d=min(d,dp[v][2]-min(dp[v][2],dp[v][1]));
		//儿子放最便宜的 
		dp[u][2]+=min(dp[v][2],min(dp[v][1],dp[v][0])); 
		//如果爸爸放,那么儿子啥情况都可以,所有取所有的情况的最小值 
	}
	dp[u][1]+=d;  //加上儿子放的最小的代价
	dp[u][2]+=wei[u]; //加上自己的价钱 
}
int main(){
	readd();
	memset(dp,0,sizeof(dp));
	dfs(root,0);
	printf("%d",min(dp[root][2],dp[root][1])); 
return 0;
}

1580:加分二叉树   区间dp(不是树形PD TAT

很模板的NOIP原题,先区间dp搞一搞,顺便记下路径,递归输出方案就over了
要知道在中序序列里任选一个点作为根节点,其左边的点就是左子树的中序遍历序列,右边的点就是右子树的中序遍历序列,问题具有递归性质。枚举根节点按照题意更新即可。
用个数组记录下区间的根节点,递归打印前序序列。

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1010;
const int INF=0x3fffffff;
typedef long long LL;
//很模板的NOIP原题,先区间dp搞一搞,顺便记下路径,递归输出方案就over了
//要知道在中序序列里任选一个点作为根节点,其左边的点就是左子树的中序遍历序列,右边的点就是右子树的中序遍历序列,问题具有递归性质。枚举根节点按照题意更新即可。
//用个数组记录下区间的根节点,递归打印前序序列。
//区间dp(不是树形PD TAT
int a[40],summ[40],dp[40][40],rel[40][40];
int n;
bool flag;
void print(int s,int e){
	if(flag) printf(" ");
	flag=true;
	if(s==e) printf("%d",s);
	else if(s+1==e) printf("%d %d",s,e);
	else{
		printf("%d",rel[s][e]);
		print(s,rel[s][e]-1);
		print(rel[s][e]+1,e);
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		summ[i]=summ[i-1]+a[i];
	}
	for(int i=1;i<=n;i++){
		dp[i][i]=a[i];
		dp[i][i+1]=a[i]+a[i+1];
	}
	for(int len=2;len<n;len++){  //区间DP 
		for(int i=1,j=len+1;j<=n;j++,i++){
			dp[i][j]=0; //求最大值 
			for(int k=i+1;k<j;k++){
				int t=dp[i][k-1]*dp[k+1][j]+a[k];  //以k为根节点 
				if(t>dp[i][j]){
					dp[i][j]=t;
					rel[i][j]=k;  //记录根节点 
				} 
			}
			if(summ[j]-summ[i-1]>dp[i][j]){  //另一种选择 
				dp[i][j]=summ[j]-summ[i-1];
				rel[i][j]=j;
			}
		}
	}
	printf("%d
",dp[1][n]);
	flag=false;
	print(1,n);
return 0;
}

我觉得这个程序更好理解

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=35;
const int INF=0x3fffffff;
typedef long long LL;
//这个写法我更好理解

int a[maxn];
int dp[maxn][maxn];
int pre[maxn][maxn];
int n;
void dfs(int l,int r){
	if(l>r) return;
	int x=pre[l][r];
	cout<<x<<" ";
	dfs(l,x-1);
	dfs(x+1,r);
}

int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int len=1;len<=n;len++){
		for(int l=1;l<=n-len+1;l++){
			int r=l+len-1;
			if(l==r) dp[l][r]=a[l],pre[l][r]=l;//叶节点
			else{
				for(int k=l;k<r;k++){
					int left=1,right=1;
					if(k!=l) left=dp[l][k-1];///存在左子树
					if(k!=r) right=dp[k+1][r];  //存在右子树
					if(dp[l][r]<left*right+a[k]){
						dp[l][r]=left*right+a[k];
						pre[l][r]=k;
					} 
				}
			}
		}
	}
	cout<<dp[1][n]<<endl;
	dfs(1,n);	

return 0;
}

1581:旅游规划

这道题的本质是求出直径,然后求出在直径上的点  和上面的数字转换很像,而且都是求直径,可以看一下两种不同的求直径的方法
// dp一遍,求出一个点到除他所在的子树的最大距离。
//然后枚举所有点看看是不是字树内的最大距离加上子树外的最大距离等于直径长度。
//怎么求子树外的最大距离呢?
//其实只要从上到下,从根到叶子结点转移。
// u ,他的儿子 v ,f[v] 表示 v 子树外的最远距离。
//那么如果这个点吧 d1[u] 更新了,那么他就不能从 d1[u] 哪里得到更新
//不然就可以。

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=2e5+10;
const int INF=0x3fffffff;
typedef long long LL;
//https://www.cnblogs.com/JCNL666/p/10728048.html
//这道题的本质是求出直径,然后求出在直径上的点 
// dp一遍,求出一个点到除他所在的子树的最大距离。
//然后枚举所有点看看是不是字树内的最大距离加上子树外的最大距离等于直径长度。
//怎么求子树外的最大距离呢?
//其实只要从上到下,从根到叶子结点转移。
// u ,他的儿子 v ,f[v] 表示 v 子树外的最远距离。
//那么如果这个点吧 d1[u] 更新了,那么他就不能从 d1[u] 哪里得到更新
//不然就可以。
int d1[maxn],d2[maxn],f[maxn];
//分别是最长路,次长路, 这个点向上能走的最长路
int n,max_dis=-1;
vector<int> e[maxn];
void dfs1(int u,int fa){  //第一个DFS:求这棵树的直径 
	for(int i=0;i<e[u].size();i++){
		int v=e[u][i];
		if(v==fa) continue;
		dfs1(v,u);
		if(d1[v]+1>d1[u]){   //最长 
			d2[u]=d1[u];
			d1[u]=d1[v]+1;
		}
		else if(d1[v]+1>d2[u]){  //次长 
			d2[u]=d1[v]+1;
		}
	}
	max_dis=max(max_dis,d1[u]+d2[u]);   //直径 
}
void dfs2(int u,int fa){
	int ans=0;
	for(int i=0;i<e[u].size();i++){
		int v=e[u][i];
		if(v==fa) continue;
		if(d1[v]+1==d1[u]) ans++;   //个数 
	}
	for(int i=0;i<e[u].size();i++){
		int v=e[u][i];
		if(v==fa) continue;
		if(d1[u]!=d1[v]+1||(ans>1&&d1[u]==d1[v]+1)) f[v]=max(f[u],d1[u])+1;
		//如果v不在u的最长子树那条路里面,或者在但是这并不是唯一的一条最长路
		//那么v到外面的最长路,除了可以考虑u到外面的最长路+1,或者也可以考虑u的像下面的最长路+1(到自己的兄弟 
		else f[v]=max(f[u],d2[u])+1;
		//不是的话,就可以考虑次长的兄弟,不能考虑自己呀,因为要么自己就是最长的,要么就是唯一的 
		dfs2(v,u);
	}
}
int main(){
	scanf("%d",&n);
	int u,v;
	for(int i=1;i<n;i++){
		scanf("%d %d",&u,&v);
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs1(0,0);
	dfs2(0,0);
	for(int i=0;i<n;i++){
		if(d1[i]+max(d2[i],f[i])==max_dis) printf("%d
",i);
		//到子树下面的最长路或者到外面的最长路加起来等于直径 
	}
return 0;
}

1582:周年纪念晚会  --->第三类爸爸儿子有限制

和前面写过的party favor很像

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1010;
const int INF=0x3fffffff;
typedef long long LL;
const int MIN=-INF;
int n;
struct node{
	int x,isroot;
	vector<int> child;
}ed[1000001];
int dp[1000001][2];
void dfs(int x){
	dp[x][0]=0;
	dp[x][1]=ed[x].x;
	for(int i=0;i<ed[x].child.size();i++){
		dfs(ed[x].child[i]);
		dp[x][0]+=max(dp[ed[x].child[i]][1],dp[ed[x].child[i]][0]);
		dp[x][1]+=(dp[ed[x].child[i]][0]);
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&ed[i].x);
		ed[i].isroot=1;
	}
	for(int i=1;i<=n-1;i++){
		int x,y;
		scanf("%d %d",&x,&y);
		ed[y].child.push_back(x);
		ed[x].isroot=0;
	}
	for(int i=1;i<=n;i++){
		if(ed[i].isroot==1){
			dfs(i);
			printf("%d",max(dp[i][1],dp[i][0]));
			return 0;
		}
	}
}

1583:叶子的染色

对于某一个节点,其被染成 x 色的代价,
//(1) 可以直接继承 其父亲被染成 x 色的代价
//(2) 可以保持父亲为 非 x 色,并将此节点单独染成 x 色

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e4+10;
const int INF=0x3fffffff;
typedef long long LL;
//还是不太理解
//https://www.cnblogs.com/luckyblock/p/11456303.html
//则,对于某一个节点,其被染成 x 色的代价,
   //(1) 可以直接继承 其父亲被染成 x 色的代价
   //(2) 可以保持父亲为 非 x 色,并将此节点单独染成 x 色
struct node{
	int from,to,next;
}ed[2*maxn];
int head[maxn],c[maxn];
int cnt,root,n,m;
int dp[maxn][2]; // f[i][j]表示,第i个点,将其染成j颜色,所需要的代价
inline int rread(){
	int f1=1,w=0;
	char ch=getchar();
	while(!isdigit(ch)&&ch!='-') ch=getchar();
	if(ch=='-') f1=-1;
	while(isdigit(ch)){w=w*10+ch-'0';ch=getchar();
	}
	return f1*w;
} 
void adde(int u,int v){
	ed[++cnt].from=u;
	ed[cnt].to=v;
	ed[cnt].next=head[u];
	head[u]=cnt;
}
void dfs(int u,int fa){
	if(u<=m) return; //根节点,就直接返回 (叶节点的代价不需要被更新)
	for(int i=head[u];i;i=ed[i].next){ //枚举所有非父节点 
		int v=ed[i].to;
		if(v==fa) continue;
		dfs(v,u);
		dp[u][1]+=min(dp[v][1]-1,dp[v][0]);  //如果和儿子和爸爸一样,就没有必要去花这个钱 
		dp[u][0]+=min(dp[v][0]-1,dp[v][1]);//用子节点,更新当前点的各值 
	} 
}
int main(){
	n=rread();m=rread();
	root=m+1;   //随便一个 
	for(int i=1;i<=m;i++) c[i]=rread();  //1~m表示叶子节点 
	for(int i=1;i<=n-1;i++){
		int u=rread(),v=rread();
		adde(u,v);
		adde(v,u);
	}
	for(int i=1;i<=n;i++){
		dp[i][0]=dp[i][1]=1;   //初始的代价都为1 
		if(i<=m) dp[i][c[i]^1]=INF;  //初始化为无穷大,表示不能该百年 
	}
	dfs(root,root);
	printf("%d",min(dp[root][1],dp[root][0])); //root要么染色要么布染色 
return 0;
}

1584:骑士

涉及基环树什么的概念,我现在理解不了
https://www.cnblogs.com/rvalue/p/7684069.html
https://www.cnblogs.com/LiGuanlin1124/p/10801620.html
https://www.cnblogs.com/Pedesis/p/10919226.html
由于每位骑士只有一个厌恶的骑士,所以我们考虑连一条被厌恶骑士指向该骑士的有向边。这样就会形成若干个连通块,每个连通块均为基环树,且不存在指向环的边。
然后对于每个连通块,先找到环的位置,然后删去环的一条边(形成树形结构),删去的边上的两点记作a和b,
以不选用a骑士和不选用b骑士分别做一次树形dp,得到最大战斗力。将每个连通块的最大战斗力相加,得到答案。

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e6+10;
const int INF=0x3fffffff;
typedef long long LL;
//涉及基环树什么的概念,我现在理解不了
//https://www.cnblogs.com/rvalue/p/7684069.html
//https://www.cnblogs.com/LiGuanlin1124/p/10801620.html
///https://www.cnblogs.com/Pedesis/p/10919226.html 
/*
由于每位骑士只有一个厌恶的骑士,所以我们考虑连一条被厌恶骑士指向该骑士的有向边。这样就会形成若干个连通块,每个连通块均为基环树,且不存在指向环的边。
然后对于每个连通块,先找到环的位置,然后删去环的一条边(形成树形结构),删去的边上的两点记作a和b,
以不选用a骑士和不选用b骑士分别做一次树形dp,得到最大战斗力。将每个连通块的最大战斗力相加,得到答案。
*/ 
LL n,a[maxn];//仇视的对象 
LL to[maxn],next[maxn],head[maxn],dp[maxn][2],summ,cnt,root;
bool b[maxn];
bool vis[maxn];
bool book[maxn];
LL w,yx,temp;
void adde(int u,int v){
	to[++cnt]=v;
	next[cnt]=head[u];
	head[u]=cnt;
}
void findd(LL x,LL fa){
	book[x]=1;
	for(LL i=head[x];i;i=next[i]){
		if(to[i]!=fa){
			if(!book[to[i]]) findd(to[i],x);
			else{
				w=to[i];  //找到了环 
				yx=x;
				return;	
			}
		}
	}
}
void tree(LL x){
	vis[x]=1;
	b[x]=1;
	dp[x][0]=0;
	dp[x][1]=a[x]; //取得情况 
	for(LL i=head[x];i;i=next[i]){
		if(!vis[to[i]]){
			tree(to[i]);   //这个就和上司舞会一样了 
			dp[x][1]+=dp[to[i]][0];
			dp[x][0]+=min(dp[to[i]][1],dp[to[i]][0]);
		}
	}
}

void get_c(LL x){
	findd(x,-1);
	root=w;   //进行两次上司的舞会 
	tree(yx);
	temp=dp[w][0];  //不选 
	memset(vis,0,sizeof(vis));
	root=yx;  //第二次 
	tree(w);
	summ+=max(temp,dp[yx][0]);  //最两种情况里面最大的
	return; 
}

int main(){
	scanf("%lld",&n);
	LL x;
	for(int i=1;i<=n;i++){
		scanf("%lld %lld",&a[i],&x);
		adde(x,i);
		adde(i,x);
	}
	for(int i=1;i<=n;i++){
		if(!b[i]) get_c(i);
	}
	printf("%lld
",summ);
return 0;
}

  

 提高的题目

1771:仓库选址

喵星系有n个星球,星球以及星球间的航线形成一棵树。

从星球a到星球b要花费[dis(a,b)XorM]秒。dis(a,b)表示a,b间的航线长度,Xor为位运算中的异或)

为了给仓库选址,pf想知道,星球i1in到其他所有星球花费的时间之和。

第一行包含两个正整数nM

接下来n1行,每行3个正整数a,b,c,表示ab之间的航线长度为c

https://blog.csdn.net/qyxpsx7/article/details/104003550
https://blog.csdn.net/QTY2001/article/details/78208026?locationNum=7&fps=1
首先想想m=0的情况:完全不用^,那么两遍dfs就解决问题了。主题思路就是一边算儿子传到父亲的,一遍算父亲传到儿子的。
再考虑m=1的情况,只要知道有多少个点到自己的距离是奇数,多少是偶数就行。也就是两遍dfs时多传个参,再根据边权是奇是偶讨论一下就好了。
我们延续上面的思路,m<=15,也就是说只会改变后四位的值,同理,我们记录后四位的值就好了,传递时,只要把后四位的值+边权,再取后四位即可。

https://www.cnblogs.com/HLAUV/p/9890073.html
既然是棵树,又要快速地求每个点的值,那一定是树形DP加上换根的操作啦~

但是异或m要怎么处理呢?可以观察数据规模,发现m最大最大也就15,换成二进制数也就是 1111,所以发现异或m最多只会对数字的后面4位造成影响(异或0甚至无法造成什么影响)

于是愉快地写出DP数组 f[i]和sz[i][0~15]

f表示此时以i为根的子树到i节点的距离之和(减去后缀后的和)

sz表示此时距离以j为后缀的共有几个

每一次向根节点方向转移时会加上一条边的长度,此时不同后缀距离的后缀会发生相应改变,然后更新父亲相应后缀的sz值。

然后f里统计的距离总和是抹掉所有后缀后的总和,即不考虑后缀的贡献。如有一个距离是 10111(2),抹去长度为2的后缀后就只剩下10100,然后将这个结果加到f数组里,到最后根节点统计最终答案时再考虑每个后缀的贡献,此时的sz数组就派上用场了(具体看代码)

还有一点,就是最后要把答案减去m,因为统计后缀贡献时,多加了自己到自己的距离(本来为0,xor m 后变成了m)。

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e5+10;
const int INF=0x3fffffff;
typedef long long LL;

int n,m;
int tot;
struct node{
	int to,nex,w;
}ed[maxn<<1];
int head[maxn];
LL dp[maxn],path[maxn][16],sch[16];
//path表示i到其他点,后四位为j的路径条数
//sche后四位状态为j的向上的路径条数
void adde(int u,int v,int w){
	ed[++tot].nex=head[u];
	ed[tot].to=v;
	ed[tot].w=w;
	head[u]=tot;
}
void inti(){
	scanf("%d %d",&n,&m);
	int a,b,c;
	for(int i=1;i<=n-1;i++){
		scanf("%d %d %d",&a,&b,&c);
		adde(a,b,c);
		adde(b,a,c);
	}
}
void dfs1(int x,int fa){  // 
	path[x][0]=1;
	for(int i=head[x];i;i=ed[i].nex){
		int tt=ed[i].to;
		int ww=ed[i].w;
		if(tt==fa) continue;
		dfs1(tt,x);
		dp[x]+=dp[tt];
		for(int j=0;j<16;j++){
			path[x][(j+ww)%16]+=path[tt][j];  //路径条数 
			dp[x]+=1LL*path[tt][j]*ww;  //jieguo
		}
	}
}
//换根
void dfs2(int x,int fa){
	for(int i=head[x];i;i=ed[i].nex){
		int tt=ed[i].to;
		int ww=ed[i].w;
		if(tt==fa) continue;
		int sz=0;
		memset(sch,0,sizeof(sch));
		for(int j=0;j<16;j++){
			sch[(ww+j)%16]+=path[x][j]-path[tt][((j-ww)%16+16)%16];
			//都是路径条数 
			sz+=path[tt][j];
		}
		dp[tt]=dp[x]+(n-sz*2)*ww;
		for(int j=0;j<16;j++) path[tt][j]+=sch[j];
		dfs2(tt,x);
	}
} 
void calc(){
	for(int i=1;i<=n;i++){
		--path[i][0];
		for(int j=0;j<16;j++){
			int val=(j^m)-j;
			dp[i]+=1LL*path[i][j]*val;
		}
	}
} 
void work(){
	dfs1(1,0);
	dfs2(1,0);
	calc();
}
int main(){
   inti();
    work(); 
    for(int i=1;i<=n;i++) printf("%d
",dp[i]);
return 0;
}

  还是不是很懂

1772:动漫排序

WW最近迷上了日本动漫,每天都有无数部动漫的更新等着他去看,所以他必须将所有的动漫排个顺序,当然,虽然有无数部动漫,但除了1号动漫,每部动漫都有且仅有一部动漫是它的前传(父亲),也就是说,所有的动漫形成一个树形结构。而动漫的顺序必须满足以下两个限制:

①一部动漫的所有后继(子孙)都必须排在它的后面。

②对于同一部动漫的续集(孩子),小W喜爱度高的须排在前面。

光排序小WW还不爽,他想知道一共有多少种排序方案,并且输出它mod10007的答案。

第一行表示TT表示数据组数。接下来每组数据第一行nn表示有多少部动漫等待排序,接下来nn行每行第一个数tottot表示这部动漫有多少部续集,接下来tottot个数按照小WW喜爱从大到小给出它的续集的编号。

现需要把两个约束转化为一个约束(通过建树),然后运用插板法(排列组合)

对于限制2,因为大儿子一定比小儿子先遍历,那么我们可以将小儿子当做前一个比它大的儿子的儿子。

现在,限制就只剩下“先父亲后儿子”,即求遍历一棵树,当父亲被走过才可以走儿子的方案数。
显然,这是一棵二叉树。
设f[x]表示,遍历以x为根的子树的方案数。
转移:
设两个儿子分别为i,j(只有一个儿子的话,f[x]就等于儿子的f值),以i为根的子树大小为s1,j的为s2;
f[x]=f[i]∗f[j]∗Cmin(s1,s2)s1+s2
(C组合求的是插板问题)
其实就是当前有s1个点,按顺序插入s2个点中。

记住这个插板法的使用方法

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1010;
const int INF=0x3fffffff;
typedef long long LL;
typedef unsigned long long ull;
int t,n;
///https://blog.csdn.net/chen1352/article/details/73729564
//插板法
int mod=10007;
LL tree[maxn][2]; 
LL size[maxn];
LL f[maxn],jc[maxn*3],ny[maxn*3];
LL ksm(LL x,LL p){
    LL ans=1;
    while(p){
        if(p&1) ans=ans*x%mod;
        x=x*x%mod;
        p>>=1;
    }
    return ans;
}
LL c(LL mm,LL nn){  //here 插板法 其实就是当前有s1个点,按顺序插入s2个点中。
    if(nn>mm) swap(nn,mm);
    return jc[mm]*ny[nn]%mod*ny[mm-nn];   //在公式里面,ny[],ny[]都是在除号下面 
}
void js(int x){
    size[x]=1;
    int l=tree[x][0],r=tree[x][1];
    if(l) js(l);
    if(r) js(r);
    int s1=size[l];
    int s2=size[r]; //左右两边的方法相乘,然后乘以与大小相关的插板法 
    size[x]+=(s1+s2);
    if(l&&r){
        f[x]=f[l]%mod*f[r]%mod*c(s1+s2,min(s1,s2))%mod;
    }
    else if(l) f[x]=f[l];
    else f[x]=1;
    
}
int main(){
    scanf("%d",&t);
    jc[0]=ny[0]=1;
    for(int i=1;i<=3000;i++){
        jc[i]=(jc[i-1])*i%mod; //阶乘 
        ny[i]=ksm(jc[i],mod-2); //快速幂  这个阶数? 
    }
    
    while(t--){
        scanf("%d",&n);
        memset(tree,0,sizeof(tree));
        memset(size,0,sizeof(size));
        memset(f,0,sizeof(f));
        f[0]=1;
        for(int i=1;i<=n;i++){
            int last=i,x,tot;
            scanf("%d",&tot);
            for(int j=1;j<=tot;j++){
                scanf("%d",&x);
                if(!tree[last][0]) tree[last][0]=x;
                else tree[last][1]=x;
                last=x;
            }
        }
        js(1);
        printf("%lld
",f[1]);
    }
return 0;
}
View Code

1773:消息传递

H国的社会等级森严,除了国王之外,每个人均有且只有一个直接上级,当然国王没有上级。如果AA是BB的上级,BB是CC的上级,那么AA就是CC的上级。绝对不会出现这样的关系:AA是BB的上级,BB也是AA的上级。

最开始的时刻是0,你要做的就是用1单位的时间把一个消息告诉某一个人,让他们自行散布消息。在任意一个时间单位中,任何一个已经接到消息的人,都可以把消息告诉他的一个直接上级或者直接下属。

现在,你想知道:

①到底需要多长时间,消息才能传遍整个H国的所有人?

②要使消息在传递过程中消耗的时间最短,可供选择的人有哪些?

这个题如果数据范围小的话,可以用一种简单一点的写法,但是数据范围大就不行了

也是用到了换根dp

可以很方便的求出以u为根传递完它的子树的代价f[u]。
考虑如何求出传完它父亲及其上方点的代价。
使用换根dp。令g[u]表示去掉u及其子树并以u的父亲为根的全部代价。
将所有儿子的f与自己的g排序后即可求出所有儿子的g。
每个点的答案就很好计算了。

https://www.cnblogs.com/66t6/p/11622924.html
https://www.cnblogs.com/betablewaloot/p/12210779.html
另一种更更更简单的方法 !!!但是数据范围不行!因为加了一维,所以空间是不行的
加一维,空间换时间,同一颗子树在不同根进行dp的时候,如果父亲是一样的,那么结果就是一样的,可以直接调用---记忆化
拿出草稿纸,画一棵树如下: 1->2,3; 2->4,5; 3->6
这是把1当成根的情况,再画一棵树: 3->1,6; 1->2; 2->4,5
显然这是同一棵树,只是根不同,在常规解法中的作用是分别求出从1和3出发的最小值,但我们发现,在这两棵树中,dp[2]相等,因为他们都表示同一颗子树,
体现在这两棵树中2的父亲都是1,这就是突破口。

我们把状态加一维,变成dp[i][fa] (空间换时间,血赚不亏),表示以i节点为根的子树遍历完成的最小值,而fa值是根节点i的父亲,(若i是整棵树的根,那么fa=0),
那么这样在以不同根节点遍历树的时候,若以节点i为根的子树与之前计算过的以i为根节点的子树相同,即i的父亲相同,就可以直接调用。

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=2e5+10;
const int INF=0x3fffffff;
typedef long long LL;
typedef unsigned long long ull;
//感觉实在求树的中心   该店到树中其他点的距离,最远距离最小(可能有多个) 
//换根dp

int f[maxn],g[maxn]; //向下、向上所需要的时间
int n,head[maxn],pre[maxn],sub[maxn],ans[maxn],ans1=INF;
//pre,sub数组的使用! 
struct node{
    int to,nex;
}ed[maxn];
int tot;
void adde(int x,int y){
    ed[++tot].to=y;
    ed[tot].nex=head[x];
    head[x]=tot;
}
pair<int,int> b[maxn];
void dfs1(int now){  //这个是处理向下的数组 f[] 
    for(int i=head[now];i;i=ed[i].nex){
        dfs1(ed[i].to);
    }
    int cnt=0;
    for(int i=head[now];i;i=ed[i].nex){
        b[++cnt]=make_pair(f[ed[i].to],ed[i].to);
    }
    //直接排序得到 
    sort(b+1,b+1+cnt);
    //倒着传递,因为时间需要的越久越先传递
    for(int i=1;i<=cnt;i++){
        f[now]=max(f[now],b[i].first+cnt-i+1);
        //需要加1 
    } 
}
//注意一下:为了处理方便,我们并不一开始显式去掉i的儿子j,而是吧downk和upi加进去排序,在需要求出upj时我们二分查找j的位置,并把这个位置后面的位置的order值减一,
//因为我们在我们一开始的假设中,选j实际上是花了一个时间的,但实际并没有.实际操作上可以利用前缀和后缀max来实现
//换根 
void dfs2(int now){
    int cnt=0;
    for(int i=head[now];i;i=ed[i].nex){
        b[++cnt]=make_pair(f[ed[i].to],ed[i].to);
    }
    //这下面两个 
    if(now!=1) b[++cnt]=make_pair(g[now],-1); //前面已经算出来了 
    if(cnt==1&&now==1) g[now]=1;
    sort(b+1,b+1+cnt);
    for(int i=1;i<=cnt;i++) {
        ans[now]=max(ans[now],b[i].first+cnt-i+1);
        pre[0]=sub[cnt+1]=-1; 
    }
    for(int i=1;i<=cnt;i++) pre[i]=max(pre[i-1],b[i].first+cnt-i);
    for(int i=cnt;i>=2;i--) sub[i]=max(sub[i+1],b[i].first+cnt-i+1);
    for(int i=1;i<=cnt;i++){
        if(b[i].second!=-1){
            g[b[i].second]=max(pre[i-1],sub[i+1]);   //求出每个子树的g 
        }
    }
    for(int i=head[now];i;i=ed[i].nex) dfs2(ed[i].to);  //这里进行dfs 
}
int dann[maxn],ans2; 
int main(){
    scanf("%d",&n);
    for(int i=2;i<=n;i++){
        int x;
        scanf("%d",&x);
        adde(x,i);   //输入的是i的上级的编号 
    }
    dfs1(1);
    dfs2(1);
    for(int i=1;i<=n;i++){
        if(ans1>ans[i]) {
            ans1=ans[i];
            dann[ans2=1]=i;
        }
        else if(ans[i]==ans1) dann[++ans2]=i;
    }
    printf("%d
",ans1+1);
    for(int i=1;i<=ans2;i++) printf("%d ",dann[i]);
return 0;
}
View Code

1774:大逃杀

将地图上的所有地点标号为11到nn,地图中有n1n−1条双向道路连接这些点,通过一条双向道路需要一定时间,保证从任意一个点可以通过道路到达地图上的所有点。

有些点上可能有资源,到达一个有资源的点后,可以获取资源来增加 wiwi的武力值。资源被获取后就会消失,获取资源不需要时间。可选择不获取资源。

有些点上可能有敌人,到达一个有敌人的点后,必须花费titi秒与敌人周旋,并将敌人消灭。敌人被消灭后就会消失。不能无视敌人。

如果一个点上既有资源又有敌人,必须先消灭敌人才能获取资源。

游戏开始时Y君可以空降到任意一个点上,接下来,有T秒时间行动,Y君希望游戏结束时,武力值尽可能大。

首先,我们这题显然是一道树形DP题。
我们可以设三个状态转移方程——
f[i,j]进入以i为根的子树并返回到i
g[i,j]进入以i为根的子树但不返回到i
h[i,j]从以i为根的子树里一点出发,经过i点并调回以i为根的子树中
用了j秒的最大武力值

然后转移方式每种类型都要考虑到

https://blog.csdn.net/weixin_30296405/article/details/96045379 这个写的很好

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=310;
const int INF=0x3fffffff;
typedef long long LL;
typedef unsigned long long ull;
//讲解:https://blog.csdn.net/weixin_30296405/article/details/96045379 
//https://www.cnblogs.com/betablewaloot/p/12247208.html 
int n,t;
int tot,head[maxn],w[maxn],tt[maxn];
int f[maxn][maxn],g[maxn][maxn],h[maxn][maxn];
int ans=-1;
struct node{
    int to,nex,wei;
}ed[maxn<<1];
void adde(int a,int b,int c){
    ed[++tot].to=b;
    ed[tot].nex=head[a];
    ed[tot].wei=c;
    head[a]=tot;
}
void dfs(int now,int fa){
    if(tt[now]>t) return;  //边界条件 
    for(int i=head[now];i;i=ed[i].nex){
        int op=ed[i].to;
        if(op==fa) continue;
        dfs(op,now);
        for(int j=t;j>=tt[now];j--){  //注意这两个循环啊  这个是剩余的时间, 
            for(int k=j-ed[i].wei;k>=tt[now];k--){    //这个是走过这条路一次剩余的时间 
                int h1=f[now][k],h2=g[now][k],h3=h[now][k];
                int shen=j-k-ed[i].wei; //表示剩下的时间(跟式子是反的) 
                if(shen>=tt[op]){
                    g[now][j]=max(g[now][j],g[op][shen]+h1);
                    h[now][j]=max(h[now][j],g[op][shen]+h2);
                }
                shen-=ed[i].wei;
                if(shen>=tt[op]){
                    f[now][j]=max(f[now][j],f[op][shen]+h1);
                    g[now][j]=max(g[now][j],f[op][shen]+h2);
                    h[now][j]=max(h[now][j],h[op][shen]+h1);
                    h[now][j]=max(h[now][j],f[op][shen]+h3);
                }
            }
            ans=max(ans,h[now][j]); //最后的结果是比较这个 
        }
    }
}
int main(){
    scanf("%d %d",&n,&t);
    for(int i=1;i<=n;i++) scanf("%d",&w[i]);
    for(int i=1;i<=n;i++){
        scanf("%d",&tt[i]);
        f[i][tt[i]]=w[i];
        g[i][tt[i]]=w[i];
        h[i][tt[i]]=w[i];
    }
    int u,v,tim;
    for(int i=0;i<n-1;i++){
        scanf("%d %d %d",&u,&v,&tim);
        adde(u,v,tim);
        adde(v,u,tim);
    }
    dfs(1,0);
    printf("%d
",ans);
return 0;
}
View Code

1775:梦中漫步

梦游中的你来到了一棵NN个结点的树上。你一共做了Q个梦,每个梦需要你从点uu走到点vv之后才能苏醒。由于你正在梦游,所以每到一个结点后,你会在它连出去的边中等概率地选择一条边走过去。为了确保第二天能够准时到校,你要求出每个梦期望经过多少条边才能苏醒。为了避免精度误差,你要输出答案模109+7的结果。

树形dp+期望
//这个期望,概率的计算就不说了,这个得可以写出式子来,然后还要回结合树形这个结构来计算
//https://www.cnblogs.com/betablewaloot/p/12247263.html
/*
首先,设d[i]为i的出度,f[i]为从i走向根节点的期望步数,g[i]为从根节点走到i的期望步数
f[i]=1/d[i]+Σ(f[i]+f[son]+1)/d[i] 简化之后:f[i]=d[i]+Σf[son]

g[i]=1/d[fa[i]]+(1+g[i]+g[fa[i]])/d[fa[i]]+Σ(son!=i)(1+g[i]+f[son])/d[fa[i]] 简化之后 ;g[i]=d[i]+g[fa[i]]+Σf[son]
然后就用dfs预处理出这两个数组
f[i]=f[i]+f[fa],g[i]=g[i]+g[fa],求到最大的祖先的期望步数
对于一组询问u,v,路径就是u到lca,和v到lca
那么ans=f[u]-f[lca]+g[v]-g[lca]

期望是具有线性结构的

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=100010;
const int INF=0x3fffffff;
typedef long long LL;
typedef unsigned long long ull;
//树形dp+期望
//这个期望,概率的计算就不说了,这个得可以写出式子来,然后还要回结合树形这个结构来计算
//https://www.cnblogs.com/betablewaloot/p/12247263.html
/*
首先,设d[i]为i的出度,f[i]为从i走向根节点的期望步数,g[i]为从根节点走到i的期望步数
f[i]=1/d[i]+Σ(f[i]+f[son]+1)/d[i]  简化之后:f[i]=d[i]+Σf[son] 

g[i]=1/d[fa[i]]+(1+g[i]+g[fa[i]])/d[fa[i]]+Σ(son!=i)(1+g[i]+f[son])/d[fa[i]] 简化之后 ;g[i]=d[i]+g[fa[i]]+Σf[son]
然后就用dfs预处理出这两个数组
f[i]=f[i]+f[fa],g[i]=g[i]+g[fa],求到最大的祖先的期望步数
对于一组询问u,v,路径就是u到lca,和v到lca
那么ans=f[u]-f[lca]+g[v]-g[lca] 

*/ 
struct node{
    int to,nex;
}ed[maxn*2];
int head[maxn],tot;
int n,q;
LL f[maxn][20],deep[maxn],ans,k1[maxn],k2[maxn];
//k1是向下得期望路径条数,k2是向上得期望路径条数
//sol:向下:先dfs递归,然后返回求出,向上:先求出上面的,在向下计算,就是最后dfs递归 
//f是倍增数组,用来求LCA得 
int mod=1e9+7;
void adde(int x,int y){
    ed[++tot].to=y;
    ed[tot].nex=head[x];
    head[x]=tot;
}
void dfs(int x,int fa){   //求出k1数组 
    f[x][0]=fa;
    k1[x]=0;  
    for(int i=head[x];i;i=ed[i].nex){
        int tt=ed[i].to;
        if(tt!=fa){
            dfs(tt,x);
            k1[x]=(k1[x]+k1[tt]+1)%mod;
        }
        
    }
    k1[x]=(k1[x]+1)%mod;
}
void dfs1(int x,int fa){   //求出k2 数组 
    LL summ=0;
    for(int i=head[x];i;i=ed[i].nex){
        if(ed[i].to!=fa){
            summ=(summ+k1[ed[i].to]+1)%mod;  //儿子的 
        }
        else summ=(summ+k2[x]+1)%mod;   //计算父亲的 ,这个时候的值已经求出来了,因为是自上向下计算的 
    }
    for(int i=head[x];i;i=ed[i].nex){
        if(ed[i].to!=fa){
            k2[ed[i].to]=((summ-k1[ed[i].to])%mod+mod)%mod;
            dfs1(ed[i].to,x);
        }
    }
}
void dfs2(int x,int fa){  //这个是求出累计数组,因为 f[i]=f[i]+f[fa],g[i]=g[i]+g[fa],求到最大的祖先的期望步数
    deep[x]=deep[fa]+1;
    k1[x]=(k1[x]+k1[fa])%mod; //这样才可以最后 ans=f[u]-f[lca]+g[v]-g[lca] 这样计算
    k2[x]=(k2[x]+k2[fa])%mod;
    for(int i=head[x];i;i=ed[i].nex){
        if(ed[i].to!=fa) dfs2(ed[i].to,x);
    } 
}
int getlca(int x,int y){
    if(deep[x]<deep[y]) swap(x,y);
    int d=deep[x]-deep[y];
    if(d){
        for(int i=0;i<=log(n)/log(2)+1&&d;i++,d>>=1){
            if(d&1) x=f[x][i];
        }
    }
    if(x==y) return x;
    for(int i=log(n)/log(2)+1;i>=0;i--) {
        if(f[x][i]!=f[y][i]){
            x=f[x][i];y=f[y][i];
        }
    }
    return f[x][0];
}
int main(){
    scanf("%d %d",&n,&q);
    int u,v;
    for(int i=1;i<=n-1;i++){
        scanf("%d %d",&u,&v);
        adde(u,v);adde(v,u);
    }
    dfs(1,-1);  //求出k1 
    k1[1]=0;
    k2[1]=0; //正常的初始化别忘了
    dfs1(1,-1);
    dfs2(1,-1);
    //求出倍增数组
    for(int i=1;i<=log(n)/log(2)+1;i++){
        for(int j=1;j<=n;j++){
            f[j][i]=f[f[j][i-1]][i-1];
        }
    } 
    for(int i=1;i<=q;i++){
        scanf("%d %d",&u,&v);
        int lca=getlca(u,v);
        printf("%lld
",(k1[u]-k1[lca]+k2[v]-k2[lca]+mod)%mod);
    }
return 0;
}
View Code

换根dp

换根dp的通法:1.第一次扫描时,任选一个点为根,在有根树上执行一次树形DP,也就在回溯时发生的,自底向上的状态转移。

2.第二次扫描时,从刚才选出的根出发,对整棵树执行一次dfs,在每次递归前进行自上向下的推导,计算出换根后的解。

例题POJ3585 Accumulation Degree

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=200010;
const int INF=0x3fffffff;
typedef long long LL;
typedef unsigned long long ull;
int head[maxn];
struct node{
    int to,nex,w;
}ed[maxn<<1];
int dp[maxn],f[maxn],deg[maxn];
//dp表示表示rt为根时,直接往节点x灌水,然后向x的子树内流的最大流量
//令f[x]表示rt=x时的dp[x]。
//https://www.cnblogs.com/wxq1229/p/12317448.html 
int tot=0;
int t,n;
void adde(int u,int v,int w){
    ed[++tot].to=v;
    ed[tot].nex=head[u];
    ed[tot].w=w;
    head[u]=tot;
}
void dfs(int u,int fa){  //dp[u]只算了向下的 
    for(int i=head[u];i;i=ed[i].nex){
        int t=ed[i].to;
        if(t==fa) continue;
        dfs(t,u);
        if(deg[t]==1) dp[u]+=ed[i].w;
        else dp[u]+=min(dp[t],ed[i].w);
    }
}
//换根:最后递归
void getans(int u,int fa){
    for(int i=head[u];i;i=ed[i].nex){
        int t=ed[i].to;
        if(t==fa) continue;
        if(deg[u]==1) f[t]=dp[t]+ed[i].w;
        else f[t]=dp[t]+min(f[u]-min(dp[t],ed[i].w),ed[i].w);
        getans(t,u); 
    }
} 
void sol(){
    memset(deg,0,sizeof(deg));memset(dp,0,sizeof(dp));
    memset(f,0,sizeof(f));memset(head,0,sizeof(head));
    scanf("%d",&n);
    int u,v,w;
    tot=0;
    for(int i=1;i<=n-1;i++){
        scanf("%d %d %d",&u,&v,&w);
        adde(u,v,w);adde(v,u,w);
        ++deg[u];++deg[v];
    } 
    dfs(1,0);
    f[1]=dp[1]; //只需要这样赋值就可以了
    getans(1,0);
    int ans=0;
    for(int i=1;i<=n;i++) ans=max(ans,f[i]);
    printf("%d
",ans);
}
int main(){
    scanf("%d",&t);
    while(t--){
        sol();
    }
return 0;
}
View Code

LuoguP3478

#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<stack>
#include<cstdio>
#include<queue>
#include<map>
#include<vector>
#include<set>
using namespace std;
const int maxn=1e6+10;
const int INF=0x3fffffff;
typedef long long LL;
typedef unsigned long long ull;
//这个的推导也不难
/*
同样换根,令f[x]表示x为根时的所有节点的深度和。
考虑将根换到x的一个儿子v上,那么对于子树v里面的节点,他们的深度会减少1,其他节点深度会加上1,即(sizev表示v以1为根时的子树大小)
f[v]=f[x]+n-sizev-sizev=f[x]+n-2×sizev
先以1为根求出size,然后换根就好了
*/
int n,tot;
int head[maxn];
struct node{
    int to,nex;
}ed[maxn<<1];
LL f[maxn],dep[maxn];
int size[maxn];
//是求得深度和!!!,所以肯定要算深度啊! 
void adde(int u,int v){
    ed[++tot].to=v;
    ed[tot].nex=head[u];
    head[u]=tot;
}
void dfs1(int x,int fa,int deep){
    size[x]=1;
    dep[x]=deep;
    for(int i=head[x];i;i=ed[i].nex){
        int tt=ed[i].to;
        if(tt==fa) continue;
        dfs1(tt,x,deep+1);
        size[x]+=size[tt];
    }
}
void getans(int u,int fa){
    for(int i=head[u];i;i=ed[i].nex){
        int tt=ed[i].to;
        if(tt==fa) continue;
        f[tt]=f[u]+n-2ll*size[tt];
        getans(tt,u);
    }
} 
int main(){
    scanf("%d",&n);
    int x,y;
    for(int i=1;i<=n-1;i++){
        scanf("%d %d",&x,&y);
        adde(x,y);adde(y,x);
    }
    dfs1(1,0,0);
    for(int i=1;i<=n;i++) f[1]+=dep[i];
    getans(1,0);
    int ans=0;
    int mx=0;
    for(int i=1;i<=n;i++){
        if(ans<f[i]){
            ans=f[i];
            mx=i;
        }
    }
    printf("%d
",mx);
return 0;
}


#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define fore(i,x) for(int i=head[x],v=e[i].to;i;i=e[i].nxt,v=e[i].to)
const int N=1e6+10;
int n;
struct edge
{
    int to,nxt;
}e[N<<1];
int head[N],cnt=0;
inline void ade(int x,int y)
{e[++cnt]=(edge){y,head[x]},head[x]=cnt;}
inline void addedge(int x,int y)
{ade(x,y),ade(y,x);}
ll f[N],dep[N];
int siz[N];
void dfs(int x,int prev,int deep)
{
    dep[x]=deep,siz[x]=1;
    fore(i,x) if(v!=prev)
        dfs(v,x,deep+1),siz[x]+=siz[v];
}
void getans(int x,int prev)
{
    fore(i,x) if(v!=prev)
    {
        f[v]=f[x]+n-2ll*siz[v];
        getans(v,x);
    }
}
int main()
{
    scanf("%d",&n);
    for(int i=1,x,y;i<n;i++)
        scanf("%d%d",&x,&y),addedge(x,y);
    dfs(1,0,0); 
    for(int i=1;i<=n;i++) f[1]+=dep[i];
    getans(1,0);
    ll mx=0; int ans=0;
    for(int i=1;i<=n;i++)
        if(f[i]>mx) mx=f[i],ans=i;
    printf("%d
",ans);
    return 0;
}
View Code
原文地址:https://www.cnblogs.com/shirlybaby/p/12319687.html