树形dp 入门

今天学了树形dp,发现树形dp就是入门难一些,于是好心的我便立志要发一篇树形dp入门的博客了。

树形dp的概念什么的,相信大家都已经明白,这里就不再多说。直接上例题。

一、常规树形DP

P1352 没有上司的舞会

题目描述

某大学有N个职员,编号为1~N。他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数Ri,但是呢,如果某个职员的上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

 

这是一道非常经典的树形dp,首先分析题目,如果上司去了,那么他的所有下司都不能去;如果上司不去,那么他的所有下司去不去无所谓,但要取最大值。接下来就是状态转移方程:

我们设f[i][0]为i不去的情况,f[i][1]为i去的情况,那么

f[i][0]+=max(f[i的下司][1],f[i的下司][0]);
f[i][1]+=f[i的下司]][0];

初始化,如果i去,那么显然f[i][1]=i的快乐指数,如果i不去那么f[i][1]=0;

最后贴上代码

 1 #include<iostream>
 2 #include<cstring>
 3 #include<cmath>
 4 #include<string>
 5 #include<cstdio>
 6 using namespace std;
 7 struct edge
 8 {
 9     int num,child[6001];
10 }g[6001];
11 int n;
12 int a[10000];
13 int tree[10000];
14 int f[6001][2];
15 int aa,bb,root;
16 void dp(int t)
17 {
18     f[t][1]=a[t];//初始化 
19     f[t][0]=0;//初始化 
20     for(int i=1;i<=g[t].num;i++)
21     {
22         dp(g[t].child[i]);
23         f[t][0]+=max(f[g[t].child[i]][1],f[g[t].child[i]][0]);//状态转移 
24         f[t][1]+=f[g[t].child[i]][0];//状态转移
25     }
26 }
27 int main()
28 {
29     cin>>n;
30     for(int i=1;i<=n;i++)
31     {
32         scanf("%d",&a[i]);
33     }
34     while(scanf("%d%d",&aa,&bb))
35     {
36         if(aa==0&&bb==0) break;
37         g[bb].num++;//记录孩子结点的个数 
38         g[bb].child[g[bb].num]=aa;//储存孩子结点 
39         tree[aa]=bb;//记录父亲结点 
40     }
41     root=1;
42     while(tree[root]) root++;//找出树根 
43     dp(root);//从树根开始dp 
44     int ans=max(f[root][1],f[root][0]);
45     cout<<ans;
46     return 0;
47 }

 

洛谷P2016 战略游戏

题目描述

Bob喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的办法。现在他有个问题。

他要建立一个古城堡,城堡中的路形成一棵树。他要在这棵树的结点上放置最少数目的士兵,使得这些士兵能了望到所有的路。

注意,某个士兵在一个结点上时,与该结点相连的所有边将都可以被了望到。

请你编一程序,给定一树,帮Bob计算出他需要放置最少的士兵.

 

和上个题一模一样,就是初始化不同,上个题每个人都有不同的权值,而这个题每个人的权值都为1;

状态转移一模一样 ;初始化,f[i][1]=1,f[i][1]=0;

最后上代码

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cmath>
 4 #include<string>
 5 #include<cstring>
 6 using namespace std;
 7 struct edge
 8 {
 9     int num,child[1501];
10 }g[1501];
11 int a[1501];
12 int f[1501][2];
13 int x,y;
14 int n;
15 void dp(int t)
16 {
17     f[t][1]=1;//初始化为1 
18     f[t][0]=0;
19     for(int i=1;i<=g[t].num;i++)
20     {
21         dp(g[t].child[i]);
22         f[t][0]+=f[g[t].child[i]][1];//状态转移 
23         f[t][1]+=min(f[g[t].child[i]][1],f[g[t].child[i]][0]);//状态转移 
24     }
25 }
26 int main()
27 {
28     cin>>n;
29     for(int i=1;i<=n;i++)
30     {
31         scanf("%d",&x);
32         scanf("%d",&g[x].num);//记录该结点的孩子个数 
33         for(int j=1;j<=g[x].num;j++)
34         {
35             scanf("%d",&g[x].child[j]);//存储该结点的孩子 
36             a[j]=i;//记录父亲 
37         }
38     }
39     int root=0;
40     while(a[root]) root++;//找出根节点 
41     dp(root);
42     int ans=min(f[root][1],f[root][0]);
43     cout<<ans;
44     return 0;
45 }

 

洛谷P2458 [SDOI2006]保安站岗

题目描述

五一来临,某地下超市为了便于疏通和指挥密集的人员和车辆,以免造成超市内的混乱和拥挤,准备临时从外单位调用部分保安来维持交通秩序。

已知整个地下超市的所有通道呈一棵树的形状;某些通道之间可以互相望见。总经理要求所有通道的每个端点(树的顶点)都要有人全天候看守,在不同的通道端点安排保安所需的费用不同。

一个保安一旦站在某个通道的其中一个端点,那么他除了能看守住他所站的那个端点,也能看到这个通道的另一个端点,所以一个保安可能同时能看守住多个端点(树的结点),因此没有必要在每个通道的端点都安排保安。

编程任务:

请你帮助超市经理策划安排,在能看守全部通道端点的前提下,使得花费的经费最少。

 

难度提升,如果看不懂可以先放一放

分析:

我们发现,要使所有点最终全部被覆盖,那么无非有3种状态:

(以下全部都是对于要覆盖任意一个以x为根的子树来说的)
(其中我们设y节点为y的儿子,fa为x的父亲)
1.x节点被自己覆盖,即选择x点来覆盖x点

