2017清北學堂(提升組精英班)集訓筆記——動態規劃Part3

如今是晚上十二點半,好累(無奈臉),接着給各位——也是給本身,更新筆記吧~html

序列型狀態劃分:算法

經典例題:乘積最大(Luogu 1018)編程

* 設有一個長度爲 N 的數字串,要求選手使用 K 個乘號將它分紅 K+1 個部分,找出一種分法,使得這 K+1 個部分的乘積可以爲最大。
* 例如,有一個數字串: 312,當 N=3, K=1 時會有如下兩種分法:
1 3×12=36
2 31×2=62
* 符合題目要求的結果是: 31×2=62
* 如今,請你幫助你的好朋友 XZ 設計一個程序,求得正確的答案。
* 題目中要求把整個序列有序地切分紅 K+1 個部分。
* 狀態設計:設咱們把前 F[i][j] 爲前 i 個數字切成 j 部分所能獲得的最大乘積。
* 很顯然這時候咱們只須要經過枚舉最後一部分的數字有多大,就能獲得結果乘積了。(知足最優性)
數組

1 //T20:乘積最大(DP)
2 ++k;//K+1
3 f[0][0]=1;//初值
4 for(int i=1;i<=n;++i)//枚舉長度
5     for(int j=1;j<=k;++j)//枚舉切割部分
6         for(int l=0;l<i;++l)//枚舉前一塊的最後位置(能夠從0開始)
7             f[i][j]=max(f[i][j],f[l][j-1]*val(l+1,i));//val(a,b)表示a~b之間造成的數 
8 cout<<f[n][k]<<endl;//答案

爲何不設計狀態爲 F[i][j][k],表示把 i 到 j 的數字切成 k 塊?學習

由於不須要求出中間一塊要被一個*號截開的狀況,不必求出中間一塊[i,j]的最大乘積。優化

經典例題:數字遊戲(Luogu 1043)
  丁丁最近沉迷於一個數字遊戲之中。這個遊戲看似簡單,但丁丁在研究了許多天以後卻發覺原來在簡單的規則下想要贏得這個遊戲並不那麼容易。遊戲是這樣的,在你面前有一圈整數(一共 n 個),你要按順序將其分爲m 個部分,各部份內的數字相加,相加所得的 m 個結果對 10 取模後再相乘,最終獲得一個數 k。遊戲的要求是使你所得的 k 最大或者最小。
  例如,對於下面這圈數字( n=4, m=2):
  要求最小值時, ((2-1) mod 10)×((4+3) mod 10)=1×7=7,要求最大值時,爲 ((2+4+3) mod 10)×(-1 mod 10)=9×9=81。特別值得注意的是,不管是負數仍是正數,對 10 取模的結果均爲非負值。
網站

* 題目中要求把整個環形序列切分紅 m 個部分;先不考慮環形的問題,倘若是對於一個線形序列,如何作?
* 狀態設計:參考前一道題目的作法,設咱們把前 F[i][j] 爲前 i 個數字切成 j 部分所能獲得的最大乘積。
* 很顯然這時候咱們只須要經過枚舉最後一部分的數字有多大,就能獲得結果乘積了。 (知足最優性)
* 環形序列:第一個位置不必定是開頭,有可能位於序列的中間。
* 解決方法:枚舉每個位置,把它看成開頭算一遍,獲得的結果取最大值即爲答案。
當出現循環節時候,咱們就能夠用「複製一遍序列」的方法來作,這樣就能夠實現頭尾相接,以下圖:
spa

對於每一條鏈,像上一題那樣作就OK!設計

 1 //T21:數字遊戲(DP)
 2 void work(int *a)//預處理過程 
 3 {
 4     s[0]=0;//預處理前綴和
 5     for(int i=1;i<=n;++i) s[i]=s[i-1]+a[i];
 6     for(int i=0;i<=n;++i)
 7         for(int j=0;j<=m;++j)
 8             f[i][j]=0,g[i][j]=INF;//初值
 9     f[0][0]=g[0][0]=1;//初值
10     ...//DP過程
11 } 
 1 //T21:數字遊戲(DP)
 2 void work(int *a)
 3 {
 4     ...//初值,預處理
 5     for(int i=1;i<=n;++i)
 6         for(int j=1;j<=m;++j)
 7             for(int k=0;k<i;++k)//分別計算最大值和最小值
 8             {
 9                 //問題:爲何f不用判斷而g須要判斷,最大值的話也是不合法的,可是對答案沒啥影響,由於0確定不是最大值
10                 f[i][j]=max(f[i][j],f[k][j-1]*(((s[i]-s[k])%10+10)%10));
11                 if(g[k][j-1]!=INF)//g[k][j-1]=INF表明前k個不能切成j-1個部分,當i到k這一段爲0的時候,前面一段的最小值=inf會有問題
12                     g[i][j]=min(g[i][j],g[k][j-1]*(((s[i]-s[k])%10+10)%10));
13             }
14     amax=max(amax,f[n][m]);//全局最大值 
15     amin=min(amin,g[n][m]);//全局最小值 
16 }
 1 //T21:數字遊戲(DP)
 2 //主程序
 3 cin>>n>>m;
 4 for(int i=1;i<=n;++i) cin>>a[i],a[n+i]=a[i];//複製一遍
 5 amax=0;
 6 amin=INF;//初值
 7 for(int i=1;i<=n;++i)//以每一個位置開始計算(以a[i-1]爲開頭的數組(指針)保證枚舉每一條鏈) 
 8 {
 9     work(a+i-1);
10 } 
11 cout<<amin<<endl<<amax<<endl;

