深入理解樹形揹包

【題目描述】P1273 【有線電視網】

某收費有線電視網計劃轉播一場重要的足球比賽。他們的轉播網和用戶終端構成一棵樹狀結構,這棵樹的根結點位於足球比賽的現場,樹葉爲各個用戶終端,其他中轉站爲該樹的內部節點。

從轉播站到轉播站以及從轉播站到所有用戶終端的信號傳輸費用都是已知的,一場轉播的總費用等於傳輸信號的費用總和。

現在每個用戶都準備了一筆費用想觀看這場精彩的足球比賽,有線電視網有權決定給哪些用戶提供信號而不給哪些用戶提供信號。

寫一個程序找出一個方案使得有線電視網在不虧本的情況下使觀看轉播的用戶儘可能多。

【題目分析】

這是一個非常不錯的樹形揹包的題目,有助於理解樹形揹包的實質! 按照常見樹形揹包定義狀態:設dp[u][j]表示在以u爲根的子樹中,選擇j個客戶所能獲得的最大收益。

狀態轉移:dp[u][j]=max(dp[u][j-k],dp[v][k]-w(u,v));

樹形揹包一直有個疑惑,爲什麼在枚舉揹包容量的時候要反向枚舉,淺層次的理解就是套用01揹包枚舉體積;但是細想又不對,因爲01揹包只有在使用滾動數組的時候才必須逆推;如果使用2維的話不需要;

那是不是意味着樹形揹包中原本應該是3維,滾動優化爲2維了呢???

仔細回味01揹包的狀態定義:f[i][j]表示在前i個物品佔用體積爲j的空間能夠得到的最大價值;這裏有一個限定前i個,初次學習樹形揹包一看人家的代碼感覺就是這麼寫的,並沒有深入去思考爲什麼。

所以仿照01揹包定義dp[i][u][j]表示在以u爲根的子樹的前i棵子樹中選擇j個節點能夠獲得的最大費用

dp[i][u][j]=max(dp[i-1][u][j-k]+dp[v的總兒子數][v][k]-w(u,v))

以上就是樹形dp沒有滾動優化的狀態方程,爲什麼需要滾動優化?爲什麼可以滾動優化?

爲什麼需要滾動優化?————不滾動很可能會MLE

爲什麼可以滾動優化???

1.對於u而言很顯然是可以滾動優化的,只不過j-k<j要保證dp[u][j-k]是第i-1輪的值則j必須要逆推!!!

2.關鍵在於v,滾動優化後怎麼保證每次dp[v][k]==dp[v的總兒子數][v][k]? 其實關鍵還是在於01揹包的本質的理解,dp[i][v][j]從v的前i個子樹中選擇j個節點,遞推求解的時候是i依次增大的,所以結束時一定有"i==v的總兒子數" 因此使用滾動數組計算v結束後,dp[v][k]中一定是保存的從v的所有兒子中選k個節點的值;

從而保證dp[v][k]==dp[v的總兒子數][v][k]

#include<bits/stdc++.h>
using namespace std;
const int N=3001;
struct node{
    int to,w,nxt;
    node(int a=0,int b=0,int c=0):to(a),w(b),nxt(c){}
} g[N];
int fir[N],cnt,money[N],n,m,dp[N][N];
void add(int f,int t,int w){
    g[++cnt]=node(t,w,fir[f]);
    fir[f]=cnt;
}
int dfs(int u){
    if(u>n-m){//u是一個葉子節點 
        dp[u][1]=money[u];//葉子節點則選一個節點得到最大利益一定是該節點出的錢 
        dp[u][0]=0;
        return 1;
    }
    dp[u][0]=0;//不選任何節點一定收益爲0 
    int leafs=0;//統計以u爲根的子樹中葉子的數目 
    for(int i=fir[u];i;i=g[i].nxt){
        int v=g[i].to,w=g[i].w;
        int t=dfs(v);//t是v爲根的子樹中的葉子節點數
        leafs+=t;
        for(int j=leafs;j>0;j--)//在前i個子樹中選,因此這裏只需要知道前i個子樹的總結點數,而不是u的所有節點個數 
            for(int k=0,kk=min(j,t);k<=kk;k++)//v最多分到的節點不可能超過他的葉子數,也不能超過其所在子樹節點總數 
                dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]-w);
    }
    return leafs;
}
int main(){
    int x,t,w;
    cin>>n>>m;
    for(int i=1;i<=n-m;i++){
        cin>>x;
        for(int j=0;j<x;j++){
            cin>>t>>w;
            add(i,t,w);
        }
    }
    for(int j=n-m+1;j<=n;j++)
        cin>>money[j];
    memset(dp,0x8f,sizeof(dp));
    memset(dp,0,sizeof(dp[0]));
    dfs(1);
    for(int i=m;i>=0;i--){//將用戶反向枚舉,一旦發現非負數就輸出
        if(dp[1][i]>=0){
            cout<<i<<endl;
            break;
        }
    } 
    return 0;
}

 

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