2.x节点被儿子y覆盖,即选择y点来覆盖x点

3.x节点被父亲fa覆盖,即选择fa点来覆盖x点

借此三种状态,我们可以设f[x][0/1/2]为让以x为根的子树中的节点全部被覆盖,且x点的被覆盖情况为1/2/3时的最小代价

为了方便,我们不妨设这三种情况分别为:

1.f[x][0]---对应上面的1

2.f[x][1]---对应上面的2

3.f[x][2]---对应上面的3

既然是DP,总是有转移方程的,我们想一下dp方程要如何设计

设计状态转移方程:

(1):对应上面的1.

f[x][0]=∑ min(f[y][0],f[y][1],f[y][2]) + val[x]

其中val[x]是选择x点的代价

我们很容易想到,在节点x被选择之后,我们就可以无拘无束了(蛤?),也就是说对于x儿子节点y的状态可以不去考虑,因为x节点被选择之后y节点无论如何也会被覆盖到,所以我们在儿子y的所有状态里取min,累加起来就行了

(2):对应上面的3(先讲3,因为2比较难以理解,放到了后面)

f[x][2]=∑ min(f[y][0],f[y][1])

为什么3情况对应的转移方程要这样写呢?

我们不妨这样理解,对于x节点我们让它的父亲节点fa覆盖它,那么根据我们的状态设计,此时必须要满足以x的儿子y为根的子树之中所有点已经被覆盖

那么这时就转化为一个子问题,要让y子树满足条件,只有两种决策:要么y被y的儿子覆盖,要么被y自己覆盖(即选择y节点),只需要在y的这两种状态取min累加就可以了

(3):对应上面的2(DuangDuangDuang 敲黑板划重点啦)

f[x][1]=∑ min(f[y][0],f[y][1]),如果选择的全部都是f[y][1],要再加上min(f[y][0]-f[y][1])

到了这里,我们就要回顾一下我们设计的dp状态了:

设f[x][0/1/2]为让以x为根的子树中的节点全部被覆盖,且x点的被覆盖情况为1/2/3时的最小代价

先提示一下,如果你理解了下面,那么本题是很简单的。。如果你没理解,就返回到这里再看一遍吧,我就在这里等着你

对于此时的状态,f[x][1]代表对于节点x让x被自己的儿子覆盖,那么和分析(2)一样,都要先满足此时以y的子树已经满足了条件,才能进行转移,这就是前面那部分:∑ min(f[y][0],f[y][1])的来历,那么后面那一长串又是怎么回事呢?

我们可以这样理解,此时既然要保证x点是被自己的儿子覆盖的,那么如果此时y子树已经满足了全部被覆盖,但是y此时被覆盖的状态却是通过y节点自己的儿子达到的,那么x就没有被儿子y覆盖到,那么我们不妨推广一下,如果x所有的儿子y所做的决策都不是通过选择y点来满足条件,那么我们就必须要选择x的一个子节点y,其中y满足f[y][0]-f[y][1]最小,并把这个最小的差值累加到f[x][1]中去,这样才能使得x点被自己的儿子覆盖,状态f[x][1]也才能合理地得到转移

好了,如果你还是没有太懂这个(3)的设计过程,请你回到之前再仔细看几遍

如果你已经理解了上面,那么恭喜你这个题,你已经A掉了

因为转移方程既然有了,那么我们就只需要最后的答案了

由于题目中没有说这棵树的根节点是哪个,所以你可以默认1就是根,或者开一个数组在加边的时候记录一下每个点的入度,最后没有入度的点就是根(但好像没有区别,毕竟我A掉了)

最后答案即为min(f[root][0],f[root][1])

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cmath>
 4 #include<string>
 5 #include<cstring>
 6 using namespace std;
 7 struct edge
 8 {
 9     int num,child[6001];
10 }g[6001];
11 int a[6001];
12 int dp[6001][3];
13 int root[6001];
14 int n,aa;
15 void dfs(int x)
16 {
17     dp[x][0]=a[x];
18     int cc=1000000000;
19     int num=0;
20     for(int i=1;i<=g[x].num;i++)
21     {
22         dfs(g[x].child[i]);
23         int t=min(dp[g[x].child[i]][0],dp[g[x].child[i]][1]);
24         dp[x][0]+=min(t,dp[g[x].child[i]][2]);
25         dp[x][1]+=t;
26         dp[x][2]+=t;
27         if(dp[g[x].child[i]][0]<dp[g[x].child[i]][1]) num++;
28         else cc=min(cc,dp[g[x].child[i]][0]-dp[g[x].child[i]][1]);
29     }
30     if(num==0) dp[x][1]+=cc;
31 }
32 int main()
33 {
34     scanf("%d",&n);
35     for(int i=1;i<=n;i++)
36     {
37         scanf("%d",&aa);
38         scanf("%d",&a[aa]);
39         scanf("%d",&g[aa].num);
40         for(int j=1;j<=g[aa].num;j++)
41         {
42             scanf("%d",&g[aa].child[j]);
43             root[g[aa].child[j]]=aa;
44         }
45     }
46     aa=1;
47     while(root[aa]) aa++;
48     dfs(aa);
49     cout<<min(dp[aa][0],dp[aa][1]);
50     return 0;
51 }

二、树形背包问题(在树上进行分组背包处理)

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

洛谷P2015 二叉苹果树

题目描述

有一棵苹果树,如果树枝有分叉,一定是分2叉(就是说没有只有1个儿子的结点)

