今天學習了樹形\(dp\),一開始瀏覽各大\(blog\),發現都\(TM\)是題,連個入門的\(blog\)都沒有,體驗極差。所以我立志要寫一篇可以讓初學樹形\(dp\)的童鞋快速入門。
樹形\(dp\)
概念類
樹形\(dp\)是一種很優美的動態規划,真的很優美真的,前提是在你學會它之后。
實現形式
樹形\(dp\)的主要實現形式是\(dfs\),在\(dfs\)中\(dp\),主要的實現形式是\(dp[i][j][0/1]\),\(i\)是以\(i\)為根的子樹,\(j\)是表示在以\(i\)為根的子樹中選擇\(j\)個子節點,\(0\)表示這個節點不選,\(1\)表示選擇這個節點。有的時候\(j\)或\(0/1\)這一維可以壓掉
基本的\(dp\)方程
選擇節點類
樹形背包類
例題類
以上就是對樹形\(dp\)的基本介紹,因為樹形\(dp\)沒有基本的形式,然后其也沒有固定的做法,一般一種題目有一種做法。
沒有上司的舞會
這道題是一樹形\(dp\)入門級別的題目,具體方程就用到了上述的選擇方程。
#include<cmath>
#include<cstdio>
#include<iostream>
#include<algorithm>
#define N 6001
using namespace std;
int ind[N],n,hap[N],dp[N][2],fa[N],root,vis[N],ne[N],po[N];
void work(int x)
{
for(int i = po[x]; i; i = ne[i])
{
work(i);
dp[x][1]=max(max(dp[x][1],dp[x][1]+dp[i][0]),dp[i][0]);
dp[x][0]=max(max(dp[x][0],dp[i][1]+dp[x][0]),max(dp[i][1],dp [i][0]));
}
}
int main()
{
cin >> n;
for(int i=1; i<=n; i++)
cin >> dp[i][1];
for(int i=1; i<=n; i++)
{
int a,b;
cin >> b >> a;
ind[b]++;
ne[b] = po[a];
po[a] = b;
}
for(int i=1; i<=n; i++)
if(!ind[i])
{
root=i;
break;
}
work(root);
cout << max(dp[root][0],dp[root][1]);
}
最大子樹和
這道題的\(dp\)方程有變,因為你的操作是切掉這個點,所以你的子樹要么加上價值,要么價值為\(0\),所以\(dp\)方程是
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
struct edge
{
int next,to;
} e[40000];
int head[40000],tot,rt,maxn;
void add(int x,int y)
{
e[++tot].next=head[x];
head[x]=tot;
e[tot].to=y;
}
int n,dp[20000],ind[20000];
int val[20000],f[20000];
void dfs_f__k(int x,int fa)
{
f[x]=fa;
for(int i=head[x]; i; i=e[i].next)
{
int v=e[i].to;
if(v!=fa)
dfs_f__k(v,x);
}
}
void dfs(int x)
{
dp[x]=val[x];
for(int i=head[x]; i; i=e[i].next)
{
int v=e[i].to;
if(v!=f[x])
{
dfs(v);
dp[x]+=max(0,dp[v]);
}
}
maxn=max(maxn,dp[x]);
}
int main()
{
scanf("%d",&n);
for(int i=1; i<=n; i++)scanf("%d",&val[i]);
for(int i=1; i<=n-1; i++)
{
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
add(b,a);
}
rt=1;
dfs_f__k(rt,0);
dfs(rt);
printf("%d",maxn);
}
選課
這道題的意思是每本書要想選擇一門課,必須要先學會它的必修課,所以這就形成了一種依賴行為,即選擇一門課必須要選擇必修課。那么他又說要選擇的價值最大,這就要用到樹形背包的知識了。
樹形背包的基本代碼形式(即上面的樹形背包類)
/*
設dp[i][j]表示選擇以i為根的子樹中j個節點。
u代表當前根節點,tot代表其選擇的節點的總額。
*/
void dfs(int u,int tot)
{
for(int i=head[x];i;i=e[i].next)
{
int v=e[i].to;
for(int k=0;k<tot;k++)//這里k從o開始到tot-1,因為v的子樹可以選擇的節點是u的子樹的節點數減一
dp[v][k]=dp[u][k]+val[u];
dfs(v,tot-1)
for(int k=1;k<=tot;k++)
dp[u][k]=max(dp[u][k],dp[v][k-1]);//這里是把子樹的值賦給了根節點,因為u選擇k個點v只能選擇k-1個點。
}
}
然后這就是樹形背包的基本形式,基本就是這樣做
代碼
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<cstring>
using namespace std;
int n,m;
struct edge
{
int next,to;
}e[1000];
int rt,head[1000],tot,val[1000],dp[1000][1000];
void add(int x,int y)
{
e[++tot].next=head[x];
head[x]=tot;
e[tot].to=y;
}
void dfs(int u,int t)
{
if (t<=0) return ;
for (int i=head[u]; i; i=e[i].next)
{
int v = e[i].to;
for (int k=0; k<t; ++k)
dp[v][k] = dp[u][k]+val[v];
dfs(v,t-1);
for (int k=1; k<=t; ++k)
dp[u][k] = max(dp[u][k],dp[v][k-1]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int a;
scanf("%d%d",&a,&val[i]);
if(a)
add(a,i);
if(!a)add(0,i);
}
dfs(0,m);
printf("%d",dp[0][m]);
}
Strategic game
這道題的意思是選擇最少的點來覆蓋一棵樹,可以用最小點覆蓋(也就是二分圖最大匹配)或者樹形\(dp\)來做,因為這里我們的專題是樹形\(dp\),所以我們現在就講樹形\(dp\)的做法。
我們做這道題的方法是用選擇方程來做,因為你要做最小點覆蓋,要么選這個點要么不選對吧。
於是\(dp\)的轉移方程就是上述一方程
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
using namespace std;
int n;
struct edge
{
int next,to;
} e[4000];
int head[4000],tot,dp[4000][2],ind[4000];
void add(int x,int y)
{
e[++tot].next=head[x];
head[x]=tot;
e[tot].to=y;
}
void dfs(int x)
{
dp[x][1]=1;
for(int i=head[x]; i; i=e[i].next)
{
int v=e[i].to;
dfs(v);
dp[x][0]+=dp[v][1];
dp[x][1]+=min(dp[v][0],dp[v][1]);
}
}
int main()
{
while(scanf("%d",&n)!=EOF)
{
memset(dp,0,sizeof(dp));
memset(head,0,sizeof(head));
memset(ind,0,sizeof(ind));
tot=0;
for(int j=1; j<=n; j++)
{
int a,b;
scanf("%d:(%d)",&a,&b);
for(int i=1; i<=b; i++)
{
int c;
scanf("%d",&c);
ind[c]++;
add(a,c);
}
}
int rt;
for(int i=0; i<=n; i++)
if(!ind[i])
{
rt=i;
break;
}
dfs(rt);
printf("%d\n",min(dp[rt][1],dp[rt][0]));
}
}