1. 程式人生 > >樹形dp(IOI 2005河流程式碼理解)

樹形dp(IOI 2005河流程式碼理解)

題目描述

幾乎整個Byteland王國都被森林和河流所覆蓋。小點的河匯聚到一起,形成了稍大點的河。就這樣,所有的河水都匯聚並流進了一條大河,最後這條大河流進了大海。這條大河的入海口處有一個村莊——名叫Bytetown 在Byteland國,有n個伐木的村莊,這些村莊都座落在河邊。目前在Bytetown,有一個巨大的伐木場,它處理著全國砍下的所有木料。木料被砍下後,順著河流而被運到Bytetown的伐木場。Byteland的國王決定,為了減少運輸木料的費用,再額外地建造k個伐木場。這k個伐木場將被建在其他村莊裡。這些伐木場建造後,木料就不用都被送到Bytetown了,它們可以在 運輸過程中第一個碰到的新伐木場被處理。顯然,如果伐木場座落的那個村子就不用再付運送木料的費用了。它們可以直接被本村的伐木場處理。 注意:所有的河流都不會分叉,也就是說,每一個村子,順流而下都只有一條路——到bytetown。 國王的大臣計算出了每個村子每年要產多少木料,你的任務是決定在哪些村子建設伐木場能獲得最小的運費。其中運費的計算方法為:每一塊木料每千米1分錢。 編一個程式: 1.從檔案讀入村子的個數,另外要建設的伐木場的數目,每年每個村子產的木料的塊數以及河流的描述。 2.計算最小的運費並輸出。

輸入

第1行:包括兩個數 n(2<=n<=100),k(1<=k<=50,且 k<=n)。n為村莊數,k為要建的伐木場的數目。除了bytetown外,每個村子依次被命名為1,2,3……n,bytetown被命名為0。

接下來n行,每行3個整數 wi——每年i村子產的木料的塊數 (0<=wi<=10000) vi——離i村子下游最近的村子(即i村子的父結點)(0<=vi<=n) di——vi到i的距離(千米)。(1<=di<=10000)

保證每年所有的木料流到bytetown的運費不超過2000,000,000分 50%的資料中n不超過20。

輸出

輸出最小花費,精確到分。

樣例輸入

4 2 1 0 1 1 1 10 10 2 5 1 2 3

樣例輸出

4

說明

下圖是樣例輸入的說明。圈內數字為村莊編號,下方數字為儲存木料塊數,邊權為河的長度。伐木場應建在村莊2和3。


應該比較好看出來這是一道樹形dp,而dp方程式有點難想,剛開始時自己想只有兩維,分別是當前村莊i,和所需建的伐木場個數k。然後怎麼想也想不出來,於是就搜了一下題解。 下面是搜出來的題解程式碼(有改動)
#include<iostream>
#include<cmath>
#include<climits>
#include<cstdio>
#include<cstring>
#include<algorithm>

typedef unsigned long long uLL;
const unsigned long long inf=2000000005;
const int MAXN=100;
using namespace std;
struct node{int rc,lc,w,d;}tree[MAXN+5];
int cnt,n,K,a,dist[MAXN+5],H[MAXN+5],fa[MAXN+5];
uLL F[MAXN+5][MAXN+5][55];
bool vis[MAXN+5];

void dfs(int s,int deep){
    H[++cnt]=s; vis[s]=1; dist[s]=deep;
    for(int i=tree[s].lc;i;i=tree[i].rc)
        if(!vis[i]) dfs(i,deep+tree[i].d);
}
inline void Read(int &Ret){
    char ch; bool flag=0;
    for(;ch=getchar(),ch<'0'||ch>'9';)if(ch=='-') flag=1;
    for(Ret=ch-'0';ch=getchar(),'0'<=ch&&ch<='9';Ret=Ret*10+ch-'0');
    flag&&(Ret=-Ret);
}
int main()
{
    Read(n); Read(K);
    memset(fa,-1,sizeof(fa));
    for(int i=1;i<=n;i++)
    {
       Read(tree[i].w); Read(a); Read(tree[i].d);
       tree[i].rc=tree[a].lc;
       tree[a].lc=i; fa[i]=a;
    }
    dfs(0,0);
    memset(F,inf,sizeof(F));
    memset(F[0],0,sizeof(F[0]));
    for(int i=n+1;i>=2;i--)
    {
        int now=H[i];
        int lson=tree[now].lc,rson=tree[now].rc;
        for(int j=fa[now];j!=-1;j=fa[j])
            for(int k=0;k<=K;k++)
            {
                for(int l=0;l<=k;l++)//不選 i
                    if(F[lson][j][l]!=inf&&F[rson][j][k-l]!=inf)
                    {
                        uLL add=(uLL)tree[now].w*(dist[now]-dist[j]);
                        F[now][j][k]=min(F[now][j][k],F[lson][j][l]+F[rson][j][k-l]+add);
                    }
                for(int l=0;l<k;l++)//選 i
                    if(F[lson][now][l]!=inf&&F[rson][j][k-l-1]!=inf)
                        F[now][j][k]=min(F[now][j][k],F[lson][now][l]+F[rson][j][k-l-1]);
            }
    }
    printf("%llu",F[tree[0].lc][0][K]);
}


第一眼看過去的時候就覺得很詭異,因為這位大神竟然沒有用到遞迴,而是直接套了四層迴圈搞了出來。 dp的狀態有三維: 用 f[i][j][k] 表示目前考慮第i個點,j表示i所有的祖先中離i最近並且建了伐木場的節點,i的兒子以及i的兄弟一共建了k個伐木場。 (在i建)f[i][j][k]=min(f[i][j][k],f[lson][i][l]+f[rson][j][k-l-1]) (不在i建)f[i][j][k]=min(f[i][j][k],f[lson][j][l]+f[rson][j][k-l]+val[i]*(dis[i][j])) 這是直接copy的那位大神的敘述。應該是比較清晰的。注意這是已經轉成二叉數後的方程式。 解釋一下: 當在i建時,離i最近的伐木場自然是i本身,而他的兄弟節點(也就是他的rson,轉了二叉),最近的依然是j,因為i已建立伐木場,所以i的兒子還有l個伐木場可建,而兄弟就還有k-l-1個伐木場可建 而不在i建時,離i和它的兄弟們最近的伐木場自然仍然是j,其餘同上。而這時需要加上木材運到j的花費了(上面的那種狀態不用的原因是因為還未確定是否在除i以外的其他節點建造伐木場,i本身是沒有花費的) 都說程式設計有三難:思路難,程式碼難,除錯難。 將思路解決了,在來看看這個不用遞迴的神奇程式碼 在處理輸入的時候就是把多叉轉成二叉,並沒有什麼特殊的處理。 而需要注意的是dfs: 這個dfs首先會將每個節點距根節點的距離處理出來,儲存在dist陣列中 然後給每個節點賦予一個按照先序遍歷的順序的編號,在列舉節點的時候,就是按這個編號的從大到小來列舉的。 那麼為什麼可以按照先序遍歷來處理呢? 畫個圖可以發現,每一個子樹本身必定大於它的所有的兒子節點,或者更準確地說,對於每一個子樹i,它的編號都剛好比它的某個兒子小1,也就意味著,除葉子節點以外,每個節點都能保證它所有兒子節點都處理完後再來處理它本身,那麼恰好滿足dp的狀態轉移所需要的順序了。(表示深深的膜拜)
然後是有一句話memset(F[0],0,sizeof(F[0])); 我之前一直都沒想通,為何要把它清為0。 後來有點明白了。 注意,這裡的F[0]並不是指的根節點,而是指葉子節點的下方節點,如果它們的值也是inf,那麼你會發現,dp是跑不動的。 然後就沒啥了,其餘還有點細節問題 再次表示深深的膜拜