这棵树共有N个结点(叶子点或者树枝分叉点),编号为1-N,树根编号一定是1。

我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有4个树枝的树

2   5
 \ / 
  3   4
   \ /
    1

现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。

给定需要保留的树枝数量,求出最多能留住多少苹果。

 

很典型的一道树形dp题目。我们设dp[i][j]为在以i为根的子树上取j个点的最大值,因为苹果树是二叉树,所以如果在左子树上

取k个点,那么就在右子树上取j-k个点,状态转移方程为:

dp[i][j]=max(dp[i][j],dp[i][k]+dp[v][j-k]);  其中v表示i的子节点。

代码如下:

 1 #include<iostream>
 2 #include<cmath>
 3 #include<cstdio>
 4 #include<string>
 5 #include<cstring>
 6 using namespace std;
 7 struct edge
 8 {
 9     int next;
10     int to;
11     int dis;
12 }g[100000];
13 int n,m;
14 int last[10000];
15 int dp[1001][1001];
16 int num;
17 int aa,bb,cc;
18 void dfs(int x,int fa)
19 {
20     for(int i=last[x];i;i=g[i].next)
21     {
22         int v=g[i].to;
23         if(v!=fa)
24         {
25             dp[v][1]=g[i].dis;
26             dfs(v,x);
27             for(int j=m;j>=1;j--)
28             for(int k=1;k<=j;k++)
29             {
30                 dp[x][j]=max(dp[x][j],dp[x][k]+dp[v][j-k]);//状态转移
31             }
32         }
33     }
34 }
35 void add(int from,int to,int dis)
36 {
37     g[++num].next=last[from];
38     g[num].to=to;
39     g[num].dis=dis;
40     last[from]=num;
41 }
42 int main()
43 {
44     scanf("%d%d",&n,&m);
45     m++;//由于题目给的是树枝的数量,所以结点数等于树枝树加1。
46     for(int i=1;i<n;i++)
47     {
48         scanf("%d%d%d",&aa,&bb,&cc);
49         add(aa,bb,cc);
50         add(bb,aa,cc);
51     }
52     dfs(1,1);//从根节点开始搜索
53     cout<<dp[1][m];
54     return 0;
55 }

 

洛谷P2014 选课

题目描述

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

 

大体思路和上个题相同,只不过这道题的数据是一个森林结构,我们可以自己添加一个结点作为整个森林的

根节点,于是就把一个森林变成了一棵树,对于虚拟的根我们只需要将所选的结点数加上1即可。

其他的地方跟上个题完全一样,这里不再多说,直接上代码:

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cmath>
 4 #include<string>
 5 #include<cstring>
 6 using namespace std;
 7 struct edge
 8 {
 9     int next;
10     int to;
11 }g[10000];
12 int last[1000];
13 int aa;
14 int n,m;
15 int num;
16 int a[1000];
17 int dp[1000][1000];
18 void dfs(int x,int fa)
19 {
20     dp[x][1]=a[x];//初始化为选x这门课程的学分 
21     for(int i=last[x];i;i=g[i].next)
22     {
23         int v=g[i].to;
24         if(v!=fa)
25         {
26             dfs(v,x);
27             for(int j=m;j>=1;j--)
28             for(int k=1;k<=j;k++)
29             {
30                 dp[x][j]=max(dp[x][j],dp[x][k]+dp[v][j-k]);//状态转移 
31             }
32         }
33     }
34 }
35 void add(int from,int to)
36 {
37     g[++num].next=last[from];
38     g[num].to=to;
39     last[from]=num;
40 }
41 int main()
42 {
43     scanf("%d%d",&n,&m);
44     m++;//因为加了一个虚拟的根,所以要将结点数加上1,这里一定要注意!!! 
45     for(int i=1;i<=n;i++)
46     {
47         scanf("%d%d",&aa,&a[i]);//用a数组来表示选i这门课程的学分 
48         add(aa,i);
49         add(i,aa);
50     }
51     dfs(0,0);//从虚拟的跟结点开始搜索 
52     cout<<dp[0][m];
53     return 0;
54 }

 

Hdu p1561 The moreThe Better

问题描述
ACboy很喜欢玩一种战略游戏,在一个地图上,有Ñ座城堡,每座城堡都有一定的宝物,在每次游戏中ACboy允许攻克中号个城堡并获得里面的宝物。但由于地理位置原因,有些城堡不能直接攻克,要攻克这些城堡必须先攻克其他某一个特定的城堡。你能帮ACboy算出要获得尽量多的宝物应该攻克哪中号个城堡吗?
 

和上一个题几乎一样,还是注意添加一个虚拟的根结点,添加后记得将总结点数加1.这道题是多组数据

所以记得清空数组,其他都一模一样。

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<string>
 5 #include<cmath>
 6 using namespace std;
 7 struct edge
 8 {
 9     int next;
10     int to;
11 }g[1000];
12 int last[205];
13 int a[205];
14 int dp[205][205];
15 int aa;
16 int num;
17 int n,m;
18 void add(int from,int to)
19 {
20     g[++num].next=last[from];
21     g[num].to=to;
22     last[from]=num;
23 }
24 void dfs(int x,int fa)
25 {
26     dp[x][1]=a[x];
27     for(int i=last[x];i;i=g[i].next)
28     {
29         int v=g[i].to;
30         if(v!=fa)
31         {
32             dfs(v,x);
33             for(int j=m;j>=1;j--)
34             for(int k=1;k<=j;k++)
35             {
36                 dp[x][j]=max(dp[x][j],dp[x][k]+dp[v][j-k]);
37             }
38         }
39     }
40 }
41 int main()
42 {
43     while(1)
44     {
45         scanf("%d%d",&n,&m);
46         if(n==0&&m==0) return 0;
47         m++;//这里要加1. 
48         num=0;
49         memset(a,0,sizeof(a));//千万记得清空数组!!! 
50         memset(dp,0,sizeof(dp));
51         memset(last,0,sizeof(last));
52         memset(g,0,sizeof(g));
53         for(int i=1;i<=n;i++)
54         {
55             scanf("%d%d",&aa,&a[i]);
56             add(aa,i);
57             add(i,aa);
58         }
59         dfs(0,0);
60         printf("%d\n",dp[0][m]);
61     }
62 }

 

