树形dp的题一般都结合着背包来用。以下的几道题都是结合着背包的思想来的
1:树形dp+分组背包 状态比较难想
之前说过在最长距离的那道题里说过,不会返回。但是有的提示需要考虑返回的节点的。
下面就是一个例子:
Apple Tree
题目大意:
给你一个苹果树,有N个节点,每个节点上都有一个一个苹果也就有一个权值,当你经过这个点将得到权值,重复走节点只能算一次,给N-1条边。问你走k步能得到的最大权值和。
dp[i][j][0]表示从i节点出发最后回到i节点花费最多j步能获得的最大值,
dp[i][j][1]表示从j节点出发最后不回到i节点花费最多j步能获得的最大值
dp[root][j][0] = max(dp[root][j-k][0] + dp[son][k-2][0])
//root->son 和 son->root 共花费2步,j-k是在其他儿子花费的步数
dp[root][j][1] = max(dp[root][j-k][0] + dp[son][k-1][1])
dp[root][j][1] = max(dp[root][j-k][1] + dp[son][k-2][0])
//只需要回到root一次,一种情况是先走其他儿子回到root再走son,
//另一种情况是先走son这个儿子并回到root再去走其他儿子
代码:
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
const int M=210;
int wi[M];
int dp[M][M][2];//0不回来,1回来
int n,m;
int ne;
struct Edge
{
int to,nxt;
}edge[M*2];
int st[M];
void add_edge(int fr,int to)
{
edge[ne].to=to;
edge[ne].nxt=st[fr];
st[fr]=ne++;
}
int isu[M];
void dfs(int rt)
{
isu[rt]=1;
for(int i=0;i<=m;i++)
dp[rt][i][0]=dp[rt][i][1]=wi[rt];
for(int i=st[rt];i!=-1;i=edge[i].nxt)
{
int t=edge[i].to;
if(isu[t])
continue;
dfs(t);
for(int j=m;j>=1;j--)
{
for(int k=1;k<=j;k++)
{
dp[rt][j][0]=max(dp[rt][j][0],dp[rt][j-k][1]+dp[t][k-1][0]);
dp[rt][j][0]=max(dp[rt][j][0],dp[rt][j-k][0]+dp[t][k-2][1]);
dp[rt][j][1]=max(dp[rt][j][1],dp[rt][j-k][1]+dp[t][k-2][1]);
}
}
}
}
int main()
{
int a,b;
while(scanf("%d%d",&n,&m)!=EOF)
{
memset(dp,0,sizeof(dp));
ne=0;
memset(st,-1,sizeof(st));
memset(isu,0,sizeof(isu));
//m++;
for(int i=1;i<=n;i++)
scanf("%d",&wi[i]);
for(int i=1;i<=n-1;i++)
{
scanf("%d%d",&a,&b);
add_edge(a,b);
add_edge(b,a);
}
dfs(1);
printf("%d\n",max(dp[1][m][0],dp[1][m][1]));
}
return 0;
}
2:
The more, The Better
题目大意:
ACboy很喜欢玩一种战略游戏,在一个地图上,有N座城堡,每座城堡都有一定的宝物,在每次游戏中ACboy允许攻克M个城堡并获得里面的宝物。但由于地理位置原因,有些城堡不能直接攻克,要攻克这些城堡必须先攻克其他某一个特定的城堡。你能帮ACboy算出要获得尽量多的宝物应该攻克哪M个城堡吗?
思路分析:给你一棵树,欲取子节点,必取父节点。问取的价值最多为多少。
简单的树形+背包
代码:
#include<iostream>
#include<string.h>
#include<stdio.h>
#include<vector>
#define MAX 500
using namespace std;
vector<int>q[MAX];
int dp[MAX][MAX];
int v[MAX];
void dfs(int n,int m)
{
int len=q[n].size();
int k,h;
dp[n][1]=v[n];//打掉该点获得的财富值
for(int i=0;i<len;i++)//去攻击他的下边的节点
{
if(m>1)//次数还没有超,还能再攻击
dfs(q[n][i],m-1);
for(int j=m;j>=1;j--)
{
h=j+1;
for(int k=1;k<h;k++)//攻击子节点的次数
{
if(dp[n][h]<dp[n][h-k]+dp[q[n][i]][k])//k下打非这个子树里的
dp[n][h]=dp[n][h-k]+dp[q[n][i]][k];
}
}
}
}
int main()
{
int n,m;
while(scanf("%d%d",&n,&m)!=EOF,n||m)
{
int i,p;
memset(dp,0,sizeof(dp));
memset(v,0,sizeof(v));
for(int i=0;i<=n;i++)
q[i].clear();
for(int i=1;i<=n;i++)
{
scanf("%d%d",&p,&v[i]);
q[p].push_back(i);
}
dfs(0,m+1);
cout<<dp[0][m+1]<<endl;
}
}
3.背包问题
Rebuilding Roads
题目大意:有n个点组成一个树,问至少要删除多少条边才能获得一棵有p个结点的子树
题目分析:
dp[i][j]表示以i号节点为根的子树,当有j个结点时最少需要去掉几条边。
初始化:当只有1个节点时,一定是连接它到孩子结点的所有边都去掉。
设某一孩子结点标号为v 则dp[i][j]=min(dp[i][j],dp[i][j-t]+dp[v][t]-1);
记录最小值是时,如果节点不是根节点,那么总值 需要加1,因为还有连接父亲结点的一条边没算。
代码:
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<cmath>
using namespace std;
struct node
{
int to;
int next;
};
int dp[155][155];
int brother[155];
int son[155];
int n,p;
void dfs(int r)
{
for(int i=0;i<=p;i++)
{
dp[r][i]=1000000000;
}
dp[r][1]=0;
for(int i=son[r];i!=-1;i=brother[i])
{
int v=i;
dfs(v);
for(int j=p;j>=0;j--)
{
int tmp=dp[r][j]+1;
for(int k=0;k<=j;k++)
{
tmp=min(tmp,dp[v][j-k]+dp[r][k]);
}
dp[r][j]=tmp;
}
}
}
int main()
{
int u,v;
while(~scanf("%d%d",&n,&p))
{
int cnt=0;
memset(son,-1,sizeof(son));
for(int i=1;i<n;i++)
{
scanf("%d%d",&u,&v);
brother[v]=son[u];//u为父节点,v的兄弟就是u的孩子
son[u]=v;
}
int ans;
dfs(1);
ans=dp[1][p];
for(int j=1;j<=n;j++)
{
if(ans>dp[j][p])
ans=dp[j][p]+1;//不要忘记减去父亲的那条(除了根结点)
}
cout<<ans<<endl;
}
return 0;
}
4.背包 +树的重心变形
题目:Tree Cutting poj2378
给一个树状图,有n个点。求出,去掉哪个点,使得剩下的每个连通子图中点的数量不超过n/2。如果有很多这样的点,就按升序输出。n<=10000
思路分析:
此题是求解树的重心的一个变形。
根据求解树的重心的方法,设f[i] 为 以i为根的子树的结点个数,那么 f[i] += {f[j] + 1 | j 为i的子结点} 。
设dp[i] 为删除结点i, 最大的连通图有多少个结点。最后答案将d[i] <= n / 2 的结点 i 输出即可。
代码:
#include<iostream>
#include<string.h>
#include<stdio.h>
using namespace std;
const int MAX=50010;
struct lalala //建结构体
{
int next;
int to;
}edge[MAX*2];
int head[MAX];//前驱节点
int vis[MAX];//是否访问过,做标记
int dp[MAX];//记录拥有最大的节点数
int num[MAX];//统计以每个结点为根的树的结点数,记为num[].
int n, tot;
void add(int u, int v)//建树
{
edge[++tot].next=head[u];
edge[tot].to=v;
head[u]=tot;
}
void init()
{
tot=0;
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
memset(num,0,sizeof(num));
memset(edge,0,sizeof(edge));
}
void dfs(int u)
{
vis[u]=1;
num[u]=1;
for(int i=head[u];i!=0;i=edge[i].next)
{
int v=edge[i].to;
if(vis[v])
continue;
dfs(v);
dp[u]=max(dp[u],num[v]);
num[u]+=num[v];
}
dp[u]=max(dp[u],n-num[u]);
}
int main()
{
int a, b;
while(~scanf("%d", &n))
{
init();
for(int i=1; i<=n-1; i++)
{
scanf("%d%d", &a, &b);
add(a, b);
add(b, a);
}
dfs(1);
int ans[MAX];
int k=0;
for(int i=1; i<=n; i++)
{
if(dp[i]<=n/2)
ans[++k]=i;
}
for(int i=1; i<=k; i++)
{
printf("%d\n", ans[i]);
}
}
return 0;
}
a未完待续。。。。。。