樹形DP總結

一、樹形DP存樹 how to build a tree!

1.用vector,動態數組存圖,儲存兩個節點的父子關係(接下來的代碼使用這個)

2、鏈式前向星

3、鄰接表

二、怎樣動態規劃 how to DP!

給定一棵有n個節點的樹,我們可以任選一個點爲根節點,從而

定義出每個節點的深度和每棵子樹的根

在樹上進行DP,一般就以節點從深到淺的順序作爲DP的“階段”。

DP的狀態表示中,第一維通常是節點編號(代表以該節點爲根的子樹)。

大多數時候,採用遞歸的方式實現樹形DP。

對於每個節點x,先遞歸在它的每個子結點上進行DP,在回溯時,從子節點向節點x進行轉移

普通dp比較常用的考慮方法是dp[i][0/1]表示點i選/不選的情況下,其子樹的最優情況;

如果是樹形揹包,那麼dp[i][j]表示在以點i爲根節點的子樹中,選擇j個點的最優情況。

三、例題: do the title!

(一)、沒有上司的舞會

(題目略)

設dp[x][0]表示從x爲根的子樹中邀請一部分職員參會,而x自己不去,快樂指數的最大值。

此時,x的子結點去不去隨便,於是有:

dp[x,0]=\sum s\epsilon son(x) max(dp[s,0],dp[s,1])

設dp[x][1]表示從x爲根的子樹中邀請一部分職員參會,x自己去,快樂指數的最大值。

此時,x的子結點都甭去

dp[x,1]=H[x]+\sum s\epsilon son(x) dp[s,0]

son(x)表示x的子結點集合

本題輸入的是一根有根樹,所以我們要先找到root,然後DP的目標爲max(dp[root,0],dp[root,1])

時間複雜度爲O(n)

關鍵代碼如下:

vector<int> son[10005];
int dp[10005][2],v[10005],h[100005],n;
void dfs()
{
	dp[x][0] = 0;
	dp[x][1] = h[x];
	for(int i = 0;i < son[x].size(); ++i)
	{
		int y = son[x][i];
		dfs(y);
		dp[x][0] += max(dp[y][0],dp[y][1]);
		dp[x][1] += dp[y][0];
	}
}
int main()
{
	cin >> n;
	for(int i = 1;i <= n; ++i)cin>>h[i];
	for(int i = 1;i < n; ++i)
	{
		int x,y;
		cin >> x >> y;
		v[x] = 1;//x有父親
		son[y].push_back(x);//x是y的兒子 
	}
	int root;
	for(int i = 1;i <= n; ++i)
		if(!v[i])//i沒有父親
		{
			root = i;
			break;
		} 
	dfs(root);
	cout << max(dp[root][0],dp[root][1]) << endl;
}
 

(二)、選課

題目描述

原題來自:CTSC 1997

大學實行學分制。每門課程都有一定的學分,學生只要選修了這門課並通過考覈就能獲得相應學分。學生最後的學分是他選修各門課的學分總和。

每個學生都要選擇規定數量的課程。有些課程可以直接選修,有些課程需要一定的基礎知識,必須在選了其他的一些課程基礎上才能選修。例如《數據結構》必須在選修了《高級語言程序設計》後才能選修。我們稱《高級語言程序設計》是《數據結構》的先修課。每門課的直接先修課最多隻有一門。兩門課也可能存在相同的先修課。爲便於表述,每門課都有一個課號,課號依次爲 1,2,3,⋯。

下面舉例說明:

課號 先修課號 學分
1 1
2 11 1
3 22 3
4 3
5 22 4

上例中課號 1是課號 2的先修課,即如果要先修課號 2,則課號 1必定已被選過。同樣,如果要選修課號 3,那麼課號 1和 課號 2都一定被選修過。

學生不可能學完大學開設的所有課程,因此必須在入學時選定自己要學的課程。每個學生可選課程的總數是給定的。請找出一種選課方案使得你能得到的學分最多,並滿足先修課優先的原則。假定課程間不存在時間上的衝突。

輸入

輸入的第一行包括兩個正整數 M,N,分別表示待選課程數和可選課程數。

接下來 M行每行描述一門課,課號依次爲1,2,⋯,M。每行兩個數,依次表示這門課先修課課號(若不存在,則該項值爲 00)和該門課的學分。

各相鄰數值間以空格隔開。

輸出

輸出一行,表示實際所選課程學分之和。

樣例輸入

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

樣例輸出

13

數據範圍