經典例題:能量項鍊(Luogu 1063)3d

  在 Mars 星球上,每一個 Mars 人都隨身佩帶着一串能量項鍊。在項鍊上有 N 顆能量珠。能量珠是一顆有頭標記與尾標記的珠子,這些標記對應着某個正整數。而且,對於相鄰的兩顆珠子,前一顆珠子的尾標記必定等於後一顆珠子的頭標記。由於只有這樣,經過吸盤(吸盤是 Mars 人吸取能量的一種器官)的做用,這兩顆珠子才能聚合成一顆珠子,同時釋放出能夠被吸盤吸取的能量。若是前一顆能量珠的頭標記爲 m,尾標記爲r,後一顆能量珠的頭標記爲 r,尾標記爲 n,則聚合後釋放的能量爲m*r*n( Mars 單位),新產生的珠子的頭標記爲 m,尾標記爲 n。
  須要時, Mars 人就用吸盤夾住相鄰的兩顆珠子,經過聚合獲得能量,直到項鍊上只剩下一顆珠子爲止。顯然,不一樣的聚合順序獲得的總能量是不一樣的,請你設計一個聚合順序,使一串項鍊釋放出的總能量最大。
  例如:設 N=4, 4 顆珠子的頭標記與尾標記依次爲 (2, 3) (3, 5) (5,10) (10, 2)。咱們用記號 ⊕ 表示兩顆珠子的聚合操做, (j⊕k) 表示第 j, k 兩顆珠子聚合後所釋放的能量。則第 四、 1 兩顆珠子聚合後釋放的能量爲:

- (4⊕1)=10*2*3=60
*這一串項鍊能夠獲得最優值的一個聚合順序所釋放的總能量爲:
- ((4⊕1)⊕2)⊕3) =10*2*3+10*3*5+10*5*10=710
* 環形序列:枚舉每個位置,把它看成開頭算一遍,獲得的結果取最大值即爲答案。
* 那麼咱們只須要考慮線性序列的問題了。

如上題作法,咱們仍是把這個序列複製一遍,最後剩下的珠子爲最後一個數

* 狀態設計:設 F[i][j] 爲把 i 到 j 之間的珠子合併起來,所能釋放的最大能量。
* 問題:爲何不能只設 F[i] 表明前 i 個珠子合併的最大能量。
- 由於和合並的方式有關
* 狀態轉移:枚舉最後一顆和 i, j 一同合併的珠子 k。

 1 //T22:能量項鍊(DP)
 2 int work(int *a)//對一段線性序列進行DP(枚舉i~j合併起來,再枚舉中間的k來合併) 
 3 {
 4     for(int i=0;i<=n;++i)
 5         for(int j=0;j<=n;++j) f[i][j]=0;
 6     for(int s=2;s<=n;++s)//動態規劃(從2開始——最少三個進行合併)
 7         for(int i=0;i+s<=n;++i)
 8         {
 9             int j=i+s;
10             for(int k=i+1;k<j;++k)//枚舉中間的一顆
11                 f[i][j]=max(f[i][j],f[i][k]+f[k][j]+a[i]*a[k]*a[j]);
12         }
13     return f[0][n];//答案
14 }

經典例題:擺花(Luogu 1077)

  小明的花店新開張,爲了吸引顧客,他想在花店的門口擺上一排花,共 m盆。經過調查顧客的喜愛,小明列出了顧客最喜歡的 n 種花,從 1 到n 標號。爲了在門口展出更多種花,規定第 i 種花不能超過 ai 盆,擺花時同一種花放在一塊兒,且不一樣種類的花需按標號的從小到大的順序依次擺列。
  試編程計算,一共有多少種不一樣的擺花方案。
- 2 種花,要擺 4 盆
- 第一種花不超過 3 盆,第二種花不超過 2 盆
- 答案: 2

* 狀態設計:設 F[i][j] 爲擺到第 i 種花,共擺了 j 盆,的總方案數(枚舉最後一種花擺了多少盆,往前推)

1 //T24:擺花(DP)時間複雜度:O(nm^2) 用前綴和能夠優化爲O(m)
2 f[0][0]=1;
3 for(int i=1;i<=n;++i)
4     for(int j=0;j<=m;++j)
5         for(int k=0;k<=j&&k<=a[i];++k)
6             f[i][j]=(f[i][j]+f[i-1][j-k])%MOD;
7 int ans=f[n][m];