Hdu 1011 Starship Troopers

问题描述
作为星河战队的领导者,你被派去摧毁这些虫子的基地。基地建在地下。它实际上是一个巨大的洞穴,包括许多与隧道相连的房间。每个房间都被一些虫子占据,他们的大脑隐藏在一些房间里。科学家刚开发出一种新武器,并想在某些大脑上进行实验。你的任务是摧毁整个基地,捕捉尽可能多的大脑。
杀死所有的bug总是比抓住他们的大脑更容易。为您绘制一张地图,所有房间都标有内部虫子的数量,以及包含大脑的可能性。洞穴的结构就像一棵树,从入口处有一条通往每个房间的独特路径。为了尽快完成战斗,你不想等到部队人员在前进到下一个房间之前清理房间,而是你必须在每个房间留下一些士兵来对抗里面的所有虫子。这些士兵从未重新进入他们之前访问过的房间。
星舰士兵可以对抗20个虫子。由于你没有足够的士兵,你只能占用一些房间,让神经气体完成其余的工作。同时,你应该最大限度地捕获大脑的可能性。为了简化问题,只需最大化所有可能的房间包含大脑的总和。制定这样的计划是一项艰巨的任务。你需要一台电脑的帮助。
 
输入
输入包含几个测试用例。每个测试用例的第一行包含两个整数N(0 <N <= 100)和M(0 <= M <= 100),它们分别是洞穴中的房间数和你拥有的星舰部队的数量。 。以下N行给出了房间的描述。每行包含两个非负整数 - 分别是内部错误的数量和包含大脑的可能性。接下来的N-1行给出了隧道的描述。每个隧道由两个整数描述,这两个整数是它连接的两个房间的索引。客房数量从1开始,1号房间是洞穴的入口。
最后一个测试用例后跟两个-1。
 
输出
对于每个测试用例,在一行上打印所有包含所有房间的大脑的可能性的总和。
 
样本输入
5 10 50 10 40 10 40 20 65 30 70 30 1 2 1 3 2 4 2 5 1 1 20 7 -1 -1
 
样本输出
50 7
题目是英文,就帮你们翻译了一下。
这道题与上面几道题目不同的是多了点权,这该怎么办呢,其实方法也很简单,就是初始化的时候把原来的1
改为现在的点权。这题有一个坑,如果m=0的时候,一定要特判一下,直接输出0,坑了我好久!!
最后上代码:
 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<string>
 5 #include<cmath>
 6 using namespace std;
 7 struct edge
 8 {
 9     int next;
10     int to;
11 }g[1000];
12 int last[205];
13 int a[205];
14 int b[205];
15 int dp[205][205];
16 int aa,bb;
17 int num;
18 int n,m;
19 void add(int from,int to)
20 {
21     g[++num].next=last[from];
22     g[num].to=to;
23     last[from]=num;
24 }
25 void dfs(int x,int fa)
26 {
27     for(int i=a[x];i<=m;i++)//这里的初始化和上面的题目不同,因为有点权。 
28     dp[x][i]=b[x];//当我们有a[x]名队员以至于更多时,我们最少可以获得b[x]个大脑
29     for(int i=last[x];i;i=g[i].next)
30     {
31         int v=g[i].to;
32         if(v!=fa)
33         {
34             dfs(v,x);
35             for(int j=m;j>=a[x];j--)//这里的边界条件应该注意一下 
36             for(int k=a[x];k<j;k++)
37             {
38                 dp[x][j]=max(dp[x][j],dp[x][k]+dp[v][j-k]);//一样的状态转移方程 
39             }
40         }
41     }
42 }
43 int main()
44 {
45     while(1)
46     {
47         scanf("%d%d",&n,&m);
48         if(n==-1&&m==-1) return 0;
49         num=0;
50         memset(a,0,sizeof(a));
51         memset(b,0,sizeof(b));
52         memset(dp,0,sizeof(dp));
53         memset(last,0,sizeof(last));
54         memset(g,0,sizeof(g));//清空数组,基本操作 
55         for(int i=1;i<=n;i++)
56         {
57             scanf("%d%d",&aa,&bb);
58             aa=(aa+19)/20;
59             a[i]=aa;//读入点权 
60             b[i]=bb;
61         }
62         for(int i=1;i<n;i++)
63         {
64             scanf("%d%d",&aa,&bb);
65             add(aa,bb);
66             add(bb,aa);
67         }
68         if(m==0)//这里很坑,如果m=0一定要输出0; 
69         printf("0\n");
70         else //否则就开始搜索 
71         {
72             dfs(1,1);
73             printf("%d\n",dp[1][m]);
74         }        
75     }
76 }

 

 洛谷P1273 有线电视网

题目描述

某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。

从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。

