1. 程式人生 > >動態規劃-樹形dp總結

動態規劃-樹形dp總結

一.簡單的從下到上和從上到下的統計

1.      dp[u]表示以u為根的一共有多少個節點.可以用來求重心.

2.      每個點出發能夠走得最遠的長度.dpm[u], dps[u]用來儲存u為根到子樹的最長距離,注意兩者區別是不同的兒子(不想交路徑).另外有一個f[u]指的是去掉u的兒子們之後,u可以發出的最長鏈.這樣的是一個規範的從上往下的dp.我是這樣定的:定狀態f[u]要求不能考慮u的兒子,具體考慮不考慮u看題意.比如這個就要看u.那麼我們的主要過程是已知u去推v.那麼遍歷u的兒子v,穿進去v的答案應該是1+非v的最長子樹鏈和1+u的答案.

3.  把一棵樹變成環.其實就是把一棵樹先變成一個鏈,然後再加一條邊變成環.而且變成環也好變.考慮u和v們,v首先需要變成鏈,但是每條鏈都有兩個機會不用拆,並且要求v在v的鏈中也是鏈段.這樣的話dp二維01,0帶便隨意,1代表強制根節點是一個鏈段.之後橫著dp.

flag = 1;

            f_2 = min(f_2 + dp[v][0] + 2, f_1+dp[v][1]);//v是不是一個//鏈端

if(f_2 > INF)

                f_2 = INF;

            f_1 = min(f_1 + dp[v][0] + 2, dp[v][1]+cnt*2+sum);//v//不是一個鏈端

            cnt++;

            sum += dp[v][0];

多開一維來描述根的某種狀態的方法很常用.本題是是否為鏈的一端.

(2)還有一題:不能選的點之間有相連的地方.開一維定義是否選根了

.

(3)另外cf也有一道題,要求一顆子樹上有且僅有一個黑色的點.那麼多開一維來看這個u為根的子樹是否有黑色的點.這樣從v和到u的時候就能夠決定是否是要不要切斷這條邊.

(4)1點往下面走,問在走的距離小於limit的時候最多能夠到達多少個不同的頂點.往下走要看回來不回來,因為回來才能夠繼續往下走,因此再開一維來描述是否回到了根節點.此外本題有一個小轉換,距離值一般太大,我們dp中寫的是走的點數,然後距離值作為dp的值.最後掃一遍出答案.

(5)一棵樹,上面都是村莊,水流只能夠往父親節點的方向流,題目強制1大根,並且1點有一個處理廠.一共可以建立k個處理廠.問最小的運費和.一共有100個點.要求每個點都有一個負責點

,並且它會往距離自己最近的負責點流.怎麼做?dpu的時候如果u不建的時候v的父親是誰?發現題目中說:只能往父親節點流.那麼很簡單,我們開dp[u][fuze][k]表示現在是fuze這個點來負責u,那麼如果u的兒子們沒有新建負責站,就只能是fuze來負責他們了.這樣就能夠算了.開個陣列dp[][][],然後就是揹包.dfs指明方向.

(6)  (5)題表面上一樣,但是題意重點完全不同:每個點會往自己最近的負責站走,並且可以隨便走,沒有兒子到父親的指向性了.每個點都有一個d的限制,要求它走的距離<=d就能與帶一個負責站.每個點建負責站都有自己的花費.怎麼做?其實會往最近的沒有什麼用,這個條件.我們擴充成:i依賴j的意思是j點建立了負責站使得i可以在d內滿足條件.之後怎麼解決兄弟之間的影響呢?有一個限制:如果在最優答案中,fa選的是x(x可能是任意一個點),那麼他的兒子v如果不在v的內部解決,會走一下fa,那麼一定之後會走fa的那個依賴節點.這樣走一定不走出最好的情況.因為既然v走過fa之後有一個更好的y來依賴,為什麼fa不去依賴y?所以我們可以把這個統一了.在寫的時候,dp[i][j]就是i以來的j,那麼i的兒子們要麼自己解決,要麼呼叫dp[v][j].每次的i全部都列舉1~nj.