經典例題:書本整理(Luogu 1103)

  Frank 是一個很是喜好整潔的人。他有一大堆書和一個書架,想要把書放在書架上。書架能夠放下全部的書,因此 Frank 首先將書按高度順序排列在書架上。可是 Frank 發現,因爲不少書的寬度不一樣,因此書看起來仍是很是不整齊。因而他決定從中拿掉 k 本書,使得書架能夠看起來整齊一點。
  書架的不整齊度是這樣定義的:每兩本書寬度的差的絕對值的和。例若有4 本書:
- 1x2 5x3 2x4 3x1 那麼 Frank 將其排列整齊後是:
- 1x2 2x4 3x1 5x3 不整齊度就是 2+3+2=7
- 已知每本書的高度都不同,請你求出去掉 k 本書後的最小的不整齊度。

* 第一步:先把書本按照高度排序!
* 狀態設計:從 N 本書選出 N-k 本書;設 F[i][j] 爲從前 i 本書選出j 本書的最小的不整齊度
* 狀態轉移:討論第 i 本書選不選?

* 上一種方法行不通!
- 爲何?咱們須要計算選出相鄰兩本書之間的寬度的差的絕對值,而咱們討論第i本書選不選是算不出相鄰兩本書的不整齊度,由於你不知道上一本你選了哪本
* 狀態設計:從 N 本書選出 N-k 本書;設 F[i][j] 爲從前 i 本書選出j 本書的最小的不整齊度,而且第 i 本書必需要選
* 狀態轉移:上一本書選的是什麼?

 1 //T23:書本整理(DP)
 2 int m=n-k;//共選出m本書
 3 for(int i=1;i<=n;++i)
 4     for(int j=1;j<=m;++j)
 5         f[i][j]=INF;//初值
 6 for(int i=1;i<=n;++i) f[i][1]=0;//初值,第一本書沒有前一本,爲0
 7 for(int i=2;i<=n;++i)
 8     for(int j=2;j<=m;++j)
 9         for(int l=1;l<i;++l)//l爲上一本書
10             f[i][j]=min(f[i][j],f[l][j-1]+abs(a[i]-a[l]));
11 int ans=INF;
12 for(int i=1;i<=n;++i) ans=min(ans,f[i][m]);//答案:最後一本書能夠是任意一本

樹形DP:

經典例題:奶牛家譜(Luogu 1472)

  農民約翰準備購買一羣新奶牛。在這個新的奶牛羣中, 每個母親奶牛都生兩個小奶牛。這些奶牛間的關係能夠用二叉樹來表示。這些二叉樹總共有 N 個節點 (3 <= N < 200)。這些二叉樹有以下性質:
  每個節點的度是 0 或 2。度是這個節點的孩子的數目。
  樹的高度等於 K(1 < K < 100)。高度是從根到最遠的那個葉子所須要通過的結點數; 葉子是指沒有孩子的節點。
  有多少不一樣的家譜結構? 若是一個家譜的樹結構不一樣於另外一個的, 那麼這兩個家譜就是不一樣的。輸出可能的家譜樹的個數除以 9901 的餘數。
- 5 個節點,高度爲 3
- 答案:可能的家譜樹個數爲
2

* 狀態設計:看題說話(十分簡單),設 F[i][j] 爲 i 個節點高度爲 j 的樹,一共有多少種方案。
- 問題:如何進行狀態轉移?
- 分紅左右兩棵子樹。

* 狀態轉移:枚舉子樹的狀態
- 限制:注意要知足深度的限制

                

* 狀態轉移: F[i][j] = ∑ (k 爲左子樹大小, l 爲右子樹深度)+ F[k][j-1] * F[i-k-1][l] * 2 (l < j-1) (左右子樹只有一棵深度爲 j-1,直接翻倍)+ F[k][j-1] * F[i-k-1][j-1] (左右子樹深度均爲 j-1,不重複計算)
注意:在狀態轉移方程中:當咱們左右子樹的深度相同的話f[i][j] +=  f[k][j-1] * f[i-k-1][j-1] ,當左右子樹深度不一樣的話咱們能夠截取到深度相同的部分,把下面全部往深度小的移(如上圖右所示,把紫色樹紅線下方的子樹所有移到右邊去,變成紅樹),因此ans×2。

 1 //T28:奶牛家譜(DP)時間複雜度:O(n^4)
 2 f[1][1]=1;//初值,深度爲1的子樹只有一種狀況
 3 for(int i=3;i<=n;++i)
 4     for(int j=2;j<=m;++j)
 5         for(int k=1;k<i;++k)
 6         {
 7             for(int l=1;l<j-1;++l)//l<j-1,結果乘2
 8             f[i][j]=(f[i][j]+f[k][j-1]*f[i-k-1][l]*2%MOD)%MOD;//左子樹方案數*右子樹方案數(乘法原理) 
 9             f[i][j]=(f[i][j]+f[k][j-1]*f[i-k-1][j-1]%MOD)%MOD;//左右子樹深度相同,均爲j-1
10         }
11 int ans=f[n][m];//答案以是任意一本
 1 //T28:奶牛家譜(DP/前綴和優化)時間複雜度:O(n^3)
 2 f[1][1]=g[1][1]=1;
 3 for(int j=2;j<=m;++j) g[1][j]=1;//前綴和初始化
 4 for(int i=3;i<=n;++i)
 5     for(int j=2;j<=m;++j)
 6     {
 7         for(int k=1;k<i;++k)//注意到把深度小於j-1的方案所有加起來利用前綴和能夠略去枚舉過程
 8         {
 9             f[i][j]=(f[i][j]+f[k][j-1]*g[i-k-1][j-2]*2%MOD)%MOD;
10             f[i][j]=(f[i][j]+f[k][j-1]*f[i-k-1][j-1]%MOD)%MOD;
11         }
12         g[i][j]=(g[i][j-1]+f[i][j])%MOD;//前綴和計算
13     }
14 int ans=f[n][m];

