树形DP

    科技2022-07-14  121

    一般来说树形dp在设状态转移方程时都可以用 f [ i ] [ j ] f[i][j] f[i][j]表示i这颗子树怎么怎么样的最优解,实现时一般都是用子树更新父亲(即从下向上更新),那么首先应该考虑的是一个一个子树的更新父亲还是把所有子树都算完了在更新父亲?这就要因题而异了,一般来说有两种情况:

    1.需要把所有子树的信息都掌握之后再更新子树的就需要把所有子树都算完了在更新父亲。

    2.而像树上背包这样的问题就需要一个一个的更新,每次都用一个子树更新已经更新完的子树+父亲,最后就可以将这一部分的子树更新完了,再继续往上更新,最后根节点就是答案。

    例题一:没有上司的晚会

    题目描述

    Ural大学有N个职员,编号为1~N。他们有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。每个职员有一个快乐指数。现在有个周年庆宴会,要求与会职员的快乐指数最大。但是,没有职员愿和直接上司一起参加宴会。

    输入格式

    第一行一个整数N。(1≤N≤6000)

    接下来N行,第i+1行表示i号职员的快乐指数Ri。(-128≤Ri≤127)

    接下来N-1行,每行输入一对整数L,K。表示K是L的直接上司。

    最后一行输入0,0。

    输出格式

    第1行:输出最大的快乐指数。

    样例
    样例输入

    7 1 1 1 1 1 1 1 1 3 2 3 6 4 7 4 4 5 3 5

    样例输出

    5

    #include<bits/stdc++.h> using namespace std; struct st{ int fa,ch[10005],len=0; }a[10005]; int h[10005],n,s,dp[100005][2]; void dfs(int x){ dp[x][0]=0; dp[x][1]=h[x]; for(int i=1;i<=a[x].len;i++){ int y=a[x].ch[i]; dfs(y); dp[x][0]+=max(dp[y][0],dp[y][1]); dp[x][1]+=dp[y][0]; } } int main(){ scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%d",&h[i]); } for(int i=1,l,u;i<n;i++){ scanf("%d%d",&l,&u); a[l].fa=u; a[u].ch[++a[u].len]=l; } for(int i=1;i<=n;i++){ if(a[i].fa==0){ s=i; } } dfs(s); printf("%d",max(dp[s][0],dp[s][1])); return 0; }

    例题二:求树的重心

    题目描述

    树的重心定义为树的某个节点,当去掉该节点后,树的各个连通分量中,节点数最多的连通分量其节点数达到最小值。树可能存在多个重心。如下图,当去掉点1后,树将分成两个连通块:(2,4,5),(3,6,7),则最大的连通块包含节点个数为3。若去掉点2,则树将分成3个部分,(4),(5),(1,3,6,7)最大的连通块包含4个节点;第一种方法可以得到更小的最大联通分量。可以发现,其他方案不可能得到比3更小的值了。所以,点1是树的重心。

    输入格式

    输入:第一行一个整数n,表示树的结点个数。(n<100)

    接下来n-1行,每行两个数i,j。表示i和j有边相连。

    输出格式

    输出:第一行一个整数k,表示重心的个数。

    接下来K行,每行一个整数,表示重心。按从小到大的顺序给出。

    样例
    样例输入

    7 1 2 1 3 2 4 2 5 3 6 3 7

    样例输出

    1 1

    #include<bits/stdc++.h> using namespace std; vector<int>v[1000005]; int d[1000005],cnt,ans1[100005],n,ans=0x3f3f3f3f; void dfs(int x,int y){ d[x]=1; int max1=0; for(int i=0;i<v[x].size();i++){ int z=v[x][i]; if(z!=y){ dfs(z,x); d[x]+=d[z]; max1=max(max1,d[z]); } } max1=max(max1,n-d[x]); if(max1<ans){ ans=max1; cnt=0; ans1[++cnt]=x; }else if(max1==ans){ ans1[++cnt]=x; } } int main(){ scanf("%d",&n); for(int i=1;i<n;i++){ int x,y; scanf("%d%d",&x,&y); v[x].push_back(y); v[y].push_back(x); } dfs(1,0); printf("%d",cnt); sort(ans1+1,ans1+cnt+1); for(int i=1;i<=cnt;i++){ printf("\n%d",ans1[i]); } return 0; }

    例题三:树的直径

    题目描述

    给定一个有个节点的树,树以个 N N N N − 1 N-1 N1条边的无向图形式给出, 求树的直径。

    输入格式

    输入的第1行为包含了一个正整数 n n n,为这棵二叉树的结点数。

    接下来N行,每行有个 2 2 2正整数,表示有一条从 x x x y y y的无向边。

    输出格式

    输出包括1个正整数,为这棵二叉树的直径。

    样例
    样例输入

    10 1 2 1 3 2 4 4 5 4 6 1 7 5 8 7 9 7 10

    样例输出

    6

    #include<bits/stdc++.h> using namespace std; vector<int>v[1000005]; int d[1000005],cnt,ans1[100005],n,d1[100005],max1,max2,k; void dfs(int x,int y,int g){ d[x]=g; int max1=0; for(int i=0;i<v[x].size();i++){ int z=v[x][i]; if(z!=y){ dfs(z,x,g+1); } } } void dfs2(int x,int y,int g){ d1[x]=g; int max1=0; for(int i=0;i<v[x].size();i++){ int z=v[x][i]; if(z!=y){ dfs2(z,x,g+1); } } } int main(){ scanf("%d",&n); for(int i=1;i<n;i++){ int x,y; scanf("%d%d",&x,&y); v[x].push_back(y); v[y].push_back(x); } dfs(1,0,0); for(int i=1;i<=n;i++){ if(d[i]>max1){ max1=d[i]; k=i; } } dfs2(k,0,0); for(int i=1;i<=n;i++){ if(d1[i]>max2){ max2=d1[i]; } } printf("%d",max2); return 0; }

    例题四:选课

    题目描述

    大学实行学分制。每门课程都有一定的学分,学生只要选修了这门课并通过考核就能获得相应学分。学生最后的学分是他选修各门课的学分总和。

    每个学生都要选择规定数量的课程。有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其他的一些课程基础上才能选修。例如《数据结构》必须在选修了《高级语言程序设计》后才能选修。我们称《高级语言程序设计》是《数据结构》的先修课。每门课的直接先修课最多只有一门。两门课也可能存在相同的先修课。为便于表述,每门课都有一个课号,课号依次为 1 , 2 , . . . 1,2,... 1,2,...

    上例中课号 1 1 1是课号 2 2 2的先修课,即如果要先修课号 2 2 2,则课号 1 1 1必定已被选过。同样,如果要选修课号 3 3 3,那么课号 1 1 1和 课号 2 2 2 都一定被选修过。

    学生不可能学完大学开设的所有课程,因此必须在入学时选定自己要学的课程。每个学生可选课程的总数是给定的。请找出一种选课方案使得你能得到的学分最多,并满足先修课优先的原则。假定课程间不存在时间上的冲突。

    输入格式

    输入的第一行包括两个正整数 M , N M,N M,N,分别表示待选课程数和可选课程数。

    接下来 M M M行每行描述一门课,课号依次为 1 , 2 , . . . , m 1,2,...,m 1,2,...,m。每行两个数,依次表示这门课先修课课号(若不存在,则该项值为 0 0 0)和该门课的学分。

    各相邻数值间以空格隔开。

    输出格式

    输出一行,表示实际所选课程学分之和。

    样例
    样例输入

    7 4 2 2 0 1 0 4 2 1 7 1 7 6 2 2

    样例输出

    13

    #include<bits/stdc++.h> using namespace std; int n,m,b[105]; vector<int>v[105]; int dp[105][105]; void dfs(int x){ for(int i=0;i<v[x].size();i++){ dfs(v[x][i]); } for(int i=1;i<=m+1;i++){ dp[x][i]=b[x]; } for(int i=0;i<v[x].size();i++){ for(int j=m+1;j>=1;j--){ for(int l=0;l<j;l++){ dp[x][j]=max(dp[x][j],dp[v[x][i]][l]+dp[x][j-l]); } } } } int main(){ while(scanf("%d%d",&n,&m)!=EOF&&(n||m)){ for(int i=1;i<=n;i++){ v[i].clear(); } memset(dp,0,sizeof dp); for(int i=1;i<=n;i++){ int a; scanf("%d%d",&a,&b[i]); v[a].push_back(i); } dfs(0); printf("%d\n",dp[0][m+1]); } return 0; }

    例题五:宝藏

    题目描述

    天顶星人与魔法世界的战争可以追溯到远古时代,这就是传说中的“神话时代”又称“众神陨落时代”。这场星际战争毁灭了人类创造的大部分科技文明,当时幸存的人类为了保存文明的火种,在地下建造了一个神秘的宝藏,该宝藏是没有出口,只有入口,现在修罗王在与天顶星人的接触中查到了宝藏的线索,知道宝藏总共有N个分岔口,在分岔口处是有科技源的,每个科技源都有一个价值。他偷偷带领了M个人来挖宝藏,为了安全起见,在每个分岔口必须至少留一个人下来,这个留下来的人可以挖科技源(每个人只能挖一个地方的科技源),这样才能保证不会迷路,而且这个迷宫有个特点,任意两点间有且只有一条路可通。请问如何才能多挖些科技源回去。

    输入格式

    第1行:两个正整数N,M 。N表示宝藏点的个数,M表示挖宝藏人数。(n≤1000,m≤100)

    第2行:N个整数,第i个整数表示第i个宝藏的价值。(保证|wi|<maxint)

    第N+2行:两个非负整数A和B,表示A通向B,当A=0,表示A是入口。(保证A,B≤n)

    输出格式

    输出最多挖回的价值。

    样例
    样例输入

    4 3 5 6 2 4 1 2 0 1 2 3 3 4

    样例输出

    13

    #include<bits/stdc++.h> using namespace std; int n,m,b[105]; vector<int>v[105]; int dp[105][105]; void dfs(int x){ for(int i=0;i<v[x].size();i++){ dfs(v[x][i]); } for(int i=1;i<=m+1;i++){ dp[x][i]=b[x]; } for(int i=0;i<v[x].size();i++){ for(int j=m+1;j>=1;j--){ for(int l=0;l<j;l++){ dp[x][j]=max(dp[x][j],dp[v[x][i]][l]+dp[x][j-l]); } } } } int main(){ while(scanf("%d%d",&n,&m)!=EOF&&(n||m)){ for(int i=1;i<=n;i++){ v[i].clear(); } memset(dp,0,sizeof dp); for(int i=1;i<=n;i++){ scanf("%d",&b[i]); } for(int i=1;i<=n;i++){ int a,u; scanf("%d%d",&a,&u); v[a].push_back(u); } dfs(0); printf("%d\n",dp[0][m+1]); } return 0; }

    例题六:“访问”美术

    题目描述

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

    输入格式

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

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

    输出格式

    输出偷到的画的数量

    输入

    60 7 0 8 0 3 1 14 2 10 0 12 4 6 2

    输出

    2

    #include<bits/stdc++.h> using namespace std; int l[100005],r[100005],dp[100005][605],cnt,n; void dfs(int x){ int a,b; scanf("%d%d",&a,&b); if(b==0){ cnt++; l[x]=cnt; dfs(l[x]); cnt++; r[x]=cnt; dfs(r[x]); for(int i=a*2+1;i<=n;i++){ for(int j=0;j<=i-a*2;j++){ dp[x][i]=max(dp[x][i],dp[l[x]][j]+dp[r[x]][i-2*a-j]); } } } else{ for(int i=a*2+5;i<=min(a*2+b*5,n);i++){ dp[x][i]=(i-a*2)/5; } for(int i=a*2+b*5+1;i<=n;i++){ dp[x][i]=dp[x][i-1]; } } } int main(){ scanf("%d",&n); dfs(0); printf("%d",dp[0][n-1]); return 0; }
    Processed: 0.013, SQL: 8