樹形dp(IOI 2005河流程式碼理解)
阿新 • • 發佈:2019-01-26
題目描述
幾乎整個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是跑不動的。
然後就沒啥了,其餘還有點細節問題
再次表示深深的膜拜