1. 程式人生 > >樹形動態規劃(樹形DP)入門問題—初探 & 訓練

樹形動態規劃(樹形DP)入門問題—初探 & 訓練

#include<stdio.h>
#include<string.h>
#define N 6005

struct node
{
    int pa,son;
    int next;
}point[N];
//其實從這個結構體就可以看出很多東西,這就是一個靜態連結串列,在計算過程中遍歷一個節點的所有子節點的操作也是和連結串列完全相同的。
//不過我這都是後知後覺啊。

int dp[N][2],list[N],flag[N],value[N];
int pos;
int Max(int x,int y)
{
    return x>y?x:y;
}
void add(int pa,int son)
{
    point[pos].pa=pa;
    point[pos].son=son;
    point[pos].next=list[pa];  //以靜態連結串列的形式儲存一個父節點下面所有的子節點。
    list[pa]=pos++;
    return ;
}
void dfs(int root)   //這道題說起來是樹形DP,但是最沒有講的價值的就是這個DP。就是個入門級數塔的操作放在樹上了。
{
    if(list[root]==-1)
    {
        dp[root][1]=value[root];
        dp[root][0]=0;
        return ;
    }
    int now=list[root];
    dp[root][0]=0;
    dp[root][1]=value[root];
    while(now!=-1)
    {
        dfs(point[now].son);
        dp[root][1]+=dp[point[now].son][0];  //既然取了父節點的值,子節點的值就不能再取了。
        
        //父節點的值沒有取,子節點的值分取和不取兩種情況,取其中較大的那種情況。
        dp[root][0]+=Max(dp[point[now].son][1],dp[point[now].son][0]); 
        
        now=point[now].next;//這個子節點計算過了,就要開始計算下一個子節點了。
    }
    return ;
}

int main()
{
    int i,n;
    while(scanf("%d",&n)!=EOF)
    {
        for(i=1;i<=n;i++)
            scanf("%d",&value[i]);//記錄每一個點的值
        int a,b;
        pos=0;
        memset(list,-1,sizeof(list));
        memset(flag,0,sizeof(flag));
        while(scanf("%d%d",&a,&b),a+b)
        {
            add(b,a);    //將邊加入樹中
            flag[a]=1;   //記錄a有父節點,不可能是祖節點。
        }
        a=1;
        while(flag[a]==1)
            a++;     //從a往後查,遇到的第一個flag[a]的值是-1的點,這就是大名鼎鼎的祖節點了。
        dfs(a);
        printf("%d\n",Max(dp[a][0],dp[a][1]));
    }
    return 0;
}

POJ1192 樹形DP 最優連通子集

Description
       眾所周知,我們可以通過直角座標系把平面上的任何一個點P用一個有序數對(x, y)來唯一表示,如果x, y都是整數,我們就把點P稱為整點,否則點P稱為非整點。我們把平面上所有整點構成的集合記為W。 定義1 兩個整點P1(x1, y1), P2(x2, y2),若|x1-x2| + |y1-y2| = 1,則稱P1, P2相鄰,記作P1~P2,否則稱P1, P2不相鄰。 定義 2 設點集S是W的一個有限子集,即S = {P1, P2,..., Pn}(n >= 1),其中Pi(1 <= i <= n)屬於W,我們把S稱為整點集。 定義 3 設S是一個整點集,若點R, T屬於S,且存在一個有限的點序列Q1, Q2, ?, Qk滿足: 1. Qi屬於S(1 <= i <= k); 2. Q1 = R, Qk = T; 3. Qi~Qi + 1(1 <= i <= k-1),即Qi與Qi + 1相鄰; 4. 對於任何1 <= i < j <= k有Qi ≠ Qj; 
