樹形$dp$學習筆記


今天學習了樹形\(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\)方程

選擇節點類

\[\begin{cases} dp[i][0]=dp[j][1] \\ dp[i][1]=\max/\min(dp[j][0],dp[j][1])\\ \end{cases} \]

樹形背包類

\[\begin{cases} dp[v][k]=dp[u][k]+val\\ dp[u][k]=max(dp[u][k],dp[v][k-1])\\ \end{cases} \]

例題類

以上就是對樹形\(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\)方程是

\[dp[u]+=max(dp[v],0) \]

#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]));
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM