树形DP整理
(此文章部分概念引自《信息学奥赛一本通·提高篇》,特此声明!)
文章目录
基本概念
-
动规顺序:一般按照后序遍历,即叶 —> 根
-
实现方式:树形DP一般是通过记忆化搜索实现,因此采用递归方式
-
时间复杂度:基本上是O(n),若有附加维则是O(n*m)
经典问题
-
树的重心
-
树的最长路径(最远点对)
-
树的中心问题
-
普通树形DP
例题
-
由根分成左右子树两部分的情况(普通树可通过多叉转二叉转为二叉树)
【例1】二叉苹果树 luogu2015
【题目描述】
有一棵苹果树,如果树枝有分叉,一定是分2叉(就是说没有只有1个儿子的结点)这棵树共有N个结点(叶子点或者树枝分叉点),编号为1-N,树根编号一定是1。
我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有4个树枝的树
2 5
\ /
3 4
\ /
1现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。
给定需要保留的树枝数量,求出最多能留住多少苹果。
【输入格式】
第1行2个数,N和Q(1<=Q<= N,1<N<=100)。
N表示树的结点数,Q表示要保留的树枝数量。接下来N-1行描述树枝的信息。
每行3个整数,前两个是它连接的结点的编号。第3个数是这根树枝上苹果的数量。
每根树枝上的苹果不超过30000个。
【输出格式】
一个数,最多能留住的苹果的数量。【代码】
/*
User:Mandy.H.Y
Language:c++
Problem:appletree
*/
#include<bits/stdc++.h>
#define Max(x,y) (x)>(y)?(x):(y)
#define Min(x,y) (x)<(y)?(x):(y)
#define mem(A) memset((A),0,sizeof(A))
const int maxn=102;
int n,q,first[maxn],size=0,f[maxn][maxn];
struct Edge
{
int v,w,nt;
}edge[maxn<<1];
template<typename T>inline void read(T &x)
{
x=0;char c=getchar();bool f=0;
while(c<'0'||c>'9') {f|=(c=='-');c=getchar();}
while(c>='0'&&c<='9') {x=(x<<3)+(x<<1)+(c^48); c=getchar();}
if(f)x=-x;
}
template<typename T>void putch(const T x)
{
if(x>9) putch(x/10);
putchar((x%10)|48);
}
template<typename T>inline void put(const T x)
{
if(x<0) putchar('-'),putch(-x);
else putch(x);
}
void docu()
{
freopen("appletree.txt","r",stdin);
}
void eadd(int u,int v,int w)
{
edge[++size].v=v;
edge[size].w=w;
edge[size].nt=first[u];
first[u]=size;
}
void readdata()
{
read(n);read(q);
for(int i=1;i<n;++i)
{
int x,y,z;
read(x);read(y);read(z);
eadd(x,y,z);
eadd(y,x,z);
}
}
int dp(int u,int fa,int num,int ww)
{
if(num<=0) return 0;
if(f[u][num]) return f[u][num];
int v[3],j=0,w[3];
f[u][num]+=ww;
if(num==1) return f[u][num];
for(int i=first[u];i;i=edge[i].nt)
{
int v1=edge[i].v,w1=edge[i].w;
if(v1==fa) continue;
v[++j]=v1;
w[j]=w1;
}
if(j==0) return f[u][num];
for(int j=0;j<num;++j)
{
f[u][num]=Max(f[u][num],ww+dp(v[2],u,num-j-1,w[2])+dp(v[1],u,j,w[1]));
}
return f[u][num];
}
void work()
{
put(dp(1,0,q+1,0));
}
int main()
{
// docu();
readdata();
work();
return 0;
}
【例2】选课 luogu2014(多叉转二叉)
【题目描述】
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有N门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程a是课程b的先修课即只有学完了课程a,才能学习课程b)。一个学生要从这些课程里选择M门课程学习,问他能获得的最大学分是多少?
【输入格式】
第一行有两个整数N,M用空格隔开。(1<=N<=300,1<=M<=300)
接下来的N行,第I+1行包含两个整数ki和si, ki表示第I门课的直接先修课,si表示第I门课的学分。若ki=0表示没有直接先修课(1<=ki<=N, 1<=si<=20)。
【输出格式】
只有一行,选M门课程的最大得分。
【代码】
/*
User:Mandy.H.Y
Language:c++
Problem:course
*/
#include<bits/stdc++.h>
using namespace std;
const int maxn = 320;
int f[maxn][maxn] , bro[maxn] , son[maxn], v[maxn];
void add(int fa, int s)
//类似链表的存储
{
bro[s] = son[fa];
//son[i]中存入i的第一个儿子
son[fa] = s;
//bro[i]中存入i的上一个兄弟
}
int dp(int i, int j)
//对于每一个i节点,
//定义dp(i,j)为i的所有兄弟和 i 的所有儿子,和 i 自己,学 j 门课的最大学分总和。
{
if (i==-1) return 0;
if (j==0) return 0;
if (f[i][j] != -1) return f[i][j];
//记忆化 ,必备,在下面循环中son与bro有些会重算
int m = -1<<30;
//最小值
// 全分兄弟
m = max( m, dp(bro[i] , j));
for (int k = 0; k <= j-1; k++)
//从0开始,表示不选 儿子,选i自己与j-1个兄弟
{
m = max( m , dp(son[i] , k) + dp(bro[i] , j-1-k) + v[i]);
/*那么,可以分成两种情况:
1、不学 i 这门课,全部学兄弟的课程,dp( i , j ) = dp( bro[ i ] , j)
2、学 i 以及以 i 为先修课的课程, dp( i , j ) = dp( bro[ i ] , j - 1 - k ) + dp( son[ i ] , k ) + v[ i ]*/
}
f[i][j] = m;
return m;
}
int main()
{
memset(son , -1, sizeof(son));
memset(bro , -1, sizeof(bro));
memset(f , -1, sizeof(f ));
//初始化,必备,dfs中要用于判断 、,用0易混淆,易错
int n, m;
cin>>n>>m;
for(int i=1;i<=n;i++){
int fa,vx;
cin>>fa>>vx;
add(fa,i);
v[i] = vx;
}
cout<<dp(0, m+1);
return 0;
}
【例3】 选课 luogu2014(背包)
题目描述见例2
【代码】
/*
User:Mandy.H.Y
Language:c++
Problem:course
*/
#include<bits/stdc++.h>
using namespace std;
int m,n,head[305],next1[305],f[305][305];
void readdata()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int a;
scanf("%d%d",&a,&f[i][0]);
//a是第i门的直接先修课
//当a等于0时,无父亲的节点便接到0节点上,使树有且只有一个根
next1[i]=head[a];
head[a]=i;
}
}
void init()
{
freopen("cour.txt","r",stdin);
freopen("cour.txt","w",stdout);
}
int deep(int x)
{
if(head[x]==0) return 0;
int zi=0;
//指已算过的科目的总数
for(int i=head[x];i!=0;i=next1[i])
{
int izi=deep(i);
//t表示i的子结点的个数
zi=zi+izi+1;
for(int j=zi;j>=0;j--)
//01背包,一定要是降序,否则可能一个科目选两遍
for(int k=0;k<=izi;k++)
if(j-k-1>=0&&f[x][j-k-1]+f[i][k]>f[x][j]) f[x][j]=f[x][j-k-1]+f[i][k];
//如果(还有空间选i及i的k个子结点)
}
return zi;
}
void work()
{
deep(0);
printf("%d",f[0][m]);
}
int main()
{
//init();
readdata();
work();
return 0;
}
【例4】longest mzoj1264
【题目描述】
乌托邦有n个城市,某些城市之间有公路连接。任意两个城市都可以通过公路直接或者间接到达,并且任意两个城市之间有且仅有一条路径(What does this imply? A tree!)。
每条公路都有自己的长度,这些长度都是已经测量好的。
小修想从一个城市出发开车到另一个城市,并且她希望经过的公路总长度最长。请问她应该选择哪两个城市?这个最长的长度是多少?
【输入格式】
第一行n(n<=1000)。
以下n-1行每行三个整数a, b, c。表示城市a和城市b之间有公路直接连接,并且公路的长度是c(c<=10000)。
【输出格式】
仅一个数,即最长长度。
【代码】
/*
User:Mandy.H.Y
Language:c++
Problem:longest
*/
#include<bits/stdc++.h>
#define Max(x,y) (x)>(y)?(x):(y)
#define Min(x,y) (x)<(y)?(x):(y)
#define mem(A) memset((A),0,sizeof(A))
using namespace std;
const int maxn=1002;
struct Edge
{
int v,w,nt;
}edge[maxn<<1];
int n,first[maxn],size=0,dis[3][maxn],ans=0;
//用dis[0][i]表示以i为根的子树中,i到叶结点距离的最大值
//用dis[1][i]表示以i为根的子树中,i到叶结点距离的次大值
template<typename T>inline void read(T &x)
{
x=0;bool f=0;char c=getchar();
while(c<'0'||c>'9') {f|=(c=='-'); c=getchar();}
while(c>='0'&&c<='9') {x=(x<<3)+(x<<1)+(c^48); c=getchar();}
if(f) x=-x;
}
template<typename T>void putch(const T x)
{
if(x>9) putch(x/10);
putchar((x%10)|48);
}
template<typename T>inline void put(const T x)
{
if(x<0) putchar('-'),putch(-x);
else putch(x);
}
inline void eadd(int u,int v,int w)
{
edge[++size].v=v;
edge[size].w=w;
edge[size].nt=first[u];
first[u]=size;
}
void readdata()
{
read(n);
for(int i=1;i<n;++i)
{
int x,y,z;
read(x);read(y);read(z);
eadd(x,y,z);
eadd(y,x,z);
}
}
void dp(int u,int fa)
{
dis[0][u]=0; dis[1][u]=0;
for(int i=first[u];i;i=edge[i].nt)
{
int v=edge[i].v,w=edge[i].w;
if(v==fa) continue;
dp(v,u);
int x=dis[0][v]+w;
if(x>=dis[0][u])//加不加等于都行
{
dis[1][u]=dis[0][u];
//维护次大值
dis[0][u]=x;
//更新最大值
}
else if(x>dis[1][u])
{
dis[1][u]=x;
//更新次大值
}
ans=Max(dis[0][v]+dis[1][v],ans);
}
}
void work()
{
dp(1,0);
ans=Max(dis[0][1]+dis[1][1],ans);
//最后要判断叶结点
put(ans);
}
int main()
{
readdata();
work();
return 0;
}
【例5】 战略游戏 luogu2016
【题目描述】
Bob 喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的办法。现在他有个问题。他要建立一个古城堡,城堡中的路形成一棵树。他要在这棵树的结点上放置 最少数目的士兵,使得这些士兵能了望到所有的路。注意,某个士兵在一个结点上时,与该 结点相连的所有边将都可以被了望到。
请你编一程序,给定一树,帮 Bob 计算出他需要放置最少的士兵。
程序名:stragedi
【输入格式】
输入文件中数据表示一棵树,描述如下:
第一行 N,表示树中结点的数目。
第二行至第 N+1 行,每行描述每个结点信息,依次为:该结点标号 i,k(后面有 k 条边 与结点 I 相连),接下来 k 个数,分别是每条边的另一个结点标号 r1,r2,…,rk。
对于一个 n(0<n<=1500)个结点的树,结点标号在 0 到 n-1 之间,在输入文件中每条 边只出现一次。
【输出格式】
输出文件仅包含一个数,为所求的最少的士兵数目。
例如,对于如下图所示的树:
【代码】
/*
User:Mandy.H.Y
Language:c++
Problem:stragedi
*/
#include<bits/stdc++.h>
#define Max(x,y) (x)>(y)?(x):(y)
#define Min(x,y) (x)<(y)?(x):(y)
#define mem(A) memset((A),0,sizeof(A))
using namespace std;
const int maxn=1502;
int n,first[maxn],size=0,f[2][maxn],ans=0;
struct Edge
{
int v,nt;
}edge[maxn<<1];
template<typename T>inline void read(T &x)
{
x=0;char c=getchar(); bool f=0;
while(c<'0'||c>'9') {f|=(c=='-');c=getchar();}
while(c>='0'&&c<='9') {x=(x<<3)+(x<<1)+(c^48); c=getchar();}
if(f) x=-x;
}
template<typename T>void putch(const T x)
{
if(x>9) putch(x/10);
putchar((x%10)|48);
}
template<typename T>inline void put(const T x)
{
if(x<0) putchar('-'),putch(-x);
else putch(x);
}
void docu()
{
freopen("1.txt","r",stdin);
}
void eadd(int u,int v)
{
edge[++size].v=v;
edge[size].nt=first[u];
first[u]=size;
}
void readdata()
{
read(n);
}
void dp(int u,int fa)
{
f[0][u]=0;f[1][u]=1;
for(int i=first[u];i;i=edge[i].nt)
{
int v=edge[i].v;
if(v==fa) continue;
dp(v,u);
f[0][u]+=f[1][v];
f[1][u]+=Min(f[0][v],f[1][v]);
}
}
void work()
{
for(int i=1;i<=n;++i)
{
int x,y,z;
read(x);read(y);
for(int i=1;i<=y;++i)
{
read(z);
eadd(x,z);
eadd(z,x);
}
}
dp(0,0);
ans=Min(f[0][0],f[1][0]);
put(ans);
}
int main()
{
// docu();
readdata();
work();
return 0;
}