• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 众包
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
S_Gloria
博客园    首页    新随笔    联系   管理    订阅  订阅
树形dp

还是很想引用lyd《算法竞赛进阶指南》总的几句话。

 在树上设计动态规划时,一般就以节点从深到浅(子树由大到小)的顺序作为dp的阶段。

在方程中一般第一维是节点编号,代表以该节点为根的子树。

大多数情况下都采用递归的形式实现树形dp。

对于每个节点x,先递归它的每个子节点进行dp,在回溯时,从子节点向节点x进行状态转移。

 

 

 后来翻了翻别人的博客才发现,原来树形dp==套路题,大概就是两个板子倒替用叭QwQ。

两类图:

 1 void dfs(int x)
 2 {
 3     for(int i=first[x];i;i=next[i])
 4     {
 5         int y=to[i];
 6         if(vis[y])continue;
 7         dfs(y);
 8         f[x][0]+=max(f[y][0],f[y][1]);
 9         f[x][1]+=f[y][0];
10     }
11 }
12 int main()
13 {
14     int x,y;
15     cin>>n;
16     for(int i=1;i<=n;i++)
17         cin>>f[i][1];
18     for(int i=1;i<=n-1;i++)
19     {
20         cin>>x>>y;
21         add(y,x);
22         add(x,y);//注意加两次边,转为无向图 
23     }
24     dfs(1);//任选一个点作为根进行dp,这里选择1作为根. 
25     cout<<max(f[1][1],f[1][0]);
26     return 0;
27 }
无向图code
 1 void slove(int x)
 2 {
 3     for(int i=first[x];i;i=next[i])
 4     {
 5         int y=to[i];
 6         slove(y);
 7         //dp
 8         f[x][0]+=max(f[y][0],f[y][1]);
 9         f[x][1]+=f[y][0];
10     }
11 }
12 
13 int main()
14 {
15     cin>>n;
16     for(int i=1;i<=n;i++)
17         cin>>f[i][1];//直接读入的时候初始化 
18     for(int i=1;i<=n-1;i++)
19     {
20         cin>>x>>y;
21         vis[x]=1;
22         add(y,x);
23     }
24     //找根 
25     for(int i=1;i<=n;i++)
26         if(!vis[i]){root=i;break;}
27 
28     slove(root);
29     cout<<max(f[root][1],f[root][0]);//输出答案 
30     return 0;
31 }
有向图code

tip:

1.无向图建边两次,需要vis判断是否走过

2.有向图需要找根节点

 

两类基本方程:

 

 

选择节点类
dp[i][0]=dp[j][1]
dp[i][1]=max/min(dp[j][0],dp[j][1])

 


树形背包类
dp[v][k]=dp[u][k]+val
dp[u][k]=max(dp[u][k],dp[v][k-1])

 

 例题: 

 

1.没有上司的舞会(选择节点类)

一个n个节点的树,每个点有权值Vi,根节点与子节点不能同时取,求总权值的最大值。

 

 1 #include<iostream>
 2 #include<vector>
 3 using namespace std;
 4 int n,f[7000][3];
 5 int vis[7000],h[7000];
 6 vector<int>son[7000];
 7 void dp(int x)
 8 {
 9     f[x][1]=h[x];
10     f[x][0]=0;
11     for(int i=0;i<son[x].size();i++)
12     {
13         int y=son[x][i];
14         dp(y);
15         f[x][0]+=max(f[y][1],f[y][0]);
16         f[x][1]+=f[y][0];
17     }
18 }
19 int main()
20 {
21     int x,y;
22     cin>>n;
23     for(int i=1;i<=n;i++)
24         cin>>h[i];
25     while(true)
26     {
27         cin>>x>>y;//y是x上司 
28         if(x==0&&y==0)break;
29         vis[x]=1;
30         son[y].push_back(x);
31     }
32     int root;
33     for(int i=1;i<=n;i++)
34     {
35         if(!vis[i])
36         {
37             root=i;
38             break;
39         }
40     }
41     dp(root);
42     cout<<max(f[root][1],f[root][0]);
43     return 0;
44 } 
没有上司的舞会

 

