樹形動態規劃總結

本文部分題目出自《樹的動態規劃與構造》一文

/*問題可以分解成若干相互聯繫的階段,在每一個階段都要做出決策,全部過程的決策是一個決策序列。要使整個活動的總體效果達到最優的問題,稱爲多階段決策問題。動態規劃就是解決多階段決策最優化問題的一種思想方法。

因爲樹可以描述比較複雜的關係,這對選手分析問題的能力有較高的要求,在尋找最優子結構、組織狀態時往往需要創造性思維,而且樹型動態規劃對數學要求不高,不涉及單調性優化等方面,所以競賽中往往將它作爲側重考察選手分析思考能力的題型出現。

大多數動規都是在一維二維這種規則的背景下的,可以解決的問題比較侷限,而樹作爲一種特殊的圖,可以描述比較複雜的關係,再加上樹的遞歸定義,是一種非常合適動規的框架,樹型動態規劃就成爲動規中很特殊的一種類型。*/


本文通過對幾道題目分析進行一個總結。

【例1】 加分二叉樹

給定一箇中序遍歷爲1,2,3,…,n的二叉樹
每個結點有一個權值
定義二叉樹的加分規則爲:
左子樹的加分× 右子樹的加分+根的分數
若某個樹缺少左子樹或右子樹,規定缺少的子樹加分爲1。
構造符合條件的二叉樹
該樹加分最大
輸出其前序遍歷序列


分析:這道題劃分在區間dp中也一樣可以處理。只是代碼寫法有些許差別。 我們首先先解決加分最大的問題,設f[i][j]表示中序遍歷爲i到j的二叉樹的最大加分。 則顯然有f[i][j]=max{f[i][k-1]*f[k+1][j]+val[k]}. val[k]表示k節點的權值大小。 時間複雜度O(N³);  對於記錄前序遍歷,只要將每次f[i][j]中計算出來的最優的k值存入root[i][j]中,前序遍歷即可。 代碼比較簡單不再贅述。


【例2】二叉蘋果樹

有一棵蘋果樹,如果樹枝有分叉,一定是分 2 叉(就是說沒有隻有 1 個兒子的結點)。 這棵樹共有 N(1<=N<=100) 個結點(葉子點或者樹枝分叉點),編號爲 1-N, 樹根編號一定是 1。 
現在這顆樹枝條太多了,需要剪枝。但是一些樹枝上長有蘋果。 給定需要保留的樹枝數量P,求出最多能留住多少蘋果。 


分析:由於權值在邊上,邊與點又具有對應關係。爲了方便處理可以將權值轉移到點上,狀態便可以表示。

f[i][j]表示以節點i作爲子樹保留j個節點的最大值(注意因爲根節點沒有權值所以要保留p+1個點)

通過轉移得到方程:f[i][j]=max{f[i_left][k]+f[i_right][j-1-k]}

邊界f[i][0]=0;f[i][1]=val[i];

輸出f[1][p+1]


【例3】選課

給定N門課程,每門課程有一個學分
要從N門課程中選擇M門課程,使得學分總和最大
其中選擇課程必須滿足以下條件:
每門課程最多隻有一門直接先修課
要選擇某門課程,必須先選修它的先修課
M,N<=500


分析:本題考察的主要是樹中多叉樹轉二叉樹的解法技巧。 由於每門課程最多隻有一門先修課,所以給定的圖要麼是一棵樹或者森林。但是對於兩個不相關的點,我們可以構造一個虛擬的點,將每門先修課連在這個點上,這樣就構造出了模型:在一棵具有n個節點的樹上尋找m個點使得權值和最大。 

構造方程:設前i個選取j門課程的學分最大爲g[i][j];

則  


想想是否還有其他做法? 假如這是一棵二叉樹,那麼我們對問題的求解就會簡單得多。所以不妨將多叉樹轉化成二叉樹。左孩子選的時候一定要選取根節點,而右孩子(兄弟)則與之無關。

新的狀態轉移方程:



【例4】 沒有上司的舞會(加強版)

這道例題主要是要闡述通過兩種辦法來進行遍歷以免爆棧。題目可以自行百度,時間O(N)空間O(N),其中N<=100000.

方法一:通過廣搜拓展節點,然後再反向刷新(從兒子節點向父親節點刷新)

void solve(){
     queue<int>q;
     q.push(root);
     int now,p=0;
     while (!q.empty()){
         now=q.front();q.pop();s[++p]=now;
         for (int i=head[now];i;i=nex[i]) q.push(to[i]);}
   for (int i=p;i>=1;--i){
        now=s[i];
        for(int j=head[now];j;j=nxt[j]) f[now][0]+=max(f[to[j]][0],f[to[j]][1]);
        for(int j=head[now];j;j=nxt[j]) f[now][1]+=f[to[j]][0];
       f[now][1]+=val[now];		
  }
}


方法二:深搜的非遞歸實現方法如下:
擴展每個節點,將當前兒子節點加入棧並處理,直到其所有兒子節點都已經被處理,就更新其父親節點並將其彈出棧。
void DP(int a){ 	
                S.push(a);
	memcpy(head2,head,sizeof(head));
	while (!S.empty()) {
	    int a=S.top();
	    used[a]=true; 
	    if (head2[a]){		//還有兒子沒走 
	           int tal=f[head2[a]].go;
	           if (!used[tal])  S.push(tal);     //兒子未被訪問過
	           head2[a]=f[head2[a]].next;
	           continue;
	     }         
	    //兒子節點已處理完 
	    ans[a][1]+=val[a];
	    ans[fa[a]][0]=max(ans[fa[a]][0],max(ans[a][0],ans[a][1])); //更新父節點 
	    ans[fa[a]][1]=max(ans[fa[a]][1],ans[a][0]);
	    S.pop();//彈出該點
	}
}

【例5】tyvj P1520 樹的直徑  點擊打開鏈接

分析:對於求樹的直徑有兩種方法:

    方法一:兩次深搜:任找一點A爲源點,深搜遍歷得到最遠點B,這個最遠點B必定在直徑中(感性想想,以A點爲源點找到的最長路後面一段必定屬於樹的直徑的一部分);再以這個最遠點B爲源點深搜遍歷求一個最長路,這個最長路即爲樹的直徑。 (貪心思想不給出具體證明)

    方法二:DP:顯然最長路的兩個端點必然是葉子或者根節點。設f(i)表示到i最遠的葉子,g(i)表示到i次遠的葉子,則有f(i)=max{f(j)}+1;
  g(i)=second{f(j)}+1;

其中j必須是i的兒子,計算順序是自底向上。最終答案爲  max{f(i)+g(i)}+1 。

具體代碼詳見博文

樹直徑拓展:見codeforces 338B 或者數據生成器一題。

求兩次樹直徑也可參照bzoj1912


【例6】求樹的重心  鏈接戳我

分析:重心有幾個挺美妙的性質,在今年北大的夏令營出了一題求樹上各個點到某個點距離總和最短的點,並輸出。事實上直接找到樹的重心即可。由於重心可能有一個或兩個所以找兩次的重心。

<span style="font-size:12px;">void getroot(int u,int father)//求樹的重心  
{  
    int i,v;  
    f[u]=0;son[u]=1;  
    for(i=head[u];i!=-1;i=e[i].next)  
    {  
        v=e[i].ed;  
        if(v==father)continue;  
        getroot(v,u);  
        son[u]+=son[v];  
        f[u]=max(f[u],son[v]);  
    }  
    f[u]=max(f[u],size-son[u]);  
    if(f[u]<f[root])root=u;  //root爲重心
}  </span><span style="font-size: 18px;">
</span>




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章