經典例題:最大子樹和(Luogu 1122)

  一株奇怪的花卉,上面共連有 N 朵花,共有 N-1 條枝幹將花兒連在一塊兒,而且未修剪時每朵花都不是孤立的。每朵花都有一個「美麗指數」,該數越大說明這朵花越漂亮,也有「美麗指數」爲負數的,說明這朵花看着都讓人噁心。所謂「修剪」,意爲:去掉其中的一條枝條,這樣一株花就成了兩株,扔掉其中一株。通過一系列「修剪「以後,還剩下最後一株花(也多是一朵)。老師的任務就是:經過一系列「修剪」(也能夠什麼「修剪」都不進行),使剩下的那株(那朵)花卉上全部花朵的「美麗指數」之和最大。
* 樣例以下圖(紅色樹即爲答案):

* 問題轉化:在這棵樹中取出若干個連通的節點,使得權值之和最大。
* 觀察: 以下圖,根節點爲 0。假如咱們必需要取根節點 0;同時它有三個兒子,權值分別爲 2, 0, -3;則咱們能取得的最大的權值是多少?

- 貪心地,咱們只取不小於 0 的節點。(思路相似於求最大子段和)
* 算法思想: 貪心的只取不小於 0 的兒子。
* 狀態設計:設 F[i] 爲只考慮以 i 爲根的這棵子樹,而且一定要取 i 這個點,可能達到的最大權值是多少。
* 狀態轉移:把兒子中 F[x] 大於 0 的加起來便可。

 1 //T29:最大子樹和(DP)
 2 void dfs(int x,int y=0)//y表明父親節點(當傳入參數是一個時,y默認爲0,當傳入兩個參數,缺省參數y纔會被修改) 
 3 {
 4     f[x]=a[x];//x節點必取
 5     for(unsigned i=0;i<e[x].size();++i)
 6     {
 7         int u=e[x][i];
 8         if(u==y) continue;//當u不爲父親節點
 9         dfs(u,x);//遞歸求解兒子節點的f值
10         if(f[u]>=0) f[x]+=f[u];//當兒子權值大於0,則加上
11     }
12 }
 1 //T29:最大子樹和(DP)
 2 //主程序
 3 cin>>n;
 4 for(int i=1;i<=n;++i) cin>>a[i];
 5 for(int i=1,x,y;i<n;++i)
 6 {
 7     cin>>x>>y;//樹邊
 8     e[x].push_back(y);//增長樹邊
 9     e[y].push_back(x);//增長樹邊
10 }
11 dfs(1);//遞歸求解f值
12 int ans=a[1];
13 for(int i=1;i<=n;++i) ans=max(ans,f[i]);//答案取最大的一個
14 cout<<ans<<endl;

經典例題:選課(Luogu 2014)

  在大學裏每一個學生,爲了達到必定的學分,必須從不少課程裏選擇一些課程來學習,在課程裏有些課程必須在某些課程以前學習,如高等數學老是在其它課程以前學習。如今有 N 門功課,每門課有個學分,每門課有一門或沒有直接先修課(若課程 a 是課程 b 的先修課即只有學完了課程a,才能學習課程 b)。一個學生要從這些課程裏選擇 M 門課程學習,問他能得到的最大學分是多少?
* 下圖所示:每一個節點表明一節課;父親表明他的先修課;紅顏色表明選上的課。左邊是一種合法的選課方式;而右邊則是一種不合法的選課方式,2 的先修課 1 沒有選上。

* 問題觀察:咱們能夠觀察獲得,根節點的課必定是要選的;而且選的節點是和根節點聯通的。 (不然不符合選課規則)
* 狀態設計:設 F[i][j] 爲,對於每節點 i,只考慮本身的子樹,一共選了 j 節課,所能獲得的最大權值是多少。假設i爲根,而且包括i(根節點必定要選)

* 狀態轉移:假設 i 只有兩棵子樹,那麼咱們能夠枚舉在其中一棵子樹中,咱們一共選了幾門課:
- F[i][j] = MAX (F[u][k] + F[v][j-k-1]) + A[i](意思是:我必定要選第i節課而且選了j節課的最優值A[i]+MAX(只考慮u這個節點而且u確定是要選的,選了k節課的最優值,只考慮v這個節點而且v確定是要選的,選了j-k-1節課的最優值))

* 狀態轉移:假設 i 有超過兩棵子樹,咱們可使用逐次合併的方法:先合併前兩棵子樹,而後將其視做一棵,再與餘下的子樹繼續合併。

 1 //T30:選課(DP)時間複雜度:nm^2 
 2 void dfs(int x)//處理根節點爲x的子樹
 3 {
 4     int *dp=f[x];//小技巧,用dp來代替f[x]數組
 5     for(unsigned i=0;i<e[x].size();++i)
 6     {
 7         int u=e[x][i];
 8         dfs(u);//處理兒子節點的子樹
 9         //合併操做
10         //將已經合併的子樹信息存放到dp數組當中
11         for(int j=0;j<=m;++j) tp[j]=0;//tp:臨時數組
12         for(int j=0;j<=m;++j)//從已經合併的選j門
13             for(int k=0;j+k<=m;++k)//重新加入的u子樹中選k門
14                 tp[j+k]=max(tp[j+k],dp[j]+f[u][k]);//一共選了j+k節課選一個最大的->已經選了j門,要在新的子樹中選k門 
15         for(int j=0;j<=m;++j) dp[j]=tp[j];//複製過來(這樣遍歷完就是從全部子樹中選j門課一共能夠獲得的學分最可能是多少) 
16     }
17     ...
18 } 
1 //T30:選課(DP)
2 void dfs(int x)//處理根節點爲x的子樹
3 {
4     ...
5     //必需要選根節點這一門
6     for(int j=m;j;--j) dp[j]=dp[j-1]+a[x];//新的第j門=在衆多子樹中選了j-1門+當前 7     dp[0]=0;
8 }
1 //T30:選課(DP)
2 //主程序
3 cin>>n>>m,++m;
4 for(int i=1,fa;i<=n;++i)
5 cin>>fa>>a[i],e[fa].push_back(i);
6 //咱們設全部沒有先修課的父親爲0號
7 //這樣結果是同樣的,並且必需要選0號課程
8 dfs(0);//根節點出發,遞歸
9 cout<<f[0][m]<<endl;//在根節點多取m門課

狀態壓縮DP:

* 狀態壓縮 DP利用位運算來記錄狀態,並實現動態規劃。
* 問題特色:數據規模較小;不能使用簡單的算法解決。

* 給定兩個布爾變量a,b,他們之間可進行布爾運算:
* 與運算 and:當 a,b 均爲真的時候, a b爲真;
* 或運算 or:當 a,b 均爲假的時候, a b爲假;
* 非運算 not:當 a 均爲真的時候, 非 a爲假;
* 異或運算 xor:當 a,b 不是同時真,或者同時假的時候, a 異或 b爲真。

* 咱們把 0 視做布爾的假,1 視做布爾的真,則整數亦存在二進制的運算,而運算的結果則是二進制裏對應的位做相應的布爾操做。(按位運算)
* (23)10 = (10111)2,(10)10 = (1010)2
* 與運算 and: 23 and 10 = (10)2 = 2
* 或運算 or: 23 or 10 = (11111)2 = 31
* 異或運算 xor: 23 xor 10 = (11101)2 = 29

* 另外,在整數位運算中還有位移操做:
* (23)10 = (10111)2,(10)10 = (1010)2
* 左移 «:將整個二進制向左移動若干位,並用 0 補充;
- 10 << 1 = (10100)2 = 20(左移n位=乘以2n
* 右移 »:將整個二進制向右移動若干位(實際最右邊的幾位就消失了);
- 23 >> 2 = (101)2 = 5(右移n位=除以2n

位運算比加減乘除都快!

經典例題:玉米田(Luogu 1879)

  農場主 John 新買了一塊長方形的新牧場,這塊牧場被劃分紅 M 行 N列 (1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一塊正方形的土地。 John打算在牧場上的某幾格裏種上美味的草,供他的奶牛們享用。
  遺憾的是,有些土地至關貧瘠,不能用來種草。而且,奶牛們喜歡獨佔一塊草地的感受,因而 John 不會選擇兩塊相鄰的土地,也就是說,沒有哪兩塊草地有公共邊。
  John 想知道,若是不考慮草地的總塊數,那麼,一共有多少種種植方案可供他選擇?(固然,把新牧場徹底荒廢也是一種方案)
- 1 1 1
- 0 1 0 (1 表明這塊土地適合種草)
- 總方案數: 9
* 問題限制:沒有哪兩塊草地有公共邊
* 考慮動態規劃:如何劃分狀態?
- 從上到下,從左到右?
- 記錄 F[i][j] 爲 (i,j) 選擇在這個位置種草的方案數?
- 問題:如何轉移?咱們只能限制左邊的位置不能種草;不能限制上面的位置不能種草,因此這樣不可行!
* 劃分狀態:考慮用行來劃分狀態; 第 i 行的狀態只取決於第 i-1 行。

* 狀態表示:如何表示一行的種草狀態?
* 二進制位:將一整行看做是一個大的二進制數,其上爲 1 表示種了草, 0表示沒有種草,最好不用二維數組,由於行列數目不定,很差開數組。
* 這樣,咱們能夠用一個數來表示一行的種草狀態
例如:下圖中,在一行的第一個位置和第四個位置種草,能夠用18來表示這種狀態:

* 狀態設計:設 F[i][j] 爲已經處理到第 i 行,且第 i 行的狀態是 j,的總方案數。 (其中 j 爲一個大的二進制數)
* 狀態轉移:枚舉前一行的種草狀態 k,若是沒有產生衝突,則加上:
F[i][j] = ∑ F[i − 1][k]
* 問題:如何判斷兩個狀態是否衝突? 如何運用二進制運算?

* 解決方案:採起與運算;若是兩個狀態是衝突的,則確定有一位上都爲1, and 的結果確定不爲 0;不然若結果爲 0,則表明狀態不衝突。

* 下一個問題:如何判斷當前狀態j是合法的?

1 沒有佔有障礙物:和判斷兩個狀態是否衝突是相似的
2 左右位置判斷:與上一行判斷衝突,咱們只考慮到了上下位置不可能同時種草,但尚未考慮左右的狀況。
- 解決方案: j & (j«1) = 0 且 j & (j»1) = 0(我只想說:左右兩個相互疊加判斷,真TM聰明!)

 1 //T31:玉米田(狀態壓縮DP)
 2 const int N=13;//邊長
 3 const int S=(1<<N)+5;//二進制狀態數=213,+5的目的是讓內心有個安慰~應該不會爆炸吧O(∩_∩)O,開大點數組這樣
 4 int a[N][N];//棋盤數組
 5 int s[N];//每一行空地的狀態
 6 int f[N][S];//動態規劃數組
 7 int g[S];//標記一個狀態是否合法
 8 //主過程
 9 ts=1<<m;//總狀態數
10 for(int i=0;i<ts;++i)//判斷左右位置
11     g[i]=((i&(i<<1))==0)&&((i&(i>>1))==0);//左右位置是否有衝突
 1 //T31:玉米田(狀態壓縮DP)
 2 f[0][0]=1;//初值!
 3 for(int i=1;i<=n;++i)//枚舉行
 4     for(int j=0;j<ts;++j)//枚舉這行的狀態
 5         if(g[j]&&((j&s[i])==j))//狀態自己合法且不佔障礙物
 6         {
 7             for(int k=0;k<ts;++k)//枚舉上一行的狀態
 8                 if((k&j)==0)//若是不衝突
 9                     f[i][j]=(f[i][j]+f[i-1][k])%MOD;
10         } 
11 int ans=0;
12 for(int j=0;j<ts;++j)
13     ans=(ans+f[n][j])%MOD;//答案加起來
1 //T31:玉米田(狀態壓縮DP)
2 cin>>n>>m;//讀入棋盤狀態
3 for(int i=1;i<=n;++i)
4     for(int j=1;j<=m;++j) 
5         cin>>a[i][j];
6 for(int i=1;i<=n;++i)//預處理每一行的狀態
7     for(int j=1;j<=m;++j)
8         s[i]=(s[i]<<1)+a[i][j];

經典例題:互不侵犯(Luogu 1896)

* 在 N×N 的棋盤裏面放 K 個國王,使他們互不攻擊,共有多少種擺放方案。國王能攻擊到它上下左右,以及左上左下右上右下八個方向上附近的各一個格子,共 8 個格子。
- 3 2 (在 3×3 的棋盤內放置 2 個國王)
- 總方案數: 16

* 狀態設計:設 F[i][j][k] 爲已經處理到第 i 行,且第 i 行的狀態是j,如今一共放置了 k 個國王的總方案數。 (其中 j 爲一個大的二進制數)
* 狀態轉移:枚舉前一行的放置國王狀態爲 l,若是沒有產生衝突,則加上(其中 x 爲狀態 j 的國王數)
F[i][j][k] = ∑ F[i − 1][l][k − x]
* 問題:如何判斷兩個狀態是否衝突? 如何運用二進制運算?

1 //T32:互不侵犯(狀態壓縮DP)
2 const int N=10;
3 const int S=(1<<N)+5;//二進制狀態數
4 int cnt[S];//記錄一個狀態有多少個1(按照這個狀態放國王,一共有多少個國王) 
5 int g[S];//記錄一個狀態是否合法
6 int h[S];
7 LL f[N][S][N*N];//動態轉移狀態->[行][狀態][一共擺了多少個國王]
1 //T32:互不侵犯(狀態壓縮DP)
2 ts=1<<n;
3 for(int i=0;i<ts;++i)
4 {
5     g[i]=((i&(i<<1))==0)&&((i&(i>>1))==0);
6     h[i]=i|(i<<1)|(i>>1);//h:一個狀態把它左右都佔據以後的狀態(h[i]表示第i個國王把他左右旁邊佔領以後,標記爲1)
7     cnt[i]=cnt[i>>1]+(i&1);//計算多少個1((i&1)表示最後一個是1就++) 
8 }
 1 //T32:互不侵犯(狀態壓縮DP)
 2 f[0][0][0]=1;//初值
 3 //順推
 4 for(int i=0;i<n;++i)//行數
 5     for(int j=0;j<ts;++j)//這一行狀態
 6         for(int l=0;l<=k;++l) if(f[i][j][l])//枚舉個數
 7             for(int p=0;p<ts;++p)//枚舉下一行
 8                 if(g[p]&&((h[p]&j)==0))//若是是合法狀態,且不衝突(p是一個合法狀態,p國王佔的領地和j是沒有衝突的)    
 9                 {
10                     f[i+1][p][l+cnt[p]]+=f[i][j][l];
11                 }         
12 LL ans=0;//答案
13 for(int j=0;j<ts;++j) ans+=f[n][j][k];//全部的狀態都能成爲答案 

經典例題:Little Pony and Harmony Chest(Codeforces 453B)

* 咱們稱一個正整數序列 B 是和諧的,當且僅當它們兩兩互質。
- 給出一個序列 A,求出一個和諧的序列 B,使得∑ |Ai − Bi| 最小。

* 問題思考:一個和諧的序列兩兩互質,知足什麼性質?
- 它們分解質因數後,沒有兩個數擁有相同的質因子。
* 狀態構思:若是我已經決定完前 i 個數是多少,如今要決定下一個數,須要知足什麼限制?——不能有和前面重複的質因子。
- 這表明咱們須要記錄下,目前已經用了哪些質因子了。
* 狀態設計:設 F[i][j] 爲,已經決定完前 i 個數是多少,並且已經用了狀態爲 的質因子了,如今的最小權值差是多少。
- 如何狀態壓縮? 2,3,5,7,11,...;一個表明一個二進制位;用了是 1,沒有是 0。

 1 //T35:CF453B(狀態壓縮DP)
 2 cin>>n;
 3 for(int i=1;i<=n;++i) cin>>a[i];
 4 for(int i=2;i<M;++i)//預處理質數(篩法) 
 5 {
 6     if(!fg[i]) p[++p[0]]=i;
 7     for(int j=1;j<=p[0]&&i*p[j]<M;++j)
 8     fg[i*p[j]]=1;
 9 }
10 for(int i=1;i<M;++i)//預處理每一個數的質因子所表明的狀態
11 {
12     g[i]=0;
13     for(int j=1;j<=p[0];++j)//若是i是這個質因子的倍數 
14     if(i%p[j]==0)
15     {
16         g[i]|=1<<(j-1);//由於是從1到j-1的,第一個質因子是1,第二個質因子是2,第三個4,第四個8…這樣就能獲得每個數它擁有哪些質因子的狀況 
17     } 
18 }
 1 //T35:CF453B(狀態壓縮DP)
 2 //動態規劃主過程
 3 int ns=1<<p[0];
 4 //初值:最大值(由於要求最小值) 
 5 for(int i=1;i<=n+1;++i)
 6     for(int j=0;j<ns;++j) f[i][j]=S;
 7 f[1][0]=0;
 8 for(int i=1;i<=n;++i)//枚舉位置
 9     for(int j=0;j<ns;++j) if(f[i][j]<S)//枚舉狀態
10         for(int k=1;k<M;++k)//枚舉這個位置的數
11             if((g[k]&j)==0)//g[k]:第k個數,擁有哪些質因子的狀態,若是以前沒有出現過(兩個狀態沒有衝突,沒有出現重複的質因子,執行) 
12             {
13                 //計算最優值
14                 int t=f[i][j]+absp(k-a[i]);
15                 if(t<f[i+1][g[k]|j])//更新最優值
16                 f[i+1][g[k]|j]=t,
17                 opt[i+1][g[k]|j]=k;//opt:取到最優值時,選最後一個數是什麼 18             }
 1 //T35:CF453B(狀態壓縮DP)
 2 //最優值輸出
 3 int ansp=S;
 4 int ansm=0;
 5 for(int j=0;j<ns;++j)//記錄最優值所對應的狀態
 6 if(f[n+1][j]<ansp) ansp=f[n+1][j],ansm=j;//ansp:最優值,ansm:最優狀態 
 7 for(int i=n+1;i>1;--i)//依次向前查詢,逆推 
 8 {
 9     b[i-1]=opt[i][ansm];
10     ansm^=g[b[i-1]];//至關於減去 
11 }
12 for(int i=1;i<=n;++i) cout<<b[i]<<" ";
13 cout<<endl;

經典例題:憤怒的小鳥(Luogu 2831)NOIP2016提升組

  Kiana最近沉迷於一款神奇的遊戲沒法自拔。

  簡單來講,這款遊戲是在一個平面上進行的。

  有一架彈弓位於(0,0)處,每次Kiana能夠用它向第一象限發射一隻紅色的小鳥,小鳥們的飛行軌跡均爲形如的曲線,其中a,b是Kiana指定的參數,且必須知足a<0。

  當小鳥落回地面(即x軸)時,它就會瞬間消失。

  在遊戲的某個關卡里,平面的第一象限中有n只綠色的小豬,其中第i只小豬所在的座標爲(xi,yi)。

  若是某隻小鳥的飛行軌跡通過了(xi,yi),那麼第i只小豬就會被消滅掉,同時小鳥將會沿着原先的軌跡繼續飛行;

  若是一隻小鳥的飛行軌跡沒有通過(xi,yi),那麼這隻小鳥飛行的全過程就不會對第i只小豬產生任何影響。

  例如,若兩隻小豬分別位於(1,3)和(3,3),Kiana能夠選擇發射一隻飛行軌跡爲的小鳥,這樣兩隻小豬就會被這隻小鳥一塊兒消滅。

  而這個遊戲的目的,就是經過發射小鳥消滅全部的小豬。

  這款神奇遊戲的每一個關卡對Kiana來講都很難,因此Kiana還輸入了一些神祕的指令,使得本身能更輕鬆地完成這個遊戲。這些指令將在【輸入格式】中詳述。

  假設這款遊戲一共有T個關卡,如今Kiana想知道,對於每個關卡,至少須要發射多少隻小鳥才能消滅全部的小豬。因爲她不會算,因此但願由你告訴她。

  【輸入】

  第一行包含一個正整數T,表示遊戲的關卡總數。

  下面依次輸入這T個關卡的信息。每一個關卡第一行包含兩個非負整數n,m,分別表示該關卡中的小豬數量和Kiana輸入的神祕指令類型。接下來的n行中,第i行包含兩個正實數(xi,yi),表示第i只小豬座標爲(xi,yi)。數據保證同一個關卡中不存在兩隻座標徹底相同的小豬。

若是m=0,表示Kiana輸入了一個沒有任何做用的指令。

若是m=1,則這個關卡將會知足:至多用只小鳥便可消滅全部小豬。

若是m=2,則這個關卡將會知足:必定存在一種最優解,其中有一隻小鳥消滅了至少只小豬。

保證1<=n<=18,0<=m<=2,0<xi,yi<10,輸入中的實數均保留到小數點後兩位。

上文中,符號分別表示對x向上取整和向下取整

  【輸出】

  對每一個關卡依次輸出一行答案。

  輸出的每一行包含一個正整數,表示相應的關卡中,消滅全部小豬最少須要的小鳥數量

* 狀態設計:能設計形如 F[i],表示前 i 只小豬都打下來的最小的小鳥數嗎?
- 沒法這樣作。我一次能夠打下若干只小豬,並且沒有順序之分
* 狀態設計:用一個大的二進制數 s 表示哪些小豬被打下來了(1 表示打下來了, 0 表示沒有打下來)。設 F[s] 爲打下狀態爲 s 的小豬, 最小的小鳥數。
* 狀態轉移:採用順推。考慮下一步咱們能夠打下狀態爲 t 的小豬,則能夠這樣更新:

F[s or t] = MIN (F[s or t], F[s] + 1)(F[s]表示用了多少小鳥,+1表示這個狀態多用了一隻小鳥),用F[s]+1的小鳥打下F[s or t]的小豬。
- s or t 表示我如今一共打下了 s 和 t 狀態的小豬。

 1 //T33:憤怒的小鳥(狀態壓縮DP)
 2 //DP主過程
 3 int ns=1<<n;
 4 for(int i=1;i<ns;i++) dp[i]=n;//初值,最多就n只小鳥 
 5 for(int i=0;i<ns;i++)
 6 {
 7     int l=(ns-1)^i;//異^或操做;選出沒有打下的小豬
 8     l=mk[l&(-l)];//l表示第一隻沒被打下的小豬
 9     for(int j=1;j<=g[l][0];j++)//選擇一種打的方式
10     {
11         dp[i|g[l][j]]=min(dp[i|g[l][j]],dp[i]+1);//g[l][j]爲可以打到的小豬的狀態
12     } 
13 }
14 printf("%d\n",dp[ns-1]);//答案

總結狀壓DP:

* 狀態壓縮 DP:用一個二進制數來記錄狀態
* 特色:記錄一些東西是否出現過
* 難點:位運算判斷;狀態的設計
* 標誌:數據範圍不大,某一個狀態特別小

最近發現一些網站盜用個人blog,這實在不能忍(™把關於個人名字什麼的所有刪去只保留文本啥意思。。)!!但願各位轉載引用時請註明出處,謝謝配合噢~

原博客惟一地址:http://www.cnblogs.com/geek-007/p/7237010.html

相關文章
相關標籤/搜索