现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。

写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。

输入输出格式

输入格式:

输入文件的第一行包含两个用空格隔开的整数N和M,其中2≤N≤3000,1≤M≤N-1,N为整个有线电视网的结点总数,M为用户终端的数量。

第一个转播站即树的根结点编号为1,其他的转播站编号为2到N-M,用户终端编号为N-M+1到N。

接下来的N-M行每行表示—个转播站的数据,第i+1行表示第i个转播站的数据,其格式如下:

K A1 C1 A2 C2 … Ak Ck

K表示该转播站下接K个结点(转播站或用户),每个结点对应一对整数A与C,A表示结点编号,C表示从当前转播站传输信号到结点A的费用。最后一行依次表示所有用户为观看比赛而准备支付的钱数。

输出格式:

输出文件仅一行,包含一个整数,表示上述问题所要求的最大用户数。

输入输出样例

输入样例#1: 
5 3
2 2 2 5 3
2 3 2 4 3
3 4 2
输出样例#1: 
2

有些难度的树上背包问题。首先应该明确,题目要求的用户数是叶子结点的数目,而不包括非叶子结点,所以我们在搜索的时候
可以顺便记录一下,在做背包的时候,只需要循环这个子树上的叶子结点数即可。
我们设dp[i][j]为在以i为根的子树上选取j个叶子结点所得到的最大收益。 而收益值等于点权减去边权,所以可以推出状态
转移方程:dp[x][j]=max(dp[x][j],dp[x][k]+dp[v][j-k]-g[i].dis); 其中v代表x的子结点
然后我们在枚举以1为根的树上最多选取多少个叶结点可以是最大收益值非负。
由于收益可能是负的,所以要把数组清为负无穷。
最后上代码:
 1 #include<iostream>
 2 #include<cstdio>
 3 #include<string>
 4 #include<cmath>
 5 #include<cstring>
 6 using namespace std;
 7 struct edge
 8 {
 9     int next;
10     int to;
11     int dis;
12 }g[10000];
13 int last[10000];
14 int a[10000];
15 int dp[3001][3001];
16 int aa,bb,cc;
17 int n,m,num;
18 void add(int from,int to,int dis)
19 {
20     g[++num].next=last[from];
21     g[num].to=to;
22     g[num].dis=dis;
23     last[from]=num;
24 }
25 int dfs(int x,int fa)
26 {
27     if(x>=n-m+1)//如果x为叶子节点,那么就初始化为这个点的点权 
28     {
29         dp[x][1]=a[x];
30         return 1;//记录叶子节点个数 
31     }
32     int t,sum=0;
33     for(int i=last[x];i;i=g[i].next)
34     {
35         int v=g[i].to;
36         if(v!=fa)
37         {
38             t=dfs(v,x);//t表示以v为根的树的叶子节点个数 
39             sum+=t;//sum表示以x为根的树的叶子节点个数 
40             for(int j=sum;j>=1;j--)
41             for(int k=0;k<=j;k++)
42             {
43                 dp[x][j]=max(dp[x][j],dp[x][k]+dp[v][j-k]-g[i].dis);//状态转移 
44             }
45         }
46     }
47     return sum;
48 }
49 int main()
50 {
51     scanf("%d%d",&n,&m);
52     memset(dp,~0x3f,sizeof(dp));//清为负无穷 
53     for(int i=1;i<=n-m;i++)
54     {
55         scanf("%d",&aa);
56         for(int j=1;j<=aa;j++)
57         {
58             scanf("%d%d",&bb,&cc);//读入边权 
59             add(i,bb,cc);
60             add(bb,i,cc);
61         }
62     }
63     for(int i=n-m+1;i<=n;i++)
64     {
65         scanf("%d",&a[i]);//读入点权 
66     }
67     for(int i=1;i<=n;i++) dp[i][0]=0;//这里很重要,如果选0个子结点那么收益一定为0; 
68     dfs(1,1);
69     for(int i=m;i>=1;i--)
70     {
71         if(dp[1][i]>=0)//如果收益值非负,那么第一个i就是最大值。 
72         {
73             cout<<i;
74             return 0;
75         }
76     }
77 }

 

 洛谷P1270 “访问”美术馆

题目描述

经过数月的精心准备,Peer Brelstet,一个出了名的盗画者,准备开始他的下一个行动。艺术馆的结构,每条走廊要么分叉为两条走廊,要么通向一个展览室。Peer知道每个展室里藏画的数量,并且他精确测量了通过每条走廊的时间。由于经验老到,他拿下一幅画需要5秒的时间。你的任务是编一个程序,计算在警察赶来之前,他最多能偷到多少幅画。

输入输出格式

输入格式:

第1行是警察赶到的时间,以s为单位。第2行描述了艺术馆的结构,是一串非负整数,成对地出现:每一对的第一个数是走过一条走廊的时间,第2个数是它末端的藏画数量;如果第2个数是0,那么说明这条走廊分叉为两条另外的走廊。数据按照深度优先的次序给出,请看样例。

一个展室最多有20幅画。通过每个走廊的时间不超过20s。艺术馆最多有100个展室。警察赶到的时间在10min以内。

输出格式:

输出偷到的画的数量

输入输出样例

输入样例#1: 
60
7 0 8 0 3 1 14 2 10 0 12 4 6 2
输出样例#1: 
2

