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


現在是晚上十二點半,好累(無奈臉),接着給各位——也是給自己,更新筆記吧~

序列型狀態划分:

經典例題:乘積最大(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 部分所能得到的最大乘積。
* 很顯然這時候我們只需要通過枚舉最后一部分的數字有多大,就能得到結果乘積了。 (滿足最優性)
* 環形序列:第一個位置不一定是開頭,有可能位於序列的中間。
* 解決方法:枚舉每一個位置,把它當作開頭算一遍,得到的結果取最大值即為答案。
當出現循環節時候,我們就可以用“復制一遍序列”的方法來做,這樣就可以實現頭尾相接,如下圖:

對於每一條鏈,像上一題那樣做就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)

  在 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 兩顆珠子聚合后所釋放的能量。則第 4、 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM