树形dp总结

树形dp总结

这是我自认写得最认真最详细的一篇总结

套路:

1.一般设的状态是dp[u],因为阶段是由深的到浅的。

2.如果设出状态,但不好转移,可以考虑从底向上的top序或者dfs序来转移;

3.如果这样设不出状态,就考虑可以反向计算对答案的贡献。

4.一般是O(n)的,偶尔有带log的

模型:

1.树的直径

题意:给定一棵树,每条树边附带边权,求每个点的在这棵树上能够去到的最远距离。

考虑每个点,答案由两种情况构成。

1.从x到它子树中的一点。

2.从x经过它的父亲到其他点。其他点可能是父亲的父亲往上,也可能是父亲的儿子中不是x的另一个子树往下。

所以对于每个点,我们记录两个值,该点到树上任意一点的最远距离dp[x][1]和次远距离dp[x][2]。这需要用两个dfs进行更新。

第一个dfs更新从x到它子树的最远距离dp[x][1]和次远距离dp[x][2]。

第二个dfs更新从x经过它fa的最远距离dp[x][1]和次远距离dp[x][2]。

在第二次dfs的时候顺带更新答案即可。

然后我们发现其实一个点可以到达的最远的距离就是直径的端点。

上述方法就是用来求直径的(dp方法,另一种是两次dfs,始终找最远节点)。

2.树的重心

题意:给定一棵树,删掉一个点,使得树被分成的两个联通块的最大点数最小。

性质

1.树中所有点到某个点的距离和中,到重心的距离和是最小的,如果有两个重心,他们的距离和一样。

2.把两棵树通过一条边相连,新的树的重心在原来两棵树重心的连线上。

3.一棵树添加或者删除一个节点,树的重心最多只移动一条边的位置。

4.一棵树最多有两个重心,且相邻。

题型:

一.以子树为状态

HDU - 3586
题意:敌军有一队侦察兵,每个通讯兵只与他的直接上级单线联系,队长是1号,通讯系统构成树状结构;现在,总军司令要求你切断先锋通讯兵(叶子节点)与总队长(根节点)的联系,并且消耗体力不能超过m;求出满足条件的每次切断一条边的最大体力消耗值。
显然,答案满足单调性,直接二分。
dp[u]:切除以u为根的子树中的所有叶子的最小值。
dp[u]=min(dp[son], wi)

if(limit>w) dp[u]+=dp[son];

else dp[u]+=min(dp[son], wi);

void dfs(int u,int fa,int dis)
{
	int tmp = 0;
	for(int i = head[u]; i; i = e[i].nxt)
		{
			int v = e[i].v;	if(v == fa) continue;
			dfs(v,u,e[i].w);
			tmp += dp[v];
		}
	if(dis <= mid)
	{
		if(tmp) dp[u] = min(dis,tmp);
		else dp[u] = dis;
	}
	else if(tmp) dp[u] = tmp;
}

HDU 6201 transaction transaction transaction
题意:
给出一棵树,每个点有一个权值,代表商品的售价,树上每一条边上也有一个权值,代表从这条边经过所需要的花费。
现在需要你在树上选择两个点,一个作为买入商品的点,一个作为卖出商品的点,当然需要考虑从买入点到卖出点经过边的花费。使得收益最大。允许买入点和卖出点重合,即收益最小值为0。

定义阶段:dp[u]:以u为根的子树中的最大收益。

有以下情况:

1.不经过u:dp[u] = max{dp[v]}

2.经过u:用大减小。由于可能大和小在同一个v中,所以维护最值和次值,用v更新。

f[u][0]:买最小

f[u][1]:买次小

f[u][2]:卖最大

f[u][3]:卖次大

初值:最小和最大是u本身的价值。

void dfs(int u,int fa){
	f[u][0] = f[u][2] = val[u];
	f[u][1] = inf;f[u][3] = -inf;
	for(int i = head[u]; i != -1; i = e[i].nxt){
		int v = e[i].v;
		if(v != fa){
			dfs(v,u);
			dp[u] = max(dp[v],dp[u]);
			if(f[u][0] > f[v][0] + e[i].w){
				f[u][1] = f[u][0];
				pos[u][1] = pos[u][0];
				f[u][0] = f[v][0] + e[i].w;
				pos[u][0] = v;
			}
			else if(f[u][1] > f[v][0] + e[i].w){
				f[u][1] = f[v][0] + e[i].w;
				pos[u][1] = v;
			}
			if(f[u][1] > f[v][1] + e[i].w){
				f[u][1] = f[v][1] + e[i].w;
				pos[u][1] = v;
			}
			
			if(f[u][2] < f[v][2] - e[i].w){
				f[u][3] = f[u][2];
				pos[u][3] = pos[u][2];
				f[u][2] = f[v][2] - e[i].w;
				pos[u][2] = v;
			}
			else if(f[u][3] > f[v][2] - e[i].w){
				f[u][3] = f[v][2] - e[i].w;
				pos[u][3] = v;
			}
			if(f[u][3] > f[v][3] - e[i].w){
				f[u][3] = f[v][3] - e[i].w;
				pos[u][3] = v;
			}
		}
	}
	if(pos[u][0] != pos[u][2]) dp[u] = max(f[u][2] - f[u][0],dp[u]);
	else {
		dp[u] = max(f[u][3] - f[u][0],dp[u]);
		dp[u] = max(f[u][2] - f[u][1],dp[u]);
	}
}

Y
题目大意:给定一棵树,找出集合{a,b,c}使得没有简单路能够覆盖这三个点。(n<=1e5)

正向思考感觉要疯,所以反向思考,找一个简单路覆盖{a,b,c}.

此题肯定是是要枚举一个点的,只要另两个点分别位于它的两棵子树上,或一个在fa方向一个在子树上就可以。注意前者要除2.

void dfs(int u,int fa){
	sz[u]=1;
	for(int i=head[u];i!=-1;i=e[i].nxt){
		int v=e[i].v;
		if(v!=fa){
			dfs(v,u);
			sz[u]+=sz[v];	
		}	
	}
	for(int i=head[u];i!=-1;i=e[i].nxt){
		int v=e[i].v;
		if(v!=fa){
			up[u]+=(sz[v])*(n-sz[u]);
			down[u]+=(sz[v])*(sz[u]-1-sz[v]);
		}
	}
//	cout<

二.相邻几个节点的选择问题。

UVA - 1218
题意:有n台电脑,互相以无根树的方式连接,现要将其中一部分电脑作为服务器,且要求每台电脑必须连接且只能连接一台服务器(不包括作为服务器的电脑),求最少需要多少台电脑作为服务器。

与上题类似,这里是每台电脑必须连接且只能连接一台服务器,如果自己不是服务器,儿子,父亲,有且只有一台必须成为服务器。如果自己是服务器,儿子父亲是不是无所谓。

所以有三种状态。(此处的dp状态在转移上是没有问题的,但不是每个状态都合法。)

d(u,0):u是服务器,孩子是不是服务器均可

d(u,1):u不是服务器,u的父亲是服务器,u的孩子不能是服务器

d(u,2):u不是服务器,u的父亲不是服务器,u的孩子必须有且仅有一个是服务器。

显然:

d(u,0) = 1 + Sum( Min(d(v,1), d(v,0)) ) |v是u的孩子

d(u,1) = Sum(d(v,2))

d(u,2) = min{Sum(d(v,2)) - d(v0,2) + d(v0,0)}

而Sum(d(v,2)) == d(u,1)

void dfs(int u,int fa)
{
	int tmp = inf;dp[u][0] = 1;
	for(int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].v;	
		if(v != fa) 
		{
			dfs(v,u);
			dp[u][0] += min(dp[v][0],dp[v][1]);
			dp[u][1] += dp[v][2];
			tmp = min(tmp,dp[v][0] - dp[v][2]);
		}
	}
	dp[u][2] = dp[u][1] + tmp;
}

三.涉及换根的题目

一般都是两种状态:f[u]表示u的子树中…g[u]表示 u经过fa …
1.

CodeForces - 219D
题目大意:
给出一棵树,它的边是有向边,选择一个城市,问最少调整多少条边的方向能使一个选中城市可以到达所有的点,输出最小的调整的边数,和对应的点。

对于这种题,可以考虑把重新赋边权,把正向的边赋成0,反向的边赋成1即可直接输出。

考虑每个点,答案由两种情况构成。

1.从x到它的子树中的每一点。

2.从x 经过 它的父亲往上到达其他点。

状态是显然的,

dp[x]:从x到它的子树中的每一点的最小调整的边数。

f[x]:从x经过它的父亲往上到达其他所有点需调整的最小边数。

转移:

d p [ x ] = ∑ d p [ v ] + e [ i ] . w dp[x] = \sum{dp[v] + e[i].w} dp[x]=dp[v]+e[i].w

f[x] = dp[fa] - (dp[x] + e[i].w) + f[fa] + e[i^1].w

网上有把两个数组合成一个来写的,但思维跨度大,不推荐。

priority_queue ,greater > q;
void dfs_down(int u,int fa)
{
	for(int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].v;
		if(v == fa) continue;
		dfs_down(v,u);
		dp[u] += dp[v] + e[i].w;
	}
}
void dfs_up(int u,int fa)
{
	if(dp[u] < ans){ans = dp[u];while(!q.empty()) q.pop();q.push(u);}
	else if(dp[u] == ans) q.push(u);
	for(int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].v;
		if(v == fa) continue;
		if(e[i].w) dp[v] = dp[u] - 1;
		else dp[v] = dp[u] + 1;
		dfs_up(v,u);
	}
}
int main()
{
	while(~scanf("%d",&n))
	{
		memset(dp,0,sizeof(dp));memset(head,0,sizeof(head));ans = inf;k = 1;
		for(int i = 1; i < n; ++i) {int a,b;scanf("%d%d",&a,&b);adde(a,b,0);adde(b,a,1);}
		dfs_down(1,0);dfs_up(1,0);
		printf("%d\n",ans);
		while(q.size() != 1) {printf("%d ",q.top());q.pop();}
		printf("%d\n",q.top());		
	}
	return 0;
}

HDU5593 ZYB’s Tree
大致题意:
有n = 500000节点的树, 对于每个节点求距离此节点不超过K (K <= 10)的节点有多少个,把这个n个答案XOR后输出

1.f[u][i]表示以u为根的子树中,与u的距离为i的点的个数。

f[u][i]+=f[v][i-1];

2.g[u][i]表示经过u的fa的距u为i的点的个数。

有两种情况:

<1>.经过u的祖父往上。

<2>.u的其他儿子从u到v.

g[u][i]+=g[fa][i-1]+f[fa][i-1]-f[u][i-2];

不开longlong见祖宗!!

void dfs1(int u,int fa){
	f[u][0]=1;
	for(int i=head[u];i!=-1;i=e[i].nxt){
		int v=e[i].v;
		dfs1(v,u);
		for(int i=1;i<=k;++i){
			f[u][i]+=f[v][i-1];
		}
	}
}

void dfs2(int u,int fa){
	ll sum=1;
	if(u!=1) {
		g[u][1]=1;
		sum+=f[u][1]+1;
	}
	else sum+=f[u][1];
	for(int i=2;i<=k;++i){
		if(u!=1) g[u][i]+=g[fa][i-1]+f[fa][i-1]-f[u][i-2];
		sum+=g[u][i]+f[u][i];
	}
	ans^=sum;
	for(int i=head[u];i!=-1;i=e[i].nxt){
		dfs2(e[i].v,u);
	}
}

HDU 5834
Magic boy Bi Luo with his excited tree
题意:有n个点,每个点有一定的价值,走过每条边会消耗一定的价值,求从每个点出发能够获得的最大价值。