我們則稱點R與點T在整點集S上連通,把點序列Q1, Q2,..., Qk稱為整點集S中連線點R與點T的一條道路。 定義4 若整點集V滿足:對於V中的任何兩個整點,V中有且僅有一條連線這兩點的道路,則V稱為單整點集。 定義5 對於平面上的每一個整點,我們可以賦予它一個整數,作為該點的權,於是我們把一個整點集中所有點的權的總和稱為該整點集的權和。 
我們希望對於給定的一個單整點集V,求出一個V的最優連通子集B,滿足: 1. B是V的子集 2. 對於B中的任何兩個整點,在B中連通; 
3. B是滿足條件(1)和(2)的所有整點集中權和最大的。
Input
第1行是一個整數N(2 <= N <= 1000),表示單整點集V中點的個數; 
以下N行中,第i行(1 <= i <= N)有三個整數,Xi, Yi, Ci依次表示第i個點的橫座標,縱座標和權。同一行相鄰兩數之間用一個空格分隔。-10^6 <= Xi, Yi <= 10^6;-100 <= Ci <= 100。
Output
僅一個整數,表示所求最優連通集的權和。
Sample Input
5
0 0 -2
0 1 1
1 0 1
0 -1 1
-1 0 1
Sample Output
2
題目很繁瑣,該題大意為: 給定一個平面整點集,點與點間在|x1-x2| + |y1-y2| = 1時相鄰,且形成的圖沒有迴路, 每個點有一個可正可負的權值,求最大權和連通子圖。 
解題思路一:
/*
給定的是一顆樹,根據題意,我們可以從任意一個節點出發,必能訪問到其他所有節點,那麼dp的起點可以在任意一個節點。
我們從該起點出發,對以此點為根的樹的每個分支進行搜尋,採用樹的後續遍歷法則,對於每個子樹來說,dp值首先加上根節點
(因為要保證連通性,所以返回值中必須包含根節點的值,即使為負數也必須加上)先對每個分支dp,然後看分支dp的返回值是不是正數,
如果是正數,那麼我們就把該分支的返回值加入到該樹中去。
就是每個子樹的根節點(包括葉子節點)記錄dp[i][0]與dp[i][1],前一個表示不包含根的最大值,後一個表示包含根的最大值。
那麼我們可以得到對於dp[i][0],必然是所有分支中dp[child][0]與dp[child][1]中大於0的最大值的累加
(因為不包含樹根,所以在根節點上的連通性不用保證),
dp[i][1]必然是所有分支中dp[child][1]中大於0的最大值的累加再加上該樹根本身的值(因為要保證連通性)。
最後只要比較dp[root][0]與dp[root][1],輸出較大。
*/
#include <stdio.h>
#include <iostream>
using namespace std;
int Abs(int x)
{
    return x>=0?x:-x;
}
int max(int x,int y,int z)
{
    if(x<y) x=y;
    if(x<z) x=z;
    return x;
}
int max(int x,int y)
{
    return x>y?x:y;
}
struct P
{
    int x,y,c;
    void input()
    {
        scanf("%d%d%d",&x,&y,&c);
    }
    bool isConnect(P & t)
    {
        if(Abs(x-t.x)+Abs(y-t.y)==1) return 1;
        return 0;
    }
}p[1005];
struct Edge
{
    int v,next;
}edge[10005];
int edgeNum,head[1005];
int dp[1005][2];
void addEdge(int u,int v)
{
    edge[edgeNum].v=v;
    edge[edgeNum].next=head[u];
    head[u]=edgeNum++;
}
void dfs(int x,int father)
{
    dp[x][0]=0;
    dp[x][1]=p[x].c;
    for(int i=head[x];i!=-1;i=edge[i].next)
    {
        if(edge[i].v!=father)
        {
            dfs(edge[i].v,x);
            dp[x][0]=max(dp[x][0],dp[edge[i].v][0],dp[edge[i].v][1]);
            if(dp[edge[i].v][1]>0)
            {
                dp[x][1]+=dp[edge[i].v][1];
            }
        }
    }
}
int main()
{
    int n;
    while(scanf("%d",&n)!=EOF)
    {
        for(int i=0;i<n;i++)
        {
            head[i]=-1;
            p[i].input();
        }
        edgeNum=0;
        for(int i=0;i<n;i++)
          for(int j=i+1;j<n;j++)
          {
              if(p[i].isConnect(p[j]))
              {
                  addEdge(i,j);
                  addEdge(j,i);
              }
          }
        dfs(0,-1);
        printf("%d\n",max(dp[0][0],dp[0][1]));
    }
}
解題思路2:
//poj 1192 最優連通子集 (樹型DP)
/*
題意:給定一個平面整點集,點與點間在|x1-x2| + |y1-y2| = 1時相鄰,且形成的圖沒有迴路,
      每個點有一個可正可負的權值,求最大權和連通子圖。
題解:樹型DP,dp[i][0]表示以i為根的子樹上不包括i的最大權和,dp[i][1]表示包括i的最大權和。
*/
#include <iostream>
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;
const int inf = 1<<28;
int n,m;
struct Point
{
       int x,y,c;
}p[1010];

vector<int> con[1010];
int dp[1010][2],mark[1010];

