一、樹形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][1]表示從x爲根的子樹中邀請一部分職員參會,x自己去,快樂指數的最大值。
此時,x的子結點都甭去
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;
}