这题的读入比较毒瘤,需要递归来读,如果读到房间就把房间中的画存下来。其他都还好。
我们设dp[i][j]表示在以i为根的子树上用j的时间可以偷到的最多画的数目,
dp[i][j]=max(dp[i][j],dp[i][k]+dp[v][j-k-g[i].dis);其中v是i的子节点,
g[i].dis表示i到v的距离。由于偷完画后需要出来,所以就需要将边权乘以2计算,而且需要在警察来之前跑出来,因此总时间应减1。
代码如下:
 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cmath>
 4 #include<string>
 5 #include<cstring>
 6 using namespace std;
 7 struct edge
 8 {
 9     int next;
10     int to;
11     int dis;
12 }g[10000];
13 int last[10000];
14 int dp[2000][2000];
15 int num,aa,bb,cc;
16 int a=1;
17 int s;
18 void add(int,int,int);
19 void dfs(int x)
20 {
21     for(int i=last[x];i;i=g[i].next)
22     {
23         int v=g[i].to;
24         dfs(v);
25         for(int j=s;j>=1;j--)
26         for(int k=0;k<=j;k++)
27         {
28             if(j-k-g[i].dis>=0)
29             dp[x][j]=max(dp[x][j],dp[x][k]+dp[v][j-k-g[i].dis]);//状态转移 
30         }
31         
32     }
33 }
34 void read(int faa)
35 {
36     scanf("%d%d",&aa,&bb);
37     add(faa,a+1,aa*2);
38     a++;
39     if(bb==0)
40     {
41         faa=a;
42         read(faa);
43         read(faa);
44     }
45     else
46     {
47         for(int i=1;i<=bb;i++)
48         {
49             dp[a][i*5]=i;
50         }
51     }
52 }
53 void add(int from,int to,int dis)
54 {
55     g[++num].next=last[from];
56     g[num].to=to;
57     g[num].dis=dis;
58     last[from]=num;
59 }
60 int main()
61 {
62     scanf("%d",&s);
63     s--;//由于需要在警察来之前出来,所以总时间减1。 
64     read(1);//毒瘤的读入 
65     dfs(1);//从根节点开始搜索 
66     cout<<dp[1][s];
67     return 0;
68 }

 

其实我们机房的某位大神还有一种独特的思路,他用f[i][j]表示以i为根的子树上偷j幅画所用的时间。

不得不说这是真的强。

附上代码:

 1 #include<iostream>
 2 #include<cmath>
 3 #include<cstdio>
 4 #include<string>
 5 #include<cstring>
 6 using namespace std;
 7 struct edge
 8 {
 9     int next;
10     int to;
11     int dis;
12 }g[10000];
13 int s;
14 int aa,bb;
15 int x=1;
16 int last[10000];
17 int dp[2001][2001];
18 int tree[10000];
19 int num,faa;
20 void add(int from,int to,int dis)
21 {
22     g[++num].next=last[from];
23     g[num].to=to;
24     g[num].dis=dis;
25     last[from]=num;
26 }
27 int dfs(int y,int fa)
28 {
29     if(tree[y]==1)
30     {
31         dp[y][1]=0;
32         return 1;
33     }
34     int t,sum=0;
35     for(int i=last[y];i;i=g[i].next)
36     {
37         int v=g[i].to;
38         if(v!=fa)
39         {
40             t=dfs(v,y);
41             sum+=t;
42             for(int j=sum;j>=1;j--)
43             for(int k=0;k<j;k++)
44             {
45                 dp[y][j]=max(dp[y][j],dp[y][k]+dp[v][j-k]-g[i].dis);
46             }
47         }
48     }
49     return sum;
50 }
51 void read(int faa)
52 {
53     scanf("%d%d",&aa,&bb);
54     add(faa,x+1,2*aa);
55     add(x+1,faa,2*aa);
56     x++;
57     if(bb==0)
58     {
59         faa=x;
60         read(faa);
61         read(faa);
62     }
63     else 
64     {
65         int fa=x;
66         for(int i=1;i<=bb;i++)
67         {
68             tree[x+1]=1;
69             add(fa,x+1,5);
70             add(x+1,fa,5);
71             x++;
72         }
73     }
74 }
75 int main()
76 {
77     scanf("%d",&s);
78     memset(dp,~0x3f,sizeof(dp));
79     for(int i=1;i<=2000;i++)
80     dp[i][0]=0;
81     read(1);
82     int tot=dfs(1,1);
83     for(int i=tot;i>=0;i--)
84     {
85         if(dp[1][i]+s>0)
86         {
87             cout<<i;
88             return 0;
89         }
90     }
91     return 0;
92 }

 

洛谷P3360 偷天换日

题目背景

神偷对艺术馆内的名画垂涎欲滴准备大捞一把。

题目描述

艺术馆由若干个展览厅和若干条走廊组成。每一条走廊的尽头不是通向一个展览厅,就

是分为两个走廊。每个展览厅内都有若干幅画,每副画都有一个价值。经过走廊和偷画都是

要耗费时间的。

警察会在n 秒后到达进口,在不被逮捕的情况下你最多能得到的价值。

输入输出格式

输入格式:

第一行一个整数 n(n≤600)。

第二行若干组整数,对于每组整数(t,x),t 表示进入这个展览厅或经过走廊要耗费 t

秒的时间,若x>0 表示走廊通向的展览厅内有x 幅画,接下来

x对整数(w,c)表示偷一幅价值为 w 的画需要 c秒的时间。若

x=0 表示走廊一分为二。(t,c≤5; x≤30)

输入是按深度优先给出的。房间和走廊数不超过 300 个。

输出格式:

仅一个整数,表示能获得的最大价值。

输入输出样例

输入样例#1: 
50 
5 0 10 1 10 1 5 0 10 2 500 1 1000 2 18 1 1000000 4 
输出样例#1: 
1500

上一题的加强版,读入更加毒瘤,还要用到01背包。
读入的时候要在每一个房间做01背包,之后就和上一题一模一样了。
代码如下:
 1 #include<iostream>
 2 #include<cstring>
 3 #include<cmath>
 4 #include<string>
 5 #include<cstdio>
 6 using namespace std;
 7 struct edge
 8 {
 9     int next;
10     int to;
11     int dis;
12 }g[10000];
13 int n,a=1;
14 int num;
15 int aa,bb,cc;
16 int b[10000];
17 int dp[2001][2001];
18 int c[10000];
19 int last[10000];
20 void add(int,int,int);
21 void read(int faa)
22 {
23     scanf("%d%d",&aa,&bb);
24     add(faa,a+1,aa*2);
25     a++;
26     if(bb==0)
27     {
28         faa=a;
29         read(faa);
30         read(faa);
31     }
32     else 
33     {
34         for(int i=1;i<=bb;i++)
35         {
36             scanf("%d%d",&b[i],&c[i]);
37         }
38         for(int i=1;i<=bb;i++)
39         for(int j=n;j>=c[i];j--)
40         {
41             dp[a][j]=max(dp[a][j],dp[a][j-c[i]]+b[i]);//01背包 
42         }
43     }
44 }
45 void add(int from,int to,int dis)
46 {
47     g[++num].next=last[from];
48     g[num].to=to;
49     g[num].dis=dis;
50     last[from]=num;
51 }
52 void dfs(int x)
53 {
54     for(int i=last[x];i;i=g[i].next)
55     {
56         int v=g[i].to;
57         dfs(v);
58         for(int j=n;j>=1;j--)
59         for(int k=0;k<=j;k++)
60         {
61             dp[x][j]=max(dp[x][j],dp[x][k]+dp[v][j-k-g[i].dis]);
62         }
63     }
64 }
65 int main()
66 {
67     scanf("%d",&n);
68     n--;
69     read(1);
70     dfs(1);
71     cout<<dp[1][n];
72     return 0;
73 }

 

三、树的重心问题

定义

  • 树的重心:也叫树的质心。找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,删去重心后,生成的多棵树尽可能平衡。

性质

  • 性质 1 :树中所有点到重心的距离和是最小的,一棵树最多有两个重心(比如就两个点连个边)。

  • 性质 2 :把两棵树通过加一条边得到一颗新的树,新的树的重心必然在连接原来两棵树重心的路径上。

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

     (这个重心的位置其实和边权是无关的) 证明不会,但记住就好,毕竟不会考

      洛谷P1395 会议

题目描述

有一个村庄居住着n个村民,有n-1条路径使得这n个村民的家联通,每条路径的长度都为1。现在村长希望在某个村民家中召开一场会议,村长希望所有村民到会议地点的距离之和最小,那么村长应该要把会议地点设置在哪个村民的家中,并且这个距离总和最小是多少?若有多个节点都满足条件,则选择节点编号最小的那个点。

输入输出格式

输入格式:

第一行。一个数n,表示有n个村民。

接下来n-1行,每行两个数字a和b,表示村民a的家和村民b的家之间存在一条路径。

输出格式:

一行输出两个数字x和y

x表示村长将会在哪个村民家中举办会议

y表示距离之和的最小

输入输出样例

输入样例#1: 
4
1 2 
2 3 
3 4 
输出样例#1: 
2 4

这个题说白了就是求树的重心的单源最短路,最短路相信大家都会,这里不再赘述,关键是如何求出树的重心。

首先根据dfs序把无根树变成有根树

处理出以点u为根的子书的结点个数为s[u](包括自己)

那么点u的所有的子树中最大的子树节点数就是

max{max{s[v]}(v是u的子节点),n-s[u](这是因为是有根树前面没有算上“父节点”,这里考虑进来)}

 1 void dfs(int fa,int x)
 2 {
 3     s[x]=1;
 4     for(int i=last[x];i;i=g[i].next)
 5     {
 6         int v=g[i].to;
 7         if(v!=fa)
 8         {
 9             dfs(x,v);
10             s[x]+=s[v];
11             maxx[x]=max(maxx[x],s[v]);
12         }
13     }
14     maxx[x]=max(maxx[x],n-s[x]);
15 }

然后再枚举所以的结点找出重心。之后跑一边最短路就行了

代码如下:

 1 #include<iostream>
 2 #include<cmath>
 3 #include<cstdio>
 4 #include<string>
 5 #include<cstring>
 6 #include<queue>
 7 #define p pair<int,int>
 8 #define pp make_pair
 9 using namespace std;
10 priority_queue<p,vector< p >,greater< p > >q;
11 struct edge
12 {
13     int next;
14     int to;
15     int dis;
16 }g[100000];
17 int num,aa,bb,cc,an;
18 int ans;
19 int last[100000];
20 int s[100000];
21 int c[100000];
22 int d[100000];
23 int t,dd;
24 int n,m=100000000;
25 int maxx[100000];
26 void dfs(int fa,int x)
27 {
28     s[x]=1;
29     for(int i=last[x];i;i=g[i].next)
30     {
31         int v=g[i].to;
32         if(v!=fa)
33         {
34             dfs(x,v);
35             s[x]+=s[v];
36             maxx[x]=max(maxx[x],s[v]);
37         }
38     }
39     maxx[x]=max(maxx[x],n-s[x]);
40 }
41 void add(int from,int to,int dis)
42 {
43     g[++num].next=last[from];
44     g[num].to=to;
45     g[num].dis=dis;
46     last[from]=num;
47 }
48 int main()
49 {
50     cin>>n;
51     for(int i=1;i<n;i++)
52     {
53         scanf("%d%d",&aa,&bb);
54         add(aa,bb,1);
55         add(bb,aa,1);
56     }
57     dfs(1,1);
58     for(int i=1;i<=n;i++)
59     if(maxx[i]<m)
60     {
61         m=maxx[i];
62         an=i;
63     }
64     cout<<an;
65     memset(c,0x7f,sizeof(c));
66     c[an]=0;
67     q.push(pp(0,an));
68     while(q.size())
69     {
70         int t=q.top().second;
71         int dd=q.top().first;
72         q.pop();
73         if(d[t]) continue;
74         d[t]=1;
75         for(int i=last[t];i;i=g[i].next)
76         {
77             int v=g[i].to;
78             int m=g[i].dis+dd;
79             if(c[v]>m)
80             {
81                 c[v]=m;
82                 q.push(pp(c[v],v));
83             }
84         }
85     }
86     for(int i=1;i<=n;i++)
87     {
88         ans+=c[i];
89     }
90     cout<<" "<<ans;
91     return 0;
92 }

 

洛谷 P2986 [USACO10MAR]伟大的奶牛聚集Great Cow Gat…

这题和上道题差不多,就是多了点权和边权,需要做一些小小的改动。

首先我们先找出树的重心,但是这里有点权啊,我们怎么办呢?其实我们只要在初始化的时候把点权加进去就好了呀。

 1 void dfs(int x,int fa)
 2 {
 3     s[x]=a[x];//初始化为当前点的点权 
 4     for(int i=last[x];i;i=g[i].next)
 5     {
 6         int v=g[i].to;
 7         if(v!=fa)
 8         {
 9             dfs(v,x);
10             s[x]+=s[v];
11             maxx[x]=max(maxx[x],s[v]);
12         }
13     }
14     maxx[x]=max(maxx[x],sum-s[x]);//这里用所以点的点权和来作差 
15 }

其实也就改了两个地方。最后算出最短路再乘上相应的点权即可。

代码如下:

  1 #include<iostream>
  2 #include<cmath>
  3 #include<string>
  4 #include<cstring>
  5 #include<cstdio>
  6 #include<queue>
  7 #define p pair<long long,int>
  8 #define pp make_pair
  9 using namespace std;
 10 priority_queue<p,vector< p >,greater< p > >q;
 11 struct edge
 12 {
 13     int next;
 14     int to;
 15     int dis;
 16 }g[200005];
 17 int n;
 18 long long anss;
 19 int ans=1000000000;
 20 int an;
 21 int last[100005];
 22 int s[100005];
 23 int maxx[100005];
 24 long long c[100005];
 25 int d[100005];
 26 long long a[100005];
 27 int num,t;
 28 int sum;
 29 long long dd;
 30 int aa,bb,cc;
 31 void dfs(int x,int fa)
 32 {
 33     s[x]=a[x];//初始化为当前点的点权 
 34     for(int i=last[x];i;i=g[i].next)
 35     {
 36         int v=g[i].to;
 37         if(v!=fa)
 38         {
 39             dfs(v,x);
 40             s[x]+=s[v];
 41             maxx[x]=max(maxx[x],s[v]);
 42         }
 43     }
 44     maxx[x]=max(maxx[x],sum-s[x]);//这里用所以点的点权和来作差 
 45 }
 46 void add(int from,int to,int dis)
 47 {
 48     g[++num].next=last[from];
 49     g[num].to=to;
 50     g[num].dis=dis;
 51     last[from]=num;
 52 }
 53 int main()
 54 {
 55     scanf("%d",&n);
 56     for(int i=1;i<=n;i++)
 57     {
 58         scanf("%lld",&a[i]);
 59         sum+=a[i];
 60     }
 61     for(int i=1;i<n;i++)
 62     {
 63         scanf("%d%d%d",&aa,&bb,&cc);
 64         add(aa,bb,cc);
 65         add(bb,aa,cc);
 66     }
 67     dfs(1,1);
 68     for(int i=1;i<=n;i++)
 69     {
 70         if(maxx[i]<ans)
 71         {
 72             ans=maxx[i];
 73             an=i;
 74         }
 75     }
 76     memset(c,0x7f,sizeof(c));
 77     c[an]=0;
 78     q.push(pp(0,an));
 79     while(q.size())
 80     {
 81         t=q.top().second;
 82         dd=q.top().first;
 83         q.pop();
 84         if(d[t]) continue;
 85         d[t]=1;
 86         for(int i=last[t];i;i=g[i].next)
 87         {
 88             int v=g[i].to;
 89             long long m=g[i].dis+dd;
 90             if(c[v]>m)
 91             {
 92                 c[v]=m;
 93                 q.push(pp(c[v],v));
 94             }
 95         }
 96     }
 97     for(int i=1;i<=n;i++)
 98     {
 99         anss+=a[i]*c[i];
100     }
101     cout<<anss;
102     return 0;
103 }

 

树形dp入门我就写到这里,希望这篇文章对你有所帮助,有什么意见或者建议可以在下面评论 ,若是有什么不足的地方也欢迎指正。

posted @ 2018-10-24 21:59  snowy2002  阅读(902)  评论(0编辑  收藏  举报