動態規劃原理
動態規劃這玩意兒,就好比是在拓撲圖上玩跳格子游戲。在圖論中,咱們是從特定的節點跳到其他節點;而在動態規劃里呢,我們是從一個狀態 “嗖” 地轉移到另一個狀態。狀態一般用數組來表示,就像 f [i][j],它就是一種超重要的狀態,能從類似 f [i - 1][j] 這樣的狀態 “跑” 過來哦。
1-線性動態規劃用法
確定狀態:得根據問題的特點,像尋寶一樣找出合適的狀態表示,通常就是用數組來存這些狀態啦。
找出狀態轉移方程:這可是關鍵中的關鍵,要搞清楚狀態之間是怎么 “串門” 的,也就是當前狀態是咋從之前的狀態推導出來的。
處理邊界條件:得先確定好初始狀態的值,這就像游戲開始時要站好初始位置,這樣狀態轉移才能順順利利地開始。
計算結果:按照狀態轉移方程這個 “魔法公式”,一步步穩穩地算出最終結果。
算法模板
cpp
// 定義狀態數組const?int?N =?10010;int?f[N][N];?
// 輸入數據// ...
// 初始化邊界條件// ...
// 狀態轉移for?(int?i =?1;?i <=?n;?i++)?{
????for?(int?j =?1;?j <=?m;?j++)?{
????????f[i][j]?=?max(f[i -?1][j -?1],?f[i -?1][j])?+?...;?// 狀態轉移方程
????}}
// 計算結果int?ans =?0;for?(int?i =?1;?i <=?n;?i++)?{
????ans =?max(ans,?f[n][i]);}
cout <<?ans;
例題分析
數字三角形問題
這題簡直就像是在一個特殊的數字迷宮里找最長的寶藏路線。狀態 f [i][j] 表示從最高點一路 “探險” 到第 i 行第 j 列的最大路徑和。狀態轉移方程 f [i][j] = max (f [i - 1][j - 1], f [i - 1][j]) + g [i][j],這里的 g [i][j] 就是第 i 行第 j 列那個閃閃發光的數字啦。寶子們,沖呀,解開這個謎題不在話下 (??????)??!
代碼:
#include?<bits/stdc++.h>using?namespace?std;const?int?N =?10010;int?f[N][N],?g[N][N];int?main()?{
????int?n;
????scanf("%d",?&n);
????for?(int?i =?1;?i <=?n;?i++)?{
????????for?(int?j =?1;?j <=?i;?j++)?{
????????????scanf("%d",?&g[i][j]);
????????}
????}
????for?(int?i =?1;?i <=?n;?i++)?{
????????for?(int?j =?1;?j <=?i;?j++)?{
????????????f[i][j]?=?max(f[i -?1][j -?1],?f[i -?1][j])?+?g[i][j];
????????}
????}
????int?ans =?0;
????for?(int?i =?1;?i <=?n;?i++)?ans =?max(ans,?f[n][i]);
????cout <<?ans;}
最長上升子序列相關問題
合唱隊形問題
這題就像是在給一群同學排一個超酷炫的合唱隊形。要找出最少出列同學數量,讓剩下同學能排出那個特別的合唱隊形。咱們可以先從左到右求最長上升子序列 f1,再從右到左求最長上升子序列 f2,最后找出 f1 [i] + f2 [i] 的最大值,用總人數減去這個最大值再減 1 就是答案啦。加油,寶子們,這種題目對你們來說,肯定是小菜一碟 (??.??)!
代碼
#include<bits/stdc++.h>using?namespace?std;
const?int?N =?110;int?a[N];int?f1[N],?f2[N];int?main()?{
????int?n;
????scanf("%d",?&n);
????for?(int?i =?1;?i <=?n;?i++)?{
????????scanf("%d",?&a[i]);
????}
????for?(int?i =?1;?i <=?n;?i++)?{
????????for?(int?j =?1;?j <?i;?j++)?{
????????????if?(a[i]?>?a[j])
????????????????f1[i]?=?max(f1[i],?f1[j]?+?1);
????????}
????}
????for?(int?i =?n;?i;?i--)?{
????????for?(int?j =?n;?j >?i;?j--)?{
????????????if?(a[i]?>?a[j])
????????????????f2[i]?=?max(f2[i],?f2[j]?+?1);
????????}
????}
????int?ans =?0;
????for?(int?i =?1;?i <=?n;?i++)?{
????????ans =?max(ans,?f1[i]?+?f2[i]);
????}
????cout <<?n -?ans -?1;}
奶牛吃牧草問題
有一群可愛的奶牛要去吃牧草啦,這里有 N 個區間,每個區間 x, y 代表 y - x + 1 堆優質牧草,咱要幫奶牛選不重復區間,讓它們吃到最多的牧草。可以先按區間右端點排序,再用二分查找找出能和當前區間不重疊的最大區間編號,最后用動態規劃求解。寶子們,開動腦筋,幫奶牛們實現牧草自由 (??????)??!
cpp
#include?<bits/stdc++.h>using?namespace?std;const?int?N =?150010;
pair<int,?int>?a[N];int?f[N];int?n;
int?find(int?l,?int?r,?int?val)?{
????int?res =?0;
????while?(l <=?r)?{
????????int?mid =?(l +?r)?>>?1;
????????if?(a[mid].first <?val)?{
????????????res =?mid,?l =?mid +?1;
????????}?else?r =?mid -?1;
????}
????return?res;}int?main()?{
????scanf("%d",?&n);
????for?(int?i =?1;?i <=?n;?i++)?{
????????int?s,?ss;
????????scanf("%d%d",?&s,?&ss);
????????a[i]?=?{ss,?s};
????}
????sort(a +?1,?a +?1?+?n);
????for?(int?i =?1;?i <=?n;?i++)?{
????????int?h =?a[i].second,?t =?a[i].first;
????????f[i]?=?max(f[i -?1],?f[find(1,?i -?1,?h)]?+?t -?h +?1);
????}
????cout <<?f[n];}
2-背包問題
0 - 1 背包問題
原理:對于每個物品,只有選和不選兩種情況,目標是在背包容量限制下獲得最大利益。
狀態定義:f[i][j]?表示考慮前?i?個物品,背包剩余體積為?j?時能獲得的最大價值。
狀態轉移方程:f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]),其中?v[i]?是第?i?個物品的體積,w[i]?是其價值。
滾動數組優化原理:由于?f[i]?層狀態只依賴于?f[i - 1]?層,可使用一維數組?f[j]?替代二維數組。但要倒序枚舉體積?j,防止一個物品被重復選擇。優化后的狀態轉移方程為?f[j] = max(f[j], f[j - v[i]] + w[i])。
例題:采藥問題
#include?<bits/stdc++.h>using?namespace?std;int?n,v;const?int?N=110,M=1010;int?w[N],c[N],f[M];int?main(){
????cin>>v>>n;
????int?pre=0;
????for(int?i=0;i<n;i++){
????????cin>>c[i]>>w[i];
????}
????for(int?i=0;i<n;i++){
????????pre+=c[i];
????????for(int?j=min(pre,v);j>=c[i];j--)
????????????f[j]=max(f[j],f[j-c[i]]+w[i]);
????}
????int?ans=0;
????for(int?i=0;i<=v;i++)ans=max(ans,f[i]);
????cout<<ans;}
多維 0 - 1 背包問題(以二維為例)
原理:有兩個消耗維度(如金幣和精力),在考慮物品選擇時要同時滿足兩個維度的限制。
狀態定義:f[i][j][k]?表示到第?i?個物品時,花費了?j?個金幣,花費了?k?個精力所獲得的收益。
狀態轉移:與一維 0 - 1 背包類似,只是要同時考慮兩個維度的限制。
kkksc03 實現愿望問題
kkksc03 的時間和金錢是有限的,所以他很難滿足所有同學的愿望。所以他想知道在自己的能力范圍內,最多可以完成多少同學的愿望?
#include<bits/stdc++.h>using?namespace?std;const?int?N=210;int?f[N][N];int?n,v1,v2;int?main(){
????cin>>n>>v1>>v2;
????for(int?i=1;i<=n;i++){
????????int?c1,c2;
????????cin>>c1>>c2;
????????for(int?j=v1;j>=c1;j--)
????????????for(int?k=v2;k>=c2;k--)
????????????????f[j][k]=max(f[j][k],f[j-c1][k-c2]+1);
????}
????cout<<f[v1][v2];}
多重背包問題
原理:每個物品有?s[i]?個,可選擇 0 到?s[i]?個該物品。
方法一:樸素枚舉
for?(int?i =?1;?i <=?n;?i++)
????for(int?j =?0;?j <=?s[i];?j++)
????????for(int?k =?m;?k >=?j *?v[i];?k--)
????????????f[k]?=?max(f[k],?f[k -?j *?v[i]]?+?j *?w[i]);
方法二:二進制枚舉
將數量為?s[i]?的物品拆分成若干個不同的 “新物品”,數量分別為?1, 2, 4, ..., 2^k, r(其中?r = s[i] - (1 + 2 + 4 + ... + 2^k)?且?1 + 2 + 4 + ... + 2^k <= s[i]),通過這些 “新物品” 的不同組合可表示 0 到?s[i]?之間的任意數量,然后對這些 “新物品” 使用 0 - 1 背包方法求解。
代碼
#include?<bits/stdc++.h>using?namespace?std;
const?int?N =?2010;int?f[N];
int?main()?{
????int?n,?V;
????cin >>?n >>?V;
????for?(int?i =?0;?i <?n;?i++)?{
????????int?v,?w,?s;
????????cin >>?v >>?w >>?s;
????????for?(int?k =?1;?k <=?s;?k *=?2)?{
????????????for?(int?j =?V;?j >=?k *?v;?j--)?{
????????????????f[j]?=?max(f[j],?f[j -?k *?v]?+?k *?w);
????????????}
????????????s -=?k;
????????}
????????if?(s >?0)?{
????????????for?(int?j =?V;?j >=?s *?v;?j--)?{
????????????????f[j]?=?max(f[j],?f[j -?s *?v]?+?s *?w);
????????????}
????????}
????}
????cout <<?f[V]?<<?endl;
????return?0;}
方法三:單調隊列優化:
通過單調隊列對狀態轉移進行優化,減少不必要的計算,但實現相對復雜。
完全背包問題
原理:每個物品可以選無限個。
狀態轉移方程:f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]),優化到一維數組后,正序枚舉體積?j,因為每個物品可以選多次,f[j - v[i]]?可能已經是選了若干個第?i?個物品后的狀態。
代碼:
for(int?i =?1;?i <=?n;?i++)
????for(int?j =?v[i];?j <=?V;?j++)
????????f[j]?=?max(f[j],?f[j -?v[i]]?+?w[i]);
分組背包問題
原理:物品被分成不同的組,每組只能選 1 個物品。
狀態轉移:按組、體積、組內物品的順序進行枚舉。
偽代碼
for(組)
????for(體積)
????????for(組中第幾個)
????????????// 狀態轉移
拓展:每組選?s[i]?個物品的解決方法
可以將每個組內的物品進行組合,生成新的 “組合物品”,每個組合看作一個新物品,其體積和價值是組合內物品的體積和價值之和,然后對這些新的 “組合物品” 使用分組背包的方法求解。也可以結合 DFS 進行搜索。
有依賴的背包問題
原理:選擇某一個物品時需要先選擇其前驅物品。
解決方法:
轉換為 0 - 1 背包:把物品的不同選擇情況看作不同的物品,例如買了?a?才能買?b?和?c,買了?b?才能買?d,則子樹的所有情況?a, ab, ac, abc, abd, abcd?相當于轉換成了 6 個不同的物品。
樹形 DP:把物品之間的依賴關系看作一棵樹,根節點是主物品,子節點是依賴于主物品的物品,從葉子節點開始向上遞推,計算每個節點及其子樹的所有可能選擇情況。
開心的金明
金明今天很開心,家里購置的新房就要領鑰匙了,新房里有一間金明自己專用的很寬敞的房間。更讓他高興的是,媽媽昨天對他說:“你的房間需要購買哪些物品,怎么布置,你說了算,只要不超過 n 元錢就行”。今天一早,金明就開始做預算了,他把想買的物品分為兩類:主件與附件,附件是從屬于某個主件的,下表就是一些主件與附件的例子: 主件 附件 電腦 打印機,掃描儀 書柜 圖書 書桌 臺燈,文具 工作椅 無 如果要買歸類為附件的物品,必須先買該附件所屬的主件。每個主件可以有 0 個、1 個或 2 個附件。每個附件對應一個主件,附件不再有從屬于自己的附件。金明想買的東西很多,肯定會超過媽媽限定的 n 元。
于是,他把每件物品規定了一個重要度,分為 5 等:用整數 1~5 表示,第 5 等最重要。他還從因特網上查到了每件物品的價格(都是 10 元的整數倍)。他希望在不超過 n 元的前提下,使每件物品的價格與重要度的乘積的總和最大。
設第 j 件物品的價格為 v j ? ,重要度為 w j ? ,共選中了 k 件物品,編號依次為 j 1 ? ,j 2 ? ,…,j k ? ,則所求的總和為: v j 1 ? ? ×w j 1 ? ? +v j 2 ? ? ×w j 2 ? ? +?+v j k ? ? ×w j k ? ? 請你幫助金明設計一個滿足要求的購物單。
輸入格式 第一行有兩個整數,分別表示總錢數 n 和希望購買的物品個數 m。 第 2 到第 (m+1) 行,每行三個整數,第 (i+1) 行的整數 v i ? ,p i ? ,q i ? 分別表示第 i 件物品的價格、重要度以及它對應的的主件。如果 q i ? =0,表示該物品本身是主件。
cpp
#include?<iostream>#include?<cstring>using?namespace?std;
const?int?N =?60;??// 物品最大數量const?int?M =?32010;??// 錢數的上限
int?n,?m;??// n 表示總錢數,m 表示物品個數int?v[N],?w[N];??// v 存儲物品價格,w 存儲物品價格與重要度的乘積int?f[M];??// 記錄狀態
struct?Item?{
????int?v,?w;??// 主件的價格和價格與重要度的乘積
????int?v1,?w1;??// 附件 1 的價格和價格與重要度的乘積
????int?v2,?w2;??// 附件 2 的價格和價格與重要度的乘積};
Item items[N];
int?main()?{
????cin >>?n >>?m;
????n /=?10;??// 因為價格都是 10 元的整數倍,這里統一處理為 10 元為單位
????for?(int?i =?1;?i <=?m;?i++)?{
????????int?p,?q;
????????cin >>?v[i]?>>?p >>?q;
????????v[i]?/=?10;??// 統一為 10 元為單位
????????w[i]?=?v[i]?*?p;
????????if?(q ==?0)?{??// 是主件
????????????items[i].v =?v[i];
????????????items[i].w =?w[i];
????????}?else?{??// 是附件
????????????if?(items[q].v1 ==?0)?{??// 第一個附件
????????????????items[q].v1 =?v[i];
????????????????items[q].w1 =?w[i];
????????????}?else?{??// 第二個附件
????????????????items[q].v2 =?v[i];
????????????????items[q].w2 =?w[i];
????????????}
????????}
????}
????for?(int?i =?1;?i <=?m;?i++)?{
????????if?(items[i].v >?0)?{??// 是主件
????????????for?(int?j =?n;?j >=?items[i].v;?j--)?{
????????????????// 不選附件的情況
????????????????f[j]?=?max(f[j],?f[j -?items[i].v]?+?items[i].w);
????????????????// 選附件 1 的情況
????????????????if?(j >=?items[i].v +?items[i].v1)?{
????????????????????f[j]?=?max(f[j],?f[j -?items[i].v -?items[i].v1]?+?items[i].w +?items[i].w1);
????????????????}
????????????????// 選附件 2 的情況
????????????????if?(j >=?items[i].v +?items[i].v2)?{
???????????????????f[j]?=?max(f[j],?f[j -?items[i].v -?items[i].v2]?+?items[i].w +?items[i].w2);
????????????????}
????????????????// 選附件 1 和附件 2 的情況
????????????????if?(j >=?items[i].v +?items[i].v1 +?items[i].v2)?{
???????????????????f[j]?=?max(f[j],?f[j -?items[i].v -?items[i].v1 -?items[i].v2]?+?items[i].w +?items[i].w1 +?items[i].w2);
???????????????}
???????????}
???????}
????}
????cout <<?f[n]?*?10?<<?endl;??// 還原為原來的價格單位
????return?0;}
背包拓展問題
背包計數問題
原理:統計滿足某個條件的數量。
狀態轉移方程:通常為?f[j] += f[j - v]。
例題:奶牛組隊問題
#include<bits/stdc++.h>using?namespace?std;const?int?N=2010;const?int?mod=1e8;int?f[N][N];int?a[N];int?n,m;int?op(int?x){
????return?((x%m)+m)%m;}int?main(){
????cin>>n>>m;
????for(int?i=1;i<=n;i++){
????????scanf("%d",&a[i]);
????????a[i]=a[i]%m;
????????f[i][a[i]]=1;
????}
????for(int?i=1;i<=n;i++){
????????for(int?j=0;j<m;j++)
????????????f[i][j]=((f[i][j]+f[i-1][j])%mod+f[i-1][op(j-a[i])])%mod;
????}
????cout<<f[n][0];}
背包路徑問題
原理:要求輸出具體選擇了哪些物品,使用一個額外的數組記錄最優值的轉移來源。
#include?<bits/stdc++.h>using?namespace?std;
const?int?N =?1010;int?v[N],?w[N];int?f[N][N];bool?g[N][N];??// 記錄每個狀態是從哪里轉移來的
int?main()?{
????int?n,?m;
????cin >>?n >>?m;
????for?(int?i =?1;?i <=?n;?i++)?cin >>?v[i]?>>?w[i];
????for?(int?i =?1;?i <=?n;?i++)?{
????????for?(int?j =?0;?j <=?m;?j++)?{
????????????f[i][j]?=?f[i -?1][j];
????????????if?(j >=?v[i]?&&?f[i -?1][j -?v[i]]?+?w[i]?>?f[i][j])?{
????????????????f[i][j]?=?f[i -?1][j -?v[i]]?+?w[i];
????????????????g[i][j]?=?true;??// 表示選擇了第 i 個物品
????????????}
????????}
????}
????// 輸出最大價值
????cout <<?f[n][m]?<<?endl;
????// 回溯找出選擇的物品
????vector<int>?path;
????for?(int?i =?n,?j =?m;?i;?i--)?{
????????if?(g[i][j])?{
????????????path.push_back(i);
????????????j -=?v[i];
????????}
????}
????// 輸出選擇的物品編號
????for?(int?i =?path.size()?-?1;?i >=?0;?i--)?{
????????cout <<?path[i]?<<?" ";
????}
????cout <<?endl;
????return?0;}
這些背包問題雖然各有特點,但核心都是通過合理定義狀態和狀態轉移方程來解決。多做練習,能熟練掌握噠!(??.??)