void dfs(int v)
{
     mark[v]=1;
     dp[v][0]=0,dp[v][1]=p[v].c;
     int j;
     for (int i=0;i<con[v].size();i++)
     if (!mark[con[v][i]])
     {
         j=con[v][i];
         dfs(j);
         dp[v][0]=max(dp[v][0],max(dp[j][0],dp[j][1]));
         if (dp[j][1]>0) dp[v][1]+=dp[j][1];
     }
}

int main()
{
    scanf("%d",&n);
    for (int i=0;i<n;i++)
        scanf("%d%d%d",&p[i].x,&p[i].y,&p[i].c);
    for (int i=0;i<n;i++)
    for (int j=i+1;j<n;j++)
    if (abs(p[i].x-p[j].x)+abs(p[i].y-p[j].y)==1)
    {
          con[i].push_back(j);
          con[j].push_back(i);
    }
    dfs(0);
    printf("%d/n",max(dp[0][0],dp[0][1]));

    return 0;
}
解題思路3:
/*
這道題著實花了很長時間才搞明白,對圖的知識忘得太多了,看這道題之前最好先把圖的鄰接表表示方法看下,
有助於對這道題的理解,這道題讓我對深度優先遍歷又有了進一步的瞭解

這道題最好畫個圖,有助於理解,不過你要是畫個錯誤的圖此題就無解了

其實就是一個求無向樹的所有子樹和的最大值
樹形dp
dp[i][0]表示以i為根,不包括i結點的子樹獲得最大權
dp[i][1]表示以i為根,包括i結點的子樹獲得的最大權
dp[i][0] = max(dp[k][0], dp[k][1]) k為i的所有孩子結點
dp[i][1] = i結點的權 + dp[k][1] 如果dp[k][1] > 0
*/
#include <stdio.h>
#include <cstring>
#include <stdlib.h>

using namespace std;

struct node    //這個名字起得不是很恰當,說是結點,其實是邊,本題建的是有向邊
{
    int u;     //邊所指向的結點
    int next;  //與改變共起點的邊,和鄰接表表示圖的方法差不多,只不過這裡用的是一個int型的變數而不是一個指標
};

struct Point
{
    int x;
    int y;
    int c;
};
Point point[1015];
int head[1015];     //head[1]是以1開頭的邊
node edge[1015*10]; //表示邊
int visited[1015];
int n;
int count=0;
int dp[1015][2];

void init()
{
    memset(head,-1,sizeof(head)); //把以任何一個點的開始的邊都設定為一個無效的變數
}

void input()
{
    int i;
    scanf("%d",&n);
    for(i=0;i<n;i++)
    {
        scanf("%d %d %d",&point[i].x,&point[i].y,&point[i].c);
    }
}

void addEdge(int c ,int d)
{
    edge[count].u=d;         //這是一條有向邊
    edge[count].next=head[c];
    head[c]=count++;

    edge[count].u=c;        //這是另一條有向邊
    edge[count].next=head[d];
    head[d]=count++;
}

void buildTree()
{
    int i,j;
    for(i=0;i<n;i++)
    {
        for(j=i+1;j<n;j++)
        {
            if((abs(point[i].x - point[j].x) + abs(point[i].y - point[j].y)) == 1)
            {
                addEdge(i,j);    //新增一對有向邊
            }
        }
    }
}

int max(int a,int b)
{
    return a>b?a:b;
}

void dfs(int u)
{
    visited[u]=1;
    dp[u][0]=0;
    dp[u][1]=point[u].c;
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].u;
        if(visited[v]==0)
        {
            dfs(v);

            dp[u][0]=max(dp[u][0],max(dp[v][0],dp[v][1]));//注意權值有正有負,這個語句即更新dp[u][0]
            if(dp[v][1]>0)    //當改點不選時就沒有必要更新,因為值為1,當選的時候要更新該點,因為父節點要用到子結點的值
            {
            	/*這個語句更新dp[u][i],更新的方法其實差不多,u是父節點,v是子節點,當選u節點時,要加上其兒子,
				  若不選u節點時,比較自己和兒子節點
            	*/
                dp[u][1]=dp[u][1]+dp[v][1];
            }
        }
    }
}

int main()
{
    init();//初始化操作
    input();//解決輸入問題
    buildTree();//構造樹,採用鄰接表的形式
    dfs(0);// 深度優先遍歷
    printf("%d\n",max(dp[0][0],dp[0][1]));
    return 0;
}