换根法的最经典例题,没有之一。

定义:(dp[u][]里没有包含u对答案的贡献,即未累加val[u])

dp[u][0]:在以u为根的子树中走,不回到u的最大价值。

dp[u][1]:…,回到u…。

dp[u][2]:经过u的fa,不回到u

dp[u][3] : …,回到u

转移:

dp[u][1]+=max(0,dp[v][1]+val[v]-2w)

dp[u][0]+=dp[u][1]+(dp[v][0]-w-(v对dp[u][1]的贡献))

dp[v][3]+=max(dp[u][3]+val[]-2*w,0)

dp[v][2]=两种情况取max

1.dp[u][3]+dp[u][0]-():又有两种情况。

(1)dp[v][0]是从u走的:dp[u][3]+dp[u][1]+t[u][1] (从fa的子树中走出的次大值)(原本应该是dp[u][0]减一坨,加一坨,但由于dp[u][0]是从dp[u][1]转移过来的,就把它化简了)。

(2)dp[v][0]不是从u走的:dp[u][3]+dp[u][0]-max(dp[v][1]+val[v]-2w,0);

2.dp[u][2]+dp[u][1]-max(dp[v][1]-2w+val[v],0);

void dfs1(ll u,ll fa){
	ll tmp0,tmp1,flg=0;//tmp0:v->0,tmp1:v->1
	for(ll i=head[u];i!=-1;i=e[i].nxt){
		ll v=e[i].v;
		if(v!=fa){
			flg=1;
			dfs1(v,u);
			tmp1=dp[v][1]+val[v]-2*e[i].w;
			tmp0=dp[v][0]+val[v]-e[i].w-max(tmp1,0LL);
			dp[u][1]+=max(tmp1,0LL);
			if(tmp0>t[u][0]){
				t[u][1]=t[u][0];
				t[u][0]=tmp0;
				pos[u]=v;
			}
			else if(tmp0>t[u][1]){
				t[u][1]=tmp0;
			}
		}
	}
	if(flg){
		dp[u][0]=dp[u][1]+max(0LL,t[u][0]);
	}
}

void dfs2(ll u,ll fa){
	for(ll i=head[u];i!=-1;i=e[i].nxt){
		ll v=e[i].v;
		if(v!=fa){
			ll tmp=max(0LL,dp[v][1]+val[v]-2*e[i].w);
			dp[v][3]=max(val[u]-e[i].w*2+dp[u][3]+dp[u][1]-tmp,0LL);
			if(pos[u]==v)
				dp[v][2]=max(0LL,val[u]-e[i].w+dp[u][3]+dp[u][1]-tmp+max(0LL,t[u][1]));
			else
				dp[v][2]=max(0LL,val[u]-e[i].w+dp[u][3]+dp[u][0]-tmp);
			dp[v][2]=max(dp[v][2],val[u]-e[i].w+dp[u][2]+dp[u][1]-tmp);
			dfs2(v,u);
			
		}
	}
}

ll a,b,c;