4.      算樹上有幾個三個點組成的複雜路.反向思維轉換,找出所有的簡單路徑.只要是三個點並且要求有一個簡單路徑,那麼就是列舉中間點是誰就好,然後另外兩個分別在兩個兒子上,或者父親,如果另外一個在一個兒子或者父親上,一定會不能畫.並且由於這樣列舉的一定是中間的,因此一定沒有重複.用dp來記錄以u為根的節點數.然後需要橫著dp過去.

5.      計算一棵樹上,去掉一條邊之後剩下的兩棵樹分別的直徑.首先,求u為根子樹的直徑很好求:先搞出來距離dpm和dps,然後再dfs之後就搞出了直徑.第二:是重點.就像2說的,要去掉u,並且要考慮u的兄弟,因此我們不能定fa為f[fa],因為這樣過答案f要包含fa的一些兒子,違反了2中的規則.於是定f[u],含義是去掉u本身及u的兒子之後的樹的最長直徑.那麼下面就是如何從u推到v.看v的答案組成,包含u的答案,之後是u的非v的兒子們的最長直徑,之後是和u相關的,就是u向上的最長鏈和u非v的兩條最長鏈挑出兩個組成.u向上的最長鏈經常搞.這樣就搞出來了.因此需要向下的3條鏈,兒子中2個最大的直徑.下面的程式碼因為只存了1個u為根直徑,因此每次都要再遍歷一次找出兒子的2個最大的直徑….

if(fa !=0)//第一次不用算結果

    {//這是算結果用的,因為u的f定義已經得到答案了,因此可以算答案了.

        int t = max(u_ans, dia[u])*p[ans_id].w;

        if(t < ans)

        {

            ans = t;

            ans_i = ans_id;

        }

        else if(t == ans)

        {

            ans_i = min(ans_i, ans_id);

        }

    }

    int diam = 0, dias = 0;

    int nouse = 0;

    REP(i, 0, G[u].size())//先遍歷一遍,如果已經存過兩個直徑,就不用遍歷了

    {

        int v = G[u][i].to;

        if(v==fa)

            continue;

        gao(diam, dias, nouse, dia[v]);

    }

    //int uforv_l = u_l;

    REP(i, 0, G[u].size())//重點開始

    {

        int v = G[u][i].to;

        int id = G[u][i].no;

        if(v==fa)

            continue;

        int v_ans = u_ans;//v_ans的第一個組成

        if(diam==dia[v])//第二個

        {

            v_ans = max(v_ans, dias);

        }

        else

        {

            v_ans = max(v_ans, diam);

        }

        int a, b;//第三個

        if(dp[u][0]==1+dp[v][0])

        {

            a = dp[u][1];

            b = dp[u][2];

        }

        else if(dp[u][1] == 1+dp[v][0])

        {

            a = dp[u][0];

            b = dp[u][2];

        }

        else

        {

            a = dp[u][0];

            b = dp[u][1];

        }

        v_ans = max(a+b, v_ans);

        v_ans = max(a+u_l, v_ans);

        dfs(v, u, v_ans, 1 + max(a, u_l),id);//v_l很容易就算出來了

}

6.      求出一棵樹的直徑,並且問有多少個長度為這樣的直徑.關鍵是記數.並且整明白不相交路徑和不同的路徑的區別. 對於u,在統計完子樹v內部的直徑後,應該是有u參與,伸出來兩條鏈的.關鍵有兩點,一個是u到v的最大長度,並且最大長度下有幾個不同的路.第二個關鍵是要橫著做dp,儲存一個最長的,並且累加最長的個數,然後看當前的v加上最長的來更新直徑.

7.      Bob和alice一起走,輪流選擇路.bob儘量往遠的,alice儘量往近的走,並且他倆都知道他們走的限制有一個[L, R]的限制.這樣其實從上往下的一個貪心,實際上不需要陣列的記錄記錄下來也挺好.但是這道題目的關鍵是L到r的限制是對的全域性的,在u為根的子樹上如果我們要的是最大值,我們實際上是在l到r範圍內的最大值,因此我們需要dfs的時候,穿進去到u已經有多少的距離了.再去遍歷v中找.

8.      用數字染色,要求相鄰的數字不能夠相同,並且最後每個節點的和加一起的和最小.重點是我們不能只用0和1就染到最好的.多用幾種,可能20種. 這樣之後,就是多開一維來記錄根節點是染成什麼.

二.用邊來考慮對答案的貢獻.

1.      一棵樹,隨便怎麼移動每一個點,只要最後每個點有一個地方,每個地方有一個點.每個點從a到b的長度是點對答案的貢獻,求所有最大的貢獻度和是多少.一個點跑到哪不好弄,但是換個角度,用邊的思維:一條邊,在最優答案中一定可以全部都跨過去.你那麼這條邊的貢獻就是2*w*min(兩邊的點數).轉化成了用邊對答案的貢獻+簡單的樹形dp

2.      一棵樹,選擇k個點,使得這些點中兩兩的距離和最小.要求o(n*k*k).對於一棵樹內,給他多少個點,並且自己內部距離和都好確定.但是一旦當成一個整體,和外部的點的關係就不好描述了.我們可以這樣:在u為根的子樹中,從v轉移過來,揹包分配給v,那麼v的點和外部的互動怎麼搞?這樣:v的內部的點看做一個整體,變成u這個整體之前,要加上邊u->v的貢獻值,並且是num[v]和n-num[v]所有的.這樣一來就能夠dp了.在u為根內的看做一個整體,在從v和到u的時候把u->v邊對答案的貢獻算出來.

三.先進行一些變換然後再定狀態再dp

1.      一棵樹,每個點都有一些寶藏,路都有時間消耗.有一個s和一個t,還有時間limit,s點出發要求在時間limit內必須從t出去.在這個要求下問最多的寶藏數.n*limit<=1e6.直接定狀態的話:因為需要最終結果走到t處,於是先要找到t所在的一條從s發出的線,在這條線上的是直走一次就好,而不再線上的,必須走下去再返回.這其實可以這樣想:s到t的路是一定要跑的,並且絕逼只會跑一次,不會重複跑.那麼我第一次先走一邊路,並且把寶藏都拿了,時間都改為0.之後就是普通的揹包了.這種先走一些貪心的東西可以簡化這道題.

2.      要求只能切斷limit的邊,問在切邊花費值<cost的最大limit是多少.注意可以二分limit,之後就是一個必須要選擇的樹形dp.二分很重要.

3.      列舉分叉點.一個人在x,要到y和到z,走,並且如果x到y更近的話,他會先走到y,然後從y再往z走.路徑顯然可以重複.在這個圖中,x y z都是不確定的可變的.問在這麼多種情況中最遠的路程是多少.我們一開始會想到列舉y,然後找兩條最長的不同的(但是可以相交)長鏈,但是我們發現不行,這樣會不好滿足(y近先走y,z近先走z).列舉z和y肯定都不行.那麼怎麼辦呢?我們把圖畫出來,列舉分叉點!就是獨立於x,y,z之後的一個分叉點,從x走到y從y往z走的時候可能會走一段已經走過的路程.那個新的點就是分叉點這樣有了分叉點之後我們就可以找出3條最長的連然後按照題目中的來搞就行了.關鍵是列舉分叉點.

4.      一棵樹,但是上面的邊有方向.挑一個點出發,能夠走遍所有的點,不能走的邊要改變方向,有1的代價.讓我們挑出兩個點,使得代價和最小.在o(n^2)的時間複雜度內就行了.和4.2的一樣,只是可以搞2個了.我們先變換一下,為什麼會有兩個點?我們一定在最終結果中會有一條邊不想改變,連線了兩個起點.我們只需列舉這條邊是誰,然後兩邊分別o(n)的dp就行了.

四.先找一個根,然後在樹上快速轉移

1.      一個人要旅遊,從樹上選擇一個節點作為根據地,每次旅遊一個地方就要再回來,每個點可能旅遊若干次,求出最短的路程以選取的點是什麼.先隨便找一個點,可以很快的算出.之後發現一個點往另一個點轉很好轉,u已經算出來的,往v轉,只需加加減減u到v這條邊的資訊就好了.當然也可以用u只去掉u的兒子們的方法從上往下來做,把答案分成了兩部分.但是這樣做更加簡單.

2.      一棵樹,但是上面的邊有方向.挑一個點出發,能夠走遍所有的點,不能走的邊要改變方向,有1的代價.讓我們挑出一個點,使得代價和最小.先隨便找一個根,搞出來.然後很簡單的就能在上面轉移.(其實也可以用u只去掉u的兒子的方法,之後u->v的來搞,但是這樣轉移更好寫).