1≤N≤M≤100,學分不超過 20。

題解:

設dp[x,t]表示在以x爲根的子樹中選t門課能夠獲得的最大學分

當t=0時,顯然dp[x,t]=0,t>0時,根據以上分析,可以看出,該題

其實是一個分組揹包,有p=|son(x)|組物品,揹包總容積爲t-1

我們從每組中選出不超過一個物品,使得總價值最大,於是

我們用分組揹包進行樹形DP轉移

代碼:

#include<bits/stdc++.h>
#define MAXN 1010
using namespace std;
int n,m;
int total,head[MAXN],to[MAXN],nxt[MAXN],f[MAXN][MAXN],s[MAXN];
void adl(int a,int b)
{
    total++;
    to[total]=b;
    nxt[total]=head[a];
    head[a]=total;
    return;
}
void dfs(int i)
{
    for(int e=head[i]; e; e=nxt[e])
    {
        dfs(to[e]);
        for(int j=m; j>=0; j--)
            for(int k=0;k<=j;k++)
               f[i][j]=max(f[i][j],f[i][j-k]+f[to[e]][k]);
    }
    if(i)
    {
        for(int j=m; j>0; j--)  
           f[i][j]=f[i][j-1]+s[i];
    }   
    return;
}
int main()
{
    cin>>n>>m;
    int a,b;
    for(int i=1; i<=n; i++)
    {
        cin>>a>>s[i];
        adl(a,i);
    }
    dfs(0);
    cout<<f[0][m]<<endl;
}

(三)、戰略遊戲

題目描述

Bob 喜歡玩電腦遊戲,特別是戰略遊戲。但是他經常無法找到快速玩過遊戲的方法。現在他有個問題。

現在他有座古城堡,古城堡的路形成一棵樹。他要在這棵樹的節點上放置最少數目的士兵,使得這些士兵能夠瞭望到所有的路。

注意:某個士兵在一個節點上時,與該節點相連的所有邊都將能被瞭望到。

請你編一個程序,給定一棵樹,幫 Bob 計算出他最少要放置的士兵數。

輸入

輸入數據表示一棵樹,描述如下。

第一行一個數N,表示樹中節點的數目。

第二到第N+1行,每行描述每個節點信息,依次爲該節點編號 i,數值 k,k 表示後面有 k條邊與節點 i相連,接下來 k個數,分別是每條邊的所連節點編號r1,r2,⋯,rk。

對於一個有 N個節點的樹,節點標號在0到 N-1之間,且在輸入文件中每條邊僅出現一次。

輸出

輸出僅包含一個數,爲所求的最少士兵數。

樣例輸入

4
0 1 1
1 2 2 3
2 0
3 0

樣例輸出

1

數據範圍

對於100%的數據,有0<N≤1500

題解:

定義狀態dp[u][0/1]表示u這個節點不放/放士兵

根據題意,如果當前節點不放置士兵,那麼它的子節點必須全部放置士兵,因爲要滿足士兵可以看到所有的邊,所以

dp[u][0]+=dp[to][1]其中to是u的子節點

如果當前節點放置士兵,它的子節點選不選已經不重要了(因爲樹形dp自下而上,上面的節點不需要考慮),所以

dp[u][1]+=min(dp[to][0],dp[to][1])

代碼:

​
#include<bits/stdc++.h>
#define MAXN 1505
using namespace std;
int n; 
int dp[MAXN][2],vis[MAXN];
vector<int> vc[MAXN];
int dfs(int u)
{
	dp[u][1]=1;
	for(int i=0;i<vc[u].size();++i)
	{
		int t=vc[u][i];
		dfs(t);
		dp[u][0]+=dp[t][1];
		dp[u][1]+=min(dp[t][0],dp[t][1]);//狀態轉移方程
	}
}
int main()
{
	scanf("%d",&n);
	int root;
	int a,b,c;
	for(int i=1;i<=n;++i)
	{
		scanf("%d%d",&a,&b);
		for(int j=0;j<b;++j)
		{
			scanf("%d",&c);
			vc[a].push_back(c);//存圖
			vis[i]=1;
		}
	}
	for(int i=0;i<n;++i)//判斷根
	    if(vis[i]==0)
	    {
	    	root=i;
	    	break;
	    }
	dfs(root);
	printf("%d\n",min(dp[root][0],dp[root][1]));
	return 0;
}
 
​

 

發佈了57 篇原創文章 · 獲贊 12 · 訪問量 4822
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章