int main(){
//	freopen("a.txt","r",stdin);
	T=read();
	for(int testcase=1;testcase<=T;++testcase){
		n=read();
		kk=0;
		for(ll i=1;i<=n;++i){
			head[i]=-1;
			dp[i][0]=dp[i][1]=dp[i][2]=dp[i][3]=0;
			t[i][0]=t[i][1]=-inf;
		}
		
		for(ll i=1;i<=n;++i) val[i]=read();
		for(ll i=1;i

四.反向考虑贡献

在正向设dp[u]时设不出来,就反向考虑贡献

51Nod 1737 配对
题意:给出一棵n个点的树,将这n个点两两配对,求所有可行的方案中配对两点间的距离的总和最大为多少。n<=100000

直接设dp[v]发现设不出来,所以考虑每条边对答案的贡献。然后就极水。

ans+=w*min(sz[u],n-sz[u]); w表示u到它的fa的那条边的边权。
2.

HDU 6035 Colorful Tree
题目大意:给出一棵树,定义两点之间的距离为两点之间的颜色种类数(包括端点);求所有n*(n-1)/2条路径的距离和。

51Nod 1868 彩色树
给定一颗n个点的树,每个点一个[1,n]的颜色。
设g(x,y)表示x到y的树上路径上有几种颜色。
对于一个长度为n的排列P[1…n],定义 f ( P ) = ∑ i = 1 n − 1 g ( P i , P i + 1 ) f(P)=∑^{n-1}_{i=1}g(Pi,Pi+1) f(P)=i=1n1g(Pi,Pi+1)
现在求对于n!个排列,他们的f§之和
由于答案过大,你需要将答案对1e9+7取模

首先发现这个排列是假的,可以由树上n * (n-1)/2条路径 * (n-1)! * 2 取得。

如果直接设dp[u],方程都设不出来。

我们考虑反向的贡献。

先考虑每个颜色对答案的贡献,以红色为例:可以求出带红色的路径数,但此法需要考虑带一个红点,两个红点…不好转移。

所以反向考虑不含红色的路径数。于是可以把树上所有的红点删掉,统计剩下的联通块的节点数m,对答案的贡献就是(m-1)*m/2.但这样的极限复杂度是O(n^2),考虑优化。

我们就用sum[c]表示在已经dfs过的点中以颜色为c的节点为根的子树的节点数之和。在进入u时记录ps,在离开时更新。对u的每棵子树统计答案即可。

void dfs(ll u,ll fa){
	sz[u]=1;ll s_ps=sum[c[u]];
	for(ll i=head[u];i!=-1;i=e[i].nxt){
		ll v=e[i].v;
		if(v!=fa){
			
			ll n_ps=sum[c[u]];
			dfs(v,u);
			sz[u]+=sz[v];
			ll ns=sum[c[u]];
			ll m=ns-n_ps;
			ans-=(sz[v]-m)*(sz[v]-m-1)/2;
		}
	}
	ll ns=sum[c[u]];
	ll m=ns-s_ps;
	sum[c[u]]+=sz[u]-m;
}

ll a,b;

int main(){
//	freopen("a.txt","r",stdin);
	while(~scanf("%lld",&n)){
		
		memset(head,-1,sizeof(head));kk=0;
		memset(vis,0,sizeof(vis));
		memset(sum,0,sizeof(sum));
		col=0;
		
		for(ll i=1;i<=n;++i){
			c[i]=read();
			if(!vis[c[i]]){
				vis[c[i]]=1;
				col++;
			}
		}
		for(ll i=1;i

五.树形背包

1.分组背包

POJ-1947 Rebuilding Roads
题目大意:将一棵n个节点的有根树,删掉一些边变成恰有m个节点的新树。求最少需要去掉几条边。(N<=150)

显然,状态dp[u][i]表示以u为根的子树变成i个节点最少需要删去的边数。

删u到v这条边:dp[u][i]=dp[u][i]+1;

不删u到v这条边:dp[u][i]=min(dp[u][i],dp[u][i-j]+dp[v][j]);

注意一定要用没被v更新的dp[u][i-j]来转移,所以i需要倒序枚举。

#include 
#include 
#include 
#include 
using namespace std;
const int maxn = 150 + 5;
const int inf = 0x3f3f3f3f;
vector  t[maxn];
int num[maxn],dp[maxn][maxn],n,p,ans,son[maxn];
void dfs(int u)
{
	num[u] = 1;
	int l = t[u].size(); if(!l) {return;}
	for(int i = 0; i < l; ++i)
	{
		int v = t[u][i];
		dfs(v);
		num[u] += num[v];
		for(int j = num[u]; j >= 1; --j)//!!
			for(int k = 1; k < j; ++k)
				dp[u][j] = min(dp[u][j],dp[u][j - k] + dp[v][k] - 1);
	} 
}
int main()
{
	memset(dp,inf,sizeof(dp));
	scanf("%d%d",&n,&p);
	for(int i = 1; i < n; ++i){int a,b;scanf("%d%d",&a,&b);t[a].push_back(b);++son[a];}
	for(int i = 1; i <= n; ++i) dp[i][1] = son[i];
	dfs(1);
	ans = dp[1][p];
	for(int i = 2; i <= n; ++i) ans = min(ans,dp[i][p] + 1);
	printf("%d",ans);
	return 0;
}

POJ-1155 TELE
题意:有1个电视台,N-M-1个中转站,M个用户,电视台可以通过一些线路将信号传输到用户手中,给出每条线路的费用和每个用户可以支付的金额,输出在电视台不会产生亏损的同时能够给多少用户传输信号

题目分析:定义状态dp(u,k)表示从u开始到达k个叶子所花费的最小代价。则状态转移方程为:

dp[u][i]=min(dp[u][i],dp[u][j]+dp[v][i-j]-w)

同样,需要保证dp[u][j]是未被v更新过的,所以需要倒序枚举。

void dfs(int u,int fa)
{
	dp[u][0] = 0;
	for(int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].v;	dfs(v,u);
		for(int j = num[u]; j >= 0; --j)
			for(int l = 1; l <= num[v]; ++l)
				dp[u][j + l] = max(dp[u][j + l],dp[u][j] + dp[v][l] - e[i].w);
		num[u] += num[v];
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	memset(dp,-inf,sizeof(dp));
	for(int i = 1; i <= n - m; ++i){
		int l;scanf("%d",&l);
		while(l--) {int a,b;scanf("%d%d",&a,&b);adde(i,a,b);}
	}
	for(int i = n - m + 1; i <= n; ++i) {scanf("%d",&dp[i][1]);num[i] = 1;}
	dfs(1,0);
	for(int i = m; i >= 1; --i) if(dp[1][i] >= 0) {printf("%d\n",i);break;}
	return 0;
}

HDU-4003 Find Metal Mineral
题目大意:用m个机器人去遍历有n个节点的有根树,边权代表一个机器人通过这条边的代价,求最小代价。

题目分析:定义状态dp(root,k)表示最终遍历完成后以root为根节点的子树中有k个机器人时产生的总代价。

则状态转移方程为:

dp(root,k)=min(dp(root,k),dp(son,j)+dp(root,k-j)+j*w(root,son)) j>0

要注意,当j为0的时候表示遍历完son这个子树后所有的机器人都回到root。

2.树形依赖背包

往往有多种解法,O( n^3 )的类似树上分组背包的解法,O( n^2 )的倒序按dfs序直接背包法,左儿子右兄弟法(复杂,不推荐)
1.

洛谷P2014 选课
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有N门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程a是课程b的先修课即只有学完了课程a,才能学习课程b)。一个学生要从这些课程里选择M门课程学习,问他能获得的最大学分是多少?

设dp[u][i]表示在以u为根的子树中选择i门课能获得的最大学分。(这i门课不包括i本身)然后去枚举在v中选j门课。

因为在v中选了课,u就必须选,所以是dp[u][i-j-1].

复杂度O(n^3)

注意枚举:

//这是在枚举在v中选j门课,所以对应dp[v][j];如果方程是dp[u][i]=max(dp[u][i],dp[u][j]+dp[v][i-j-1])(方程是对的,但配上下面的for就不对了)就没有在v中枚举所有情况。
for(int j=0;j=0) dp[u][i]=max(dp[u][i],dp[u][i-j-1]+dp[v][j]);
}
void dfs(int u,int fa){
	siz[u]=1;
	for(int i=head[u];i!=-1;i=e[i].nxt){
		int v=e[i].v;
		if(v!=fa){
			dfs(v,u);
			siz[u]+=siz[v];
			for(int i=siz[u]-1;i>=0;--i){
				for(int j=0;j=0) dp[u][i]=max(dp[u][i],dp[u][i-j-1]+dp[v][j]);
				}
			}	
		}
	}
}

int a;

int main(){
//	freopen("a.txt","r",stdin);
	memset(head,-1,sizeof(head));
	n=read();m=read();
	for(int i=1;i<=n;++i){
		a=read();dp[i][0]=read();
		adde(a,i);adde(i,a);
	}
	dfs(0,0);
	printf("%d\n",dp[0][m]);
	return 0;
}

此题还有O(n^2)做法。

此题是按dfs序倒序枚举每个节点,(所以第一重循环是n+1到1)

如果取当前节点,dfn序的下一个节点(可能是儿子,也可能是并列的节点)就一定可以取。

如果不取,那只能从并列的节点转移过来。

void dfs(int u,int fa){
	pos[++idx]=u;
	siz[u]=1;
	for(int i=head[u];i!=-1;i=e[i].nxt){
		int v=e[i].v;
		dfs(v,u);
		siz[u]+=siz[v];
	}
}
int main(){
//	freopen("a.txt","r",stdin);
	memset(head,-1,sizeof(head));
	n=read();m=read();
	for(int i=1;i<=n;++i){
		a=read();val[i]=read();
		adde(a,i);
	}
	dfs(0,0);
	for(int i=n+1;i>=1;--i)
		for(int j=1;j<=m+1;++j)
			dp[i][j]=max(dp[i+1][j-1]+val[pos[i]],dp[i+siz[pos[i]]][j]);
	printf("%d\n",dp[1][m+1]);
	return 0;
}

题意:有一棵树,树上有n个节点,每个节点有一定的美味值和毒素,取节点x之前必须取x的父亲。求在毒素不超过m的情况下的最大美味值。

用val表示美味值,p表示毒素。

此题是按dfs序倒序枚举每个节点,

如果取当前节点,dfn序的下一个节点(可能是儿子,也可能是并列的节点)就一定可以取。

如果不取,那只能从并列的节点转移过来。

void dfs(ll u,ll fa){
	dfn[u] = ++tot; c[tot] = u;
	for(int i = head[u]; i != -1; i = e[i].nxt){
		if(e[i].v != fa){
			dfs(e[i].v,u);
		}
	}
	ls[u] = tot;
}
ll a,b;

int main(){
	memset(head,-1,sizeof(head));
	n = read();m = read();
	for(ll i = 1; i <= n; ++i){
		val[i] = read();p[i] = read();
	}
	for(ll i = 1; i < n; ++i){
		a = read();b = read();
		adde(a,b);adde(b,a);
	}
	
	dfs(1,0);
	for(int i = p[c[n]]; i <= m; ++i){
		dp[n][i] = val[c[n]];
	}
	
	for(int i = n - 1; i >= 1; --i){
		for(int j = 0; j <= m; ++j){
			if(j >= p[c[i]]) dp[i][j] = max(dp[i][j],dp[i + 1][j - p[c[i]]] + val[c[i]]);
			dp[i][j] = max(dp[i][j],dp[ls[c[i]] + 1][j]);
		}
	}
	
	printf("%lld\n",max(dp[1][m],0LL));
	
	return 0;
}

六.综合题型

1.与lca结合。

hdu 5449 Robot Dog
题意:一棵n个节点的树,树上有k个宝石,编号1~k,现在从起点s放一条电子狗,电子狗在每个节点往各邻接点走的概率相同,问电子狗按编号顺序拿完所有宝石的期望步数.
Input
第一行一整数T表示用例组数,每组用例首先输入一整数n表示点数,之后n-1行每行两个整数u和v表示u和v在树上有一条边,之后输入一整数q表示查询数,最后q行每行首先输入一个整数k表示宝石数量,然后输入一整数s表示电子狗起点,最后k个整数表示编号从1~k的宝石在树上的位置(T<=10,2<=n<=50000,q<=100,0<=k<=500)
Output
对于每次查询,输出电子狗按顺序拿完所有宝石的期望步数,相邻两组用例的输出用一空行隔开

其实就是求x到y的期望步数。

可是不可能预处理出任意两点的期望步数,所以只能用lca转换。

于是我们需要从x到lca的期望步数,从lca到y的期望步数。

从而想到了两个状态:ff[u]:从u到根的期望步数,gg[u]:从根到u的期望步数。

仍然不好转移。再定义两个辅助状态:f[u]:从u到fa的期望步数,g[u]:从fa到u的期望步数。

考虑怎样转移:

f [ u ] = 1 d u + ∑ ( f [ v ] + 1 ) ∗ 1 d u f[u]=\frac{1}{d_{u}}+\sum{(f[v]+1)*\frac{1}{d_{u}}} f[u]=du1+(f[v]+1)du1

g [ u ] = 1 d f a + ∑ 1 + g [ u ] + f [ v ] ∗ 1 d f a + 1 d f a ∗ ( g [ f a ] + g [ u ] ) g[u]=\frac{1}{d_{fa}}+\sum{1+g[u]+f[v]}*\frac{1}{d_{fa}}+\frac{1}{d_{fa}*(g[fa]+g[u])} g[u]=dfa1+1+g[u]+f[v]dfa1+dfa(g[fa]+g[u])1//v是fa的其他儿子。

化简计算就好,样例里的小数是在骗人。注意题目中的点是0到n-1.

void dfs1(int u,int fath){
	d[u]=d[fath]+1;fa[u][0]=fath;
	for(int i=head[u];i!=-1;i=e[i].nxt){
		int v=e[i].v;
		if(v!=fath){
			dfs1(v,u);
			f[u]+=1+f[v];
		}
	}
	if(u!=1)f[u]+=1;
}

void dfs2(int u,int fath){
	for(int i=head[u];i!=-1;i=e[i].nxt){
		int v=e[i].v;
		if(v!=fath){
			ff[v]=ff[u]+f[v];	
			g[v]=f[u]-f[v]+g[u];
			gg[v]=gg[u]+g[v];
			dfs2(v,u);
		}
	}
}

int Lca(int u,int v){
	if(d[u]=0;--i){
		if(d[fa[u][i]]>=d[v]) u=fa[u][i];
	}
	if(u==v) return u;
	for(int i=D;i>=0;--i){
		if(fa[u][i]!=fa[v][i]){
			u=fa[u][i];
			v=fa[v][i];
		}
	}
	return fa[u][0];
}

int a,b,la;

int main(){
	scanf("%d",&T);
	while(T--){
		memset(head,-1,sizeof(head));k=0;
		memset(f,0,sizeof(f));
		memset(g,0,sizeof(g));
		memset(ff,0,sizeof(ff));
		memset(gg,0,sizeof(gg));
		memset(fa,0,sizeof(fa));
		
		scanf("%d",&n);
		for(int i=1;i

2.与数据结构结合

POJ - 3162
题意:对一棵树,求出从每个结点出发能到走的最长距离(每个结点最多只能经过一次),将这些距离按排成一个数组得到d[1],d[2],d[3]……d[n] ,在数列的d中求一个最长的区间,使得区间中的最大值与最小值的差不超过m。

第一个问直接求直径即可,第二问有两种维护方式,单调队列 or 线段树。

直接尺取法,枚举每个l,如果r满足条件就尝试+1,如果不行就l++。因为显然的右移l,不会使最小值变小,也不会使最大值最大。

3.与树上贪心的对比

树上贪心:从极致点入手(如果是序列就是第一个,最后一个…如果是树,一般是叶子,根…)

51nod 1378 夹克老爷的愤怒
诺德县 有N个村庄,编号0 至 N-1,这些村庄之间用N - 1条道路连接起来。
家丁都是经过系统训练的暴力机器,每名家丁可以被派驻在一个村庄,并镇压当前村庄以及距离该村庄不超过K段道路的村庄。
夹克老爷一贯奉行最小成本最大利润的原则,请问要实现对全部村庄的武力控制,夹克老爷需要派出最少多少名家丁?

从叶子往上,能不安排就不安排,用dp[u]表示当前距家丁还有多远,正数表示在家丁下面,负数表示在家丁上面。

0表示当前位置必须放家丁。注意处理u放家丁时,它对它的兄弟的管理这种情况。


void dfs(int u,int fa){
	int tmp=0,flg=-inf;
	for(int i=head[u];i!=-1;i=e[i].nxt){
		int v=e[i].v;
		if(v!=fa){
			++tmp;
			dfs(v,u);
			if(dp[v]>0)	dp[u]=min(dp[u],dp[v]-1);
			else 	flg=max(flg,dp[v]);
		}
	}
	if(!tmp) dp[u]=k;
	if(flg!=-inf){
		if(dp[u]==inf) dp[u]=k;
		if(flg+dp[u]>0) dp[u]=flg-1;
	}
	if(!dp[u])	++cnt;
}

int a,b;

int main(){
//	freopen("a.txt","r",stdin);
	memset(dp,inf,sizeof(dp));
	memset(head,-1,sizeof(head));
	n=read();k=read();
	for(int i=1;i0) cnt++;
	printf("%d\n",cnt);
	return 0;
}

VS

Fire POJ - 2152
题目大意:有N座城市,要求在这N座城市中建一个消防系统,使得每座城市城市着火时都能被按时扑灭
现在给出每座城市建一个消防站所需的花费wi和每座城市相邻消防站的最远距离lim_i(和该城市距离超过lim的城市的消防站无法救该城市的火),问要使所有的城市都能被救到火,建消防站的最小花费是多少.

设dp[i][j]表示以i城市为根的树依赖j城市消防站的最低花费(该树的每个节点都符合要求),best[i]为以i城市为根的树最低消费(该树的所有节点都符合要求) 。

那么状态转移方程就是

dp[i][j] = w[j] + sum (min(dp[k][j] - w[j], best[k])) (k为i的子节点)

dp[k][j]表示因为k可以和i节点一样依赖于j,那么花费就是dp[k][j]-w[j],或者k节点不依赖于j,而依赖于别的节点,那就是best[k]了。

此题就需要O( n^2 )预处理两个点之间的距离。枚举被依赖的节点j,也是O( n^2 )的。

七.树的切割与联通问题

51nod 1353 树
题意:
切断一棵树的任意条边,这棵树会变成一棵森林。
现要求森林中每棵树的节点个数不小于k,求有多少种切法。
数据范围:n≤2000。

对于节点u,它所在的联通块只能有三种情况:与儿子一起,与父亲一起,与自己一起。然后就发现可以分成是否切断它和儿子的联系。

一定要注意更新的顺序。

https://www.cnblogs.com/RabbitHu/archive/2017/10/30/51nod1353.html的题解很详细。

void dfs(int u,int fa){
	dp[u][1]=1;sz[u]=1;
	for(int i=head[u];i!=-1;i=e[i].nxt){
		int v=e[i].v;
		if(v!=fa){
			dfs(v,u);
			for(int j=sz[u];j>=1;--j){
				for(int k=1;k<=sz[v];++k){
					dp[u][j+k]=(dp[u][j+k]+dp[u][j]*dp[v][k]%mod)%mod;
				}
				dp[u][j]=dp[u][j]*dp[v][0]%mod;
			}
			sz[u]+=sz[v];
		}
	}
	for(int i=kk;i<=sz[u];++i){
		dp[u][0]=(dp[u][0]+dp[u][i])%mod;
	}
//	cout<

51nod 1500 苹果曼和树
苹果曼有一棵n个点的树。有一些(至少一个)结点被标记为黑色,有一些结点被标记为白色。
现在考虑一个包含k(0 ≤ k < n)条树边的集合。如果苹果曼删除这些边,那么会将这个树分成(k+1)个部分。每个部分还是一棵树。
现在苹果曼想知道有多少种边的集合,可以使得删除之后每一个部分恰好包含一个黑色结点。答案对1000000007 取余即可。

定义:dp[i][0/1]表示在u的子树中u所在的联通块没有/有一个黑节点。

分u和v联通和不连通两种情况更新。

注意枚举顺序。两个方程右边的dp[u][0],dp[u][1]必须是没有被v更新过的u。

int dfs(int u,int fa){
	dp[u][x[u]]=1;
	for(int i=head[u];i!=-1;i=e[i].nxt){
		int v=e[i].v;
		if(v!=fa){
			dfs(v,u);
			//!!
			dp[u][1]=((dp[u][1]*(dp[v][0]+dp[v][1])%mod)%mod+dp[u][0]*dp[v][1]%mod)%mod;
			dp[u][0]=(dp[u][0]*(dp[v][0]+dp[v][1])%mod)%mod;
		}
	}
}

int a;

int main(){
//	freopen("a.txt","r",stdin);
	memset(head,-1,sizeof(head));
	n=read();
	for(int i=1;i

51Nod 1677 treecnt
给定一棵n个节点的树,从1到n标号。选择k个点,你需要选择一些边使得这k个点通过选择的边联通,目标是使得选择的边数最少。
现需要计算对于所有选择k个点的情况最小选择边数的总和为多少。

其实显然一条边带来一个点,最小边数是k-1。然后就是求在树中选出大小为k的联通块的方案数。

实际上我们反向考虑每条边对于答案的贡献就是经过这条边形成的大小为k的联通块数目。

再反向一下,就是不经过这条边形成大小为k的联通块的数目。

题上又指明是集合,不是排列,组合数计算就好。

ll C(ll x,ll y){
	if(y>x) return 0;
	return fac[x]*inv[x-y]%mod*inv[y]%mod;
}

void dfs(int u,int fa){
	sz[u]=1;
	for(int i=head[u];i!=-1;i=e[i].nxt){
		int v=e[i].v;
		if(v!=fa){
			dfs(v,u);
			sz[u]+=sz[v];
		}
	}
	if(u!=1) ans=((ans+C(n,m)-C(sz[u],m)-C(n-sz[u],m))%mod+mod)%mod;
//	cout<>=1;
	}
	return ret;
}

int main(){
//	freopen("a.txt","r",stdin);
	memset(head,-1,sizeof(head));
	fac[0]=fac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<=100000;++i) {
		fac[i]=fac[i-1]*i%mod;
		inv[i]=Power(fac[i],mod-2);
	}
	n=read();m=read();
	for(int i=1;i

七.关于任意两点的问题

51Nod 1405 树的距离之和
给定一棵无根树,假设它有n个节点,节点编号从1到n, 求任意两点之间的距离(最短路径)之和。
Input
第一行包含一个正整数n (n <= 100000),表示节点个数。
后面(n - 1)行,每行两个整数表示树的边。
Output
每行一个整数,第i(i = 1,2,…n)行表示所有节点到第i个点的距离之和。

先考虑题目标题,树上的距离之和:设dp[u]是无法设出状态的,所以考虑每条边的贡献,就是将这条边切断后两个联通块的点的乘积。

对于实际的这道题,设dp[u][0]表示u的子树中的点到u的距离之和,dp[u][1]表示经过u的父亲到u的距离之和。

void dfs(ll u,ll fa){
	sz[u]=1;
	for(ll i=head[u];i!=-1;i=e[i].nxt){
		ll v=e[i].v;
		if(v!=fa){
			dfs(v,u);
			sz[u]+=sz[v];
			dp[u][0]+=dp[v][0]+sz[v];
		}
	}
}

void dfs1(ll u,ll fa){
	for(ll i=head[u];i!=-1;i=e[i].nxt){
		ll v=e[i].v;
		if(v!=fa){
			dp[v][1]=dp[u][1]+dp[u][0]-dp[v][0]-sz[v]+(n-sz[v]);
			dfs1(v,u);
		}
	}
}

ll a,b,ans[N];

int main(){
//	freopen("a.txt","r",stdin);
	memset(head,-1,sizeof(head));
	n=read();
	for(ll i=1;i

幸运树 51Nod - 1588
有一棵树,它的边分为特殊边(边权只由数字4,7组成)和普通边,求树中满足以下条件的三元组tr(i,j,k)(i,j,k是三个不同的点)的个数。

1.i到j有路径,i到k也有路径
2.每条路径中至少有一条幸运边。
数字的顺序是有意义的,举例说明,tr(1,2,3),tr(1,3,2),tr(2,1,3)是三个不同的序列。

显然,这道题是无法通过设u的子树这样的状态转移的。

于是我们注意到了题上说明每个满足题意的路径至少有一条特殊边,但又不好直接求满足题意的三元组的个数,所以我们反向求出不满足题意的三元组数目。

我们考虑删掉特殊边,在形成的联通块中找这样的点,有两种情况:

1.在当前联通块中选三个点。

2.在当前联通块中选两个点,另一个联通块中选一个点(不能从这个点向那两个点连边),所以这里只能乘2(就是三个点有4种排列方式,原本应该有6种).

void dfs(ll u,ll fa){
	vis[u]=1;sz[cnt]++;
	for(ll i=head[u];i!=-1;i=e[i].nxt){
		ll v=e[i].v;
		if(v!=fa){
			dfs(v,u);
		}
	}
}

bool Check(int x){
	while(x){
		if(x%10!=4&&x%10!=7) return false; 
		x/=10;
	}
	return true;
}

int a,b,c;

int main(){
	memset(head,-1,sizeof(head));
	n=read();
	for(ll i=1;i=3) ans-=sz[i]*(sz[i]-1)*(sz[i]-2);
		if(sz[i]>=2) ans-=2*sz[i]*(sz[i]-1)*(n-sz[i]);
	}
	printf("%lld\n",ans);
	
	return 0;
}

八.情况复杂型

51Nod 1299 监狱逃离
监狱有N条道路连接N + 1个交点,编号0至N,整个监狱被这些道路连在一起(任何2点之间都有道路),人们通过道路在交点之间走来走去。其中的一些交点只有一条路连接,这些点是监狱的出口。在各个交点中有M个点住着犯人(M <= N + 1),剩下的点可以安排警卫,有警卫把守的地方犯人无法通过。给出整个监狱的道路情况,以及犯人所在的位置,问至少需要安排多少个警卫,才能保证没有1个犯人能够逃到出口,如果总有犯人能够逃出去,输出-1。

考虑从下往上递推,每个点可以有三种情况

0:这个点为根的子树中所有的逃犯都无法到达这个点,且不存在一条从这点到叶子节点的路径。

1:这个点为根的子树中所有的逃犯都无法到达这个点,但存在一条从这点到叶子节点的路径。

2:这个点为根的子树中所有的逃犯有可能到达这个点,且不存在一条从这点到叶子节点的路径。

有以下情况:

1.如果这个点有逃犯,那么这个点以下的1点都必须被封死,要不然逃犯可以通过这个点到叶子节点,所以答案加上当前节点状态为1的儿子个数。

2.如果这个点是叶子,它的状态为1.

3.那么考虑一个点,如果它的所有儿子都被封死了(就是状态0),那么它自己也就被封死了。

4.否则如果当前点既有状态为1的儿子,又有状态为2的儿子,那么2的逃犯就可以到达这个点,然后从这个点往下逃走,所以这个点必须被封死。

5.否则就是只有0\1,0\2的情况,那么除0外是几这个点的状态就是几。

最后如果根节点的状态为2,根节点也要封死。

void dfs(int u,int fa){
	int flg0=0,flg1=0,flg2=0;
	for(int i=head[u];i!=-1;i=e[i].nxt){
		int v=e[i].v;
		if(v!=fa){
			dfs(v,u);
			if(dp[v]==0) 	flg0++;
			else if(dp[v]==1)	flg1++;
			else if(dp[v]==2)	flg2++;
		}
	}
	if(c[u]){
		dp[u]=2;
		ans+=flg1;
	}
	else if(flg0==0&&flg1==0&&flg2==0) dp[u]=1;
	else if(flg1&&flg2) {
		dp[u]=0;
		ans++;
	}
	else{
		if(flg1) dp[u]=1;
		else if(flg2) dp[u]=2;
		else dp[u]=0;
	}
}

int a,b;

int main(){
//	freopen("a.txt","r",stdin);
	memset(head,-1,sizeof(head));
	n=read();m=read();
	for(int i=1;i<=n;++i){
		a=read();b=read();
		adde(a,b);adde(b,a);
		d[a]++;d[b]++;
	}
	for(int i=1;i<=m;++i){
		a=read();
		c[a]++;
	}
	for(int i=0;i<=n;++i){
		if(d[i]==1){
			dfs(i,-1);
			if(dp[i]==2) ans++;
			printf("%d\n",ans);
			break;
		}
	}
	
	return 0;
}

附上一道CF不知道是什么算法的题。

HDU 6161 Big binary tree
题意:
给你n个点,和m条操作,n个点构成一个完全二叉树,一开始每个点的权值为该点编号。
每个操作可能为
1.询问经过x点的最大路径(该路径上的点权之和最大)。
2.修改x点的权值。

看过题解后做,发现不是MLE就是TLE。

只是学到了在操作多,不好下手时,按顺序将每个操作分解来写。

map的两个非常常见的操作。

1.if(!mp[i])->if(!mp.count(i))

当map[i]存在时,返回1,否则返回2.

2.当写了这样的代码时:

return mp[a]=(get_dp(u));

首先插入mp[a]=0;

然后调用get_dp(u);

最后把get_dp(u)的值赋给mp[a];

dp[x]表示x或x的子孙在更新后的值。(只有更新的点或它们的祖先才有dp[]值)

ll n,m;

map  mp,dp; 

ll get_dp(ll x){
	if(dp.count(x)) return dp[x];//!!如果dp[x]有值,dp[x>>1]一定有值。
	if(x>n) return 0;
	ll tmp=get_dp(x<<1|1)+((mp.count(x))?mp[x]:x);
	return tmp;
}

ll query(ll x){
	ll up=get_dp(x);
	int i=x>>1,flg=(x&1);
	ll ret=get_dp(x<<1)+get_dp(x<<1|1)+(mp.count(x)?mp[x]:x);
	while(i){	
		int tmp=0;
		up+=(mp.count(i)?mp[i]:i);
		if(flg==1) tmp+=get_dp(i<<1);
		if(flg==0) tmp+=get_dp(i<<1|1);
		ret=max(ret,up+tmp);
		flg=(i&1);
		i>>=1;
	}
	return ret;	
}

char s[10];
ll x,y;

int main(){
//	freopen("a.txt","r",stdin);
	while(~scanf("%lld%lld",&n,&m)){
		mp.clear();dp.clear();
		
		int la=n;dp[n]=n;		
		for(ll i=n>>1;i>=1;i>>=1){
			dp[i]=i+dp[la];
			la>>=1;
		}

		for(ll i=1;i<=m;++i){
			scanf("%s",s);
			if(s[0]=='c'){
				x=read();y=read();
				mp[x]=y;
				for(ll j=x;j>=1;j>>=1){
					ll a=get_dp(j<<1),b=get_dp(j<<1|1);
					dp[j]=max(a,b)+(mp.count(j)?mp[j]:j);

				}
			}
			else{
				x=read();
				write(query(x));putchar('\n');
			}
		}
	}
	return 0;
}

参考:
https://blog.csdn.net/V5ZSQ

http://www.cnblogs.com/kuangbin/archive/2012/08/29/2661928.html

https://blog.csdn.net/l123012013048/article/details/45768047

https://www.cnblogs.com/20143605–pcx/p/5351382.html

https://blog.csdn.net/alan_cty/article/details/53769796

你可能感兴趣的:(dp)