来分析这个题的话,dp的第一维表示节点状态,可以用01分别代表不选和选。

f[x,1]表示以x为根的子树,并且x选所能产生的最大值。

f[x,0]表示以x为根的子树,并且x不选的最大值。

很显然的动态转移方程:

f[x,0]=max(f[s,0],f[s,1])(s为x的子节点)

f[x,1]=f[s,0]+h[x]

在进行dfs时,要注意标记节点是否走过,以避免遍历过程中沿着反向边回到父节点。

 

 2.最大子树和最大子树和(选择节点类)

很显然的嘛,父节点选的话子节点可以选可以不选,父节点不选子节点肯定不选,那么动态转移方程就很显然了:

f[x][1]=max(f[son][1],f[son][0])

f[x][0]=0

给出优化(美化)之后的代码:

 

 1 #include<iostream>
 2 #include<stdio.h>
 3 using namespace std;
 4 int dp[20000],n;
 5 int tot=0,first[40000],to[40000],nex[40000],vis[40000];
 6 void add(int x,int y)
 7 {
 8     tot++;
 9     nex[tot]=first[x];
10     first[x]=tot;
11     to[tot]=y;
12 }
13 void dfs(int x)
14 {
15     vis[x]=1;
16     for(int i=first[x];i;i=nex[i])
17     {
18         int y=to[i];
19         if(vis[y])continue;
20         dfs(y);
21         dp[x]+=max(0,dp[y]);
22     }
23 }
24 int main()
25 {
26     int x,y;
27     cin>>n;
28     for(int i=1;i<=n;i++)
29         cin>>dp[i];
30     for(int i=1;i<n;i++)
31     {
32         cin>>x>>y;
33         add(x,y);
34         add(y,x);
35     }
36     dfs(1);
37     int ans=0;
38     for(int i=1;i<=n;i++)
39         ans=max(ans,dp[i]);
40     cout<<ans;
41     return 0;
42 }
最大子树和

3.选课(树形背包类)

大概就是在一棵树中,点有点权,选子节点时必须选父节点,但是选父节点可以不选子节点的情况下,产生的权值最大。

就是上面提到的背包类型的树形dp,大众方程:

dp[v][k]=dp[u][k]+val
dp[u][k]=max(dp[u][k],dp[v][k-1])

结合代码看一下叭QwQ:

 

 1 #include<iostream>
 2 using namespace std;
 3 int n,m,tot=0;
 4 int first[1100],to[1100],nex[1100],f[1100][1100];
 5 void add(int x,int y)
 6 {
 7     tot++;
 8     nex[tot]=first[x];
 9     first[x]=tot;
10     to[tot]=y;
11 }
12 void dp(int x)
13 {
14     for(int i=first[x];i;i=nex[i])
15     {
16         int y=to[i];
17         dp(y);
18         for(int j=m+1;j>=1;j--)
19             for(int k=0;k<j;k++)
20                 f[x][j]=max(f[x][j],f[y][k]+f[x][j-k]);
21     }
22 }
23 int main()
24 {
25     int x,y;
26     cin>>n>>m;
27     for(int i=1;i<=n;i++)
28     {
29         cin>>x>>y;
30         add(x,i);
31         f[i][1]=y;
32     }
33     dp(0);
34     cout<<f[0][m+1];
35     return 0;
36 }
选课

 

这类问题实际上是背包与树形dp的结合。

F[x,t]表示以x为根的子树中选t门课能获得的最高学分。(就是Gloria意识中的把前i个物品放入容量为j的包里获得的最大价值)

小技巧:一般以第一维态为节点编号,把背包容积作为第二维态,在状态转移时,要处理的实际是分组背包的问题。

 

 

 

 

posted on 2019-11-10 00:17  S_Gloria  阅读(133)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3