hdu1561 樹形DP The More The Better

Problem Description
ACboy很喜歡玩一種戰略遊戲,在一個地圖上,有N座城堡,每座城堡都有一定的寶物,在每次遊戲中ACboy允許攻克M個城堡並獲得裡面的寶物。但由於地理位置原因,有些城堡不能直接攻克,要攻克這些城堡必須先攻克其他某一個特定的城堡。你能幫ACboy算出要獲得儘量多的寶物應該攻克哪M個城堡嗎?
 
Input
每個測試例項首先包括2個整數,N,M.(1 <= M <= N <= 200);在接下來的N行裡,每行包括2個整數,a,b. 在第 i 行,a 代表要攻克第 i 個城堡必須先攻克第 a 個城堡,如果 a = 0 則代表可以直接攻克第 i 個城堡。b 代表第 i 個城堡的寶物數量, b >= 0。當N = 0, M = 0輸入結束。
 
Output
對於每個測試例項,輸出一個整數,代表ACboy攻克M個城堡所獲得的最多寶物的數量。
Sample Input
3 2
0 1
0 2
0 3
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
0 0
Sample Output
5
13
思路:自己建立一個根root,使森林的根都成為root的孩子,然後樹形dfs+簡單揹包

0-1揹包裸程式碼:
for i=1..N
    for v=V..0
        f[v]=max{f[v],f[v-c[i]]+w[i]};

狀態轉移方程:f[root][k]=max(f[root][k],f[root][k-j]+dp[u][j]);

m是個數,j是存幾個,f[i][j]表示的是以i為根攻克j個城堡(且這j個城堡必須是它子樹上的,不包括它本身),dp[i][j]表示的是是以i為根攻克j個城堡(且這j個城堡必須是它子樹上的,一定它本身,ans[i]表示每個城堡的寶物,所以一定有dp[i][1]=ans[i];)。
for(int k=m;k>=0;k--)
      for(int j=0;j<=k;j++)
           f[root][k]=max(f[root][k],f[root][k-j]+dp[u][j]);

更新f[root][0~m]陣列,然後全部更新完之後更新dp[root][0~m]。
如圖所示樣例,先從root即0點訪問3,3沒有孩子,執行更新dp操作,因為所以葉子都滿足dp[i][0~m]=ans[i],所以dp[3][0~m]都等於ans[3],以下同理。

返回到root,更新f[0][m~0]。
訪問root-->2-->7-->6,訪問到葉子,更新dp[6][0~m]。返回7,更新f[7][m~0],
從7-->5,更新葉子節點dp[5][0~m],
從5-->7,再次更新f[7][m~0],
從7-->2,更新dp[7][0~m],返回2節點,更新f[2][m~0],
從2-->4,更新葉子節點dp[4][0~m],
從4-->2,更新f[2][m~0],
從2-->1,更新dp[1][0~m],
從1-->2,更新f[2][m~0],
從2-->root,更新dp[2][0~m],
更新f[0][m~0],更新dp[0][0~m]。
#include<stdio.h>
#include<string.h>
#define N 205
int n,m,edgeNum=0;
int ans[N],dp[N][N],f[N][N];
int visit[N],head[N];
struct Line{int v,next;}edge[N];
int max(int a,int b){return a>b?a:b;}
void add(int u,int v)
{
    edge[edgeNum].v=v;
    edge[edgeNum].next=head[u];
    head[u]=edgeNum++;
}
void dfs(int root)
{
    visit[root]=1;
    for(int i=head[root];i!=-1;i=edge[i].next)
    {
        int u=edge[i].v;
        if(!visit[u])
        {
            dfs(u);
            for(int k=m;k>=0;k--)
              for(int j=0;j<=k;j++)
              f[root][k]=max(f[root][k],f[root][k-j]+dp[u][j]);
        }
    }
    for(int i=1;i<=m+1;i++)
      dp[root][i]=f[root][i-1]+ans[root];
}
int main()
{
    int a,b;
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        if(n==0&&m==0) break;
        edgeNum=ans[0]=0;
        memset(f,0,sizeof(f));
        memset(dp,0,sizeof(dp));
        memset(head,-1,sizeof(head));
        memset(visit,0,sizeof(visit));
        for(int i=1;i<=n;i++)
        {
            scanf("%d%d",&a,&b);
            ans[i]=b;
            add(a,i);
        }
        dfs(0);
        printf("%d\n",dp[0][m+1]);
    }
}

樹形 dp 初探