0x50「動態規劃」例題
幾點總結
1 DP三要素:狀態,階段,決策。具體地講,若DP時是雙重循環,第一重循環通常表示狀態和階段,第二重循環表示決策。若DP時是三重循環,第一重循環通常表示階段,第二重循環和第一重循環共同組成狀態,第三重循環表示決策。三者必須按照由外而內依次循環。
2 DP三條件:子問題重疊,最優子結構性質,無后效性。對于前兩點,和遞歸的條件和性質類似,決定了DP實現的兩種寫法:迭代和遞歸。而無后效性則表示DP已經求解過的子問題不受后續影響,換言之,DP對狀態空間的遍歷是要求狀態空間是DAG,DAG中的節點對應狀態,邊對應狀態轉移的過程,轉移的選取(走哪條邊)就是DP中的決策,遍歷結果是DAG的一個拓撲序。(PS:若出現后效性,通常會通過一次強制鏈接和一次強制斷開來解決,在某些環形DP/基環樹問題里常見)
線性DP
線性DP指具有線性“階段”劃分的動態規劃算法,若狀態空間有多個維度,每個維度上都有線性變化的“階段”,那么它就是線性DP。
經典問題
LIS
狀態表示:F[i]表示以A[i]為結尾的LIS長度
轉移方程:,其中
且
邊界:
目標:,其中
時間復雜度:
LIS變形問題
Q:把序列A變成單調不下降的序列,至少需要修改幾個數?
A:序列A的總長度-A的最長不下降子序列長度
e.g. A={3,2,1,4,2},A的最長不下降子序列{3,4},只要把A改成{3,3,3,4,4}即可(共修改5-2=3個數)
Q:把序列A變成嚴格單調遞增的序列,至少需要修改幾個數?
A:因為保證嚴格單調遞增,所以不能用上面的構造方法。設B[i]=A[i]-i,答案就是序列A的總長度-B的最長不下降子序列長度
e.g. A={2,2,4,3,4},則B={1,0,1,0,-1},B的最長不下降子序列{0,1},只要把B改成{-INF,0,1,INF,INF}即可,此時A={-INF+1,1,3,INF+4,INF+5}(共修改5-2=3個數)若模仿上面一題的計算方法,用A序列的總長度-A序列的LIS長度,得到答案為5-3=2錯誤,原因在于無法通過只改變A序列中的第三個元素4做到左右平衡。
LCS
狀態表示:F[i,j]表示以A[1...i]和B[1...j]的LCS長度
轉移方程:
邊界:
目標:
時間復雜度:
數字三角形
狀態表示:F[i][j]表示從左上角走到第i行第j列的最大和
轉移方程:
邊界:
目標:,其中
時間復雜度:
例題
POJ2279 Mr Young's Picture Permutations
我們將所有人按照從高到低的順序放入整個序列中,這樣就有了DP的順序。
狀態表示:F[a1,a2,a3,a4,a5]表示各排從左端起分別站了a1,a2,a3,a4,a5人的方案數量
轉移方程:
3~5排同理
邊界:
目標:
時間復雜度:
/*
*/
#define method_1
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=30+1;
const int maxk=5+5;
const int INF=0x3f3f3f3f;
ll f[maxn][maxn][maxn][maxn][maxn],k,num[maxk];
int main() {
ios::sync_with_stdio(false);
// freopen("Mr Young's Picture Permutations.in","r",stdin);
while(cin>>k&&k) {
memset(num,0,sizeof(num));
//memset(f,0,sizeof(f));
f[0][0][0][0][0]=1;
for(int i=1; i<=k; i++) {
cin>>num[i];
}
for(ll a1=1; a1<=num[1]; a1++) {
for(ll a2=0; a2<=min(num[2],a1); a2++) {
for(ll a3=0; a3<=min(num[3],a2); a3++) {
for(ll a4=0; a4<=min(num[4],a3); a4++) {
for(ll a5=0; a5<=min(num[5],a4); a5++) {
ll &p=f[a1][a2][a3][a4][a5];
p=0;
if(a1-1>=a2) p+=f[a1-1][a2][a3][a4][a5];
if(a2-1>=a3) p+=f[a1][a2-1][a3][a4][a5];
if(a3-1>=a4) p+=f[a1][a2][a3-1][a4][a5];
if(a4-1>=a5) p+=f[a1][a2][a3][a4-1][a5];
if(a5-1>=0) p+=f[a1][a2][a3][a4][a5-1];
}
}
}
}
}
cout<<f[num[1]][num[2]][num[3]][num[4]][num[5]]<<endl;
}
return 0;
}
#endif
#ifdef method_2
/*
*/
#endif
#ifdef method_3
/*
*/
#endif
5101 LCIS
狀態表示:F[i,j]表示以A[1...i]和B[1...j]構成的以B[j]結尾的LCIS長度
轉移方程:
即
邊界:
目標:
樸素實現這個轉移方程,復雜度為。
但是我們自己觀察這個方程,會發現當第二層循環變量j時,我們可以將第一層循環變量i視為定值,這讓B[k]<A[i]這個條件是固定的。這意味著當j+1時,k的取值范圍從[0,j)變成了[0,j+1),也就是說,這時決策集合的下界不變,上界最多增加一個可能的最優決策,那么我們只要O()判斷這個B[j]<A[i]是否滿足即可確定新的j能否有可能產生最優決策。
具體實現上,我們在每次進入第一層循環時,維護一個變量val表示決策集合中F[i-1,k]的最大值,在每次j的范圍增大時,判斷j能否進入決策集合即可。這樣的操作避免了重復掃描,時間復雜度O(),代碼如下,其中method_1為樸素算法,method_2為優化算法。
/*
*/
#define method_2
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=3000+5;
const int INF=0x3f3f3f3f;
int n,f[maxn][maxn],a[maxn],b[maxn],ans=0;
int main() {
ios::sync_with_stdio(false);
//freopen("LCIS.in","r",stdin);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
cin>>b[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(a[i]==b[j]){
for(int k=0;k<j;k++){
if(b[k]<a[i]){
f[i][j]=max(f[i-1][k]+1,f[i][j]);
}
}
}
else f[i][j]=f[i-1][j];
if(i==n) ans=max(ans,f[n][j]);
}
}
cout<<ans;
return 0;
}
#endif
#ifdef method_2
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=3000+5;
const int INF=0x3f3f3f3f;
int n,f[maxn][maxn],a[maxn],b[maxn],ans=0;
int main() {
ios::sync_with_stdio(false);
// freopen("LCIS.in","r",stdin);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
cin>>b[i];
}
for(int i=1;i<=n;i++){
int val=0;
if(b[0]<a[i]) val=max(val,f[i-1][0]);
for(int j=1;j<=n;j++){
if(a[i]==b[j]){
f[i][j]=val+1;
}
else f[i][j]=f[i-1][j];
if(b[j]<a[i]) val=max(val,f[i-1][j]);
if(i==n) ans=max(ans,f[n][j]);
}
}
cout<<ans;
return 0;
}
#endif
#ifdef method_3
/*
*/
#endif
5102 Mobile Service
狀態表示:F[i,x,y,z]表示完成了前i個請求,三個服務員分別在x,y,z位置的最小花費
轉移方程:
題目要求同一位置不能有多個員工,所以要用if語句判斷轉移合法性
邊界:
目標:
按照這個方程,時間復雜度
但是仔細觀察會發現,當完成前i個請求時,必然有一個員工位于位置,因此只需要知道i和兩名員工的位置,就可以推出第三個員工的位置,即消去了冗余狀態
故更改定義如下
狀態表示:F[i,x,y]表示完成了前i個請求,兩個服務員分別在x,y位置的最小花費(第三個員工在位置)
轉移方程:
題目要求同一位置不能有多個員工,所以要用if語句判斷轉移合法性
邊界:
目標:
按照這個方程,時間復雜度:,代碼如下
/*
*/
#define method_1
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=1000+5;
const int maxl=200+5;
const int INF=0x3f3f3f3f;
int l,n,f[maxn][maxl][maxl],c[maxl][maxn],p[maxn];
int main() {
ios::sync_with_stdio(false);
// freopen("Mobile Service.in","r",stdin);
cin>>l>>n;
for(int i=1; i<=l; i++) {
for(int j=1; j<=l; j++) {
cin>>c[i][j];
}
}
for(int i=1; i<=n; i++) cin>>p[i];
memset(f,INF,sizeof(f));
f[0][1][2]=0;
p[0]=3;
int ans=INF;
for(int i=1; i<=n; i++) {
for(int x=1; x<=l; x++) {
for(int y=1; y<=l; y++) {
if(x==y) continue;
if(f[i-1][x][y]==INF) continue;
int z=p[i-1];
if(x!=p[i]&&y!=p[i])f[i][x][y]=min(f[i-1][x][y]+c[z][p[i]],f[i][x][y]);
if(z!=p[i]&&y!=p[i])f[i][z][y]=min(f[i-1][x][y]+c[x][p[i]],f[i][z][y]);
if(x!=p[i]&&z!=p[i])f[i][x][z]=min(f[i-1][x][y]+c[y][p[i]],f[i][x][z]);
// if(i==n) ans=min(ans,f[n][x][y]);
//不能在這里更新 因為f[n][x][y]可能尚未達到最優解,因為后面兩個維度不一定保證遞增
}
}
}
for(int x=1; x<=l; x++)
for(int y=1; y<=l; y++)
ans=min(ans,f[n][x][y]);
cout<<ans;
return 0;
}
#endif
#ifdef method_2
/*
*/
#endif
#ifdef method_3
/*
*/
#endif
POJ3666 Making the Grade
引理:記答案為S,則在滿足S最小的前提下,一定存在一種構造B序列的方案,使得B中的數值都在A中出現過。
狀態表示:F[i]表示已經構造好前i個數,以B[i]為結尾的序列的最小S值,即此時B[i]=A[i]
轉移方程:,其中
且
cost(j+1,i-1)表示構造B[j+1]...B[i-1]滿足A[j]B[j+1]
...
B[i-1]
A[i]時,
的最小值
方程解釋:DP方程中的i就是最新的一段數中與A相同的位置,j是上一段數中這樣的位置,因此cost(j+1,i-1)就是把區間中前一部分變成A[j]后一部分變成A[i]的最小代價。
邊界:
目標:
時間復雜度:,其中計算cost的時間復雜度:
考慮優化cost的計算過程,我們做如下的狀態定義
狀態表示:F[i,j]表示已經構造好前i個數,以B[i]=j為結尾的序列的最小S值,即此時B[i]=A[i]=j
轉移方程:,其中
邊界:,其中
目標:,其中
時間復雜度:
空間復雜度:
考慮優化時間和空間復雜度
1 離散化A序列
時間復雜度:
空間復雜度:
2 類比之前LCIS的轉移方程,這里變量k的變化,即決策集合的變化仍滿足隨著j的增大只增不減的性質,所以做出同樣的優化。
時間復雜度:
空間復雜度:
代碼如下,其中method_1僅僅進行了離散化,結果為TLE。method_2進行了上述兩種優化,結果為AC。
/*
*/
#define method_2
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=2000+5;
const int INF=0x3f3f3f3f;
int n,a[maxn],b[maxn],f[maxn][maxn],cnt;
int main() {
ios::sync_with_stdio(false);
freopen("Making the Grade.in","r",stdin);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i];
}
sort(b+1,b+n+1);
cnt=unique(b+1,b+n+1)-b-1;
/*
for(int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+cnt+1,a[i])-b;
}
*/
int ans=INF;
memset(f,INF,sizeof(f));
for(int i=1;i<=cnt;i++) f[0][i]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=cnt;j++){
for(int k=1;k<=j;k++){
f[i][j]=min(f[i-1][k]+abs(a[i]-b[j]),f[i][j]);
}
if(i==n) ans=min(ans,f[n][j]);
}
}
cout<<ans;
return 0;
}
#endif
#ifdef method_2
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=2000+5;
const int INF=0x3f3f3f3f;
int n,a[maxn],b[maxn],f[maxn][maxn],cnt;
int main() {
ios::sync_with_stdio(false);
// freopen("Making the Grade.in","r",stdin);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i];
}
sort(b+1,b+n+1);
cnt=unique(b+1,b+n+1)-b-1;
/*
for(int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+cnt+1,a[i])-b;
}
*/
int ans=INF;
memset(f,INF,sizeof(f));
for(int i=0;i<=cnt;i++) f[0][i]=0;
for(int i=1;i<=n;i++){
int val=f[i-1][0];
for(int j=1;j<=cnt;j++){
val=min(val,f[i-1][j]);
f[i][j]=val+abs(a[i]-b[j]);
if(i==n) ans=min(ans,f[n][j]);
}
}
cout<<ans;
return 0;
}
#endif
#ifdef method_3
/*
*/
#endif
5103 傳紙條
從左上角走到右下角一個來回 等價于 兩個人同時從左上角出發走到右下角
狀態表示:F[x1,y1,x2,y2]表示第一個人在(x1,y1)第二個人在(x2,y2)的最大收益
此時時間復雜度:
考慮優化
觀察后發現,設目前路徑長度為i,則x1+y1=x2+y2=i+2(因為兩人同時行走)
所以通過i,x1,x2就可以確定y1,y2
修改狀態表示如下
狀態表示:F[i,x1,x2]表示第一個人在x1行,第二個人在x2行,目前路徑長度為i的最大收益(此時y1=i+2-x1,y2=i+2-x2)
轉移方程:每個人有兩種移動方式,兩個人移動狀態共有四種,這里以兩人均向右為例:
即兩人從(x1,y1)(x2,y2)走向(x1,y1+1)(x2,y2+1)
若x1=x2且y1+1=y2+1,說明兩人路徑在此處相交,答案累加一次:
否則分別累加答案:
邊界:F[0,1,1]=A[1,1]
目標:F[n+m-2,n,n]
時間復雜度:O()
代碼如下
/*
*/
#define method_1
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=50+5;
const int INF=0x3f3f3f3f;
int a[maxn][maxn],f[maxn*2][maxn][maxn],n,m;
int main() {
ios::sync_with_stdio(false);
// freopen("傳紙條.in","r",stdin);
cin>>n>>m;
for(int i=1; i<=n; i++) {
for(int j=1; j<=m; j++) {
cin>>a[i][j];
}
}
f[0][1][1]=a[1][1];
for(int i=1; i<=n+m-2; i++) {
for(int j=1; j<=min(n,i+1); j++) {
for(int k=1; k<=min(n,i+1); k++) {
int Y1=i+2-j;
int Y2=i+2-k;
if((Y1>=1&&Y1<=m)&&(Y2>=1&&Y2<=m)) {
if(j==k&&Y1==Y2) {
f[i][j][k]=max(f[i][j][k],f[i-1][j][k]+a[j][Y1]);
f[i][j][k]=max(f[i][j][k],f[i-1][j-1][k-1]+a[j][Y1]);
f[i][j][k]=max(f[i][j][k],f[i-1][j-1][k]+a[j][Y1]);
f[i][j][k]=max(f[i][j][k],f[i-1][j][k-1]+a[j][Y1]);
} else {
f[i][j][k]=max(f[i][j][k],f[i-1][j][k]+a[j][Y1]+a[k][Y2]);
f[i][j][k]=max(f[i][j][k],f[i-1][j-1][k-1]+a[j][Y1]+a[k][Y2]);
f[i][j][k]=max(f[i][j][k],f[i-1][j-1][k]+a[j][Y1]+a[k][Y2]);
f[i][j][k]=max(f[i][j][k],f[i-1][j][k-1]+a[j][Y1]+a[k][Y2]);
}
}
}
}
}
cout<<f[n+m-2][n][n];
return 0;
}
#endif
#ifdef method_2
/*
*/
#endif
#ifdef method_3
/*
*/
#endif
5104 I-country
考慮凸連通塊性質:任何一個凸連通塊可以劃分成若干行,每行的左端點列號先減小后增大,右端點列號先增大后減小。
狀態表示:F[i,j,l,r,x,y]表示前i行,選了j個格子,其中第i行選了第l到r的格子,左輪廓的單調性是x,右輪廓的單調性是y(0表示遞增,1表示遞減)時,能構成的凸連通塊的最大權值和
轉移方程:其中A為每行的前綴和數組,即
1.左邊界列號遞減,右邊界列號遞增(兩邊界都處于擴張狀態)
2.左右邊界列號都遞減(左邊界擴張,右邊界收縮)
3.左右邊界列號都遞減(左邊界收縮,右邊界擴張)
4.左邊界列號遞增,右邊界列號遞減(兩邊界都處于收縮狀態)
邊界:
目標:
時間復雜度:
本題還需要輸出方案,對于DP問題輸出方案,通常是使用一個/多個與F數組同樣大小的數組來記錄每一次轉移的最優解的取值,然后在DP結束后,從終態向初態遞歸,依次在回溯時輸出答案。
代碼如下,其中method_1沒有輸出方案,method_2輸出方案
/*
*/
#define method_2
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=15+2;
const int maxk=225+5;
const int INF=0x3f3f3f3f;
int n,m,k,a[maxn][maxn],s[maxn][maxn],f[maxn][maxk][maxn][maxn][2][2];
/*
f[i][j][l][r][x][y] 表示前i行 占據了j個格子 第i行從l列到r列 左端點狀態為x 右端點狀態為y 的最大權值
x=0 表示列號遞增 x=1 表示列號遞減
*/
int main() {
ios::sync_with_stdio(false);
freopen("I-country.in","r",stdin);
cin>>n>>m>>k;
for(int i=1; i<=n; i++) {
for(int j=1; j<=m; j++) {
cin>>a[i][j];
s[i][j]=s[i][j-1]+a[i][j];
}
}
// cout<<s[2][3]<<endl;
for(int i=1; i<=n; i++) {
for(int j=0; j<=k; j++) {
for(int l=1; l<=m; l++) {
for(int r=l; r<=m; r++) {
if(j-(r-l+1)<0) break;
for(int p=l; p<=r; p++) {
for(int q=p; q<=r; q++) {
f[i][j][l][r][1][0]=max(f[i][j][l][r][1][0],f[i-1][j-(r-l+1)][p][q][1][0]+s[i][r]-s[i][l-1]);
}
}
for(int p=l; p<=r; p++) {
for(int q=r; q<=m; q++) {
f[i][j][l][r][1][1]=max(f[i][j][l][r][1][1],f[i-1][j-(r-l+1)][p][q][1][0]+s[i][r]-s[i][l-1]);
f[i][j][l][r][1][1]=max(f[i][j][l][r][1][1],f[i-1][j-(r-l+1)][p][q][1][1]+s[i][r]-s[i][l-1]);
}
}
for(int p=1; p<=l; p++) {
for(int q=l; q<=r; q++) {
f[i][j][l][r][0][0]=max(f[i][j][l][r][0][0],f[i-1][j-(r-l+1)][p][q][1][0]+s[i][r]-s[i][l-1]);
f[i][j][l][r][0][0]=max(f[i][j][l][r][0][0],f[i-1][j-(r-l+1)][p][q][0][0]+s[i][r]-s[i][l-1]);
}
}
for(int p=1; p<=l; p++) {
for(int q=r; q<=m; q++) {
f[i][j][l][r][0][1]=max(f[i][j][l][r][0][1],f[i-1][j-(r-l+1)][p][q][0][1]+s[i][r]-s[i][l-1]);
f[i][j][l][r][0][1]=max(f[i][j][l][r][0][1],f[i-1][j-(r-l+1)][p][q][1][0]+s[i][r]-s[i][l-1]);
f[i][j][l][r][0][1]=max(f[i][j][l][r][0][1],f[i-1][j-(r-l+1)][p][q][0][0]+s[i][r]-s[i][l-1]);
f[i][j][l][r][0][1]=max(f[i][j][l][r][0][1],f[i-1][j-(r-l+1)][p][q][1][1]+s[i][r]-s[i][l-1]);
}
}
}
}
}
}
int ans=-INF;
for(int i=1; i<=n; i++) {
for(int l=1; l<=m; l++) {
for(int r=l; r<=m; r++) {
for(int x=0; x<=1; x++) {
for(int y=0; y<=1; y++) {
ans=max(ans,f[i][k][l][r][x][y]);
}
}
}
}
}
cout<<"Oil : "<<ans<<endl;
return 0;
}
#endif
#ifdef method_2
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=15+2;
const int maxk=225+5;
const int INF=0x3f3f3f3f;
int n,m,k,a[maxn][maxn],s[maxn][maxn],f[maxn][maxk][maxn][maxn][2][2];
/*
f[i][j][l][r][x][y] 表示前i行 占據了j個格子 第i行從l列到r列 左端點狀態為x 右端點狀態為y 的最大權值
x=0 表示列號遞增 x=1 表示列號遞減
*/
struct method{
int l,r,x,y;
}met[maxn][maxk][maxn][maxn][2][2];
int ai,aj,al,ar,ax,ay;
void update(int now,int l,int r,int x,int y){
int &ans=f[ai][aj][al][ar][ax][ay];
method &p=met[ai][aj][al][ar][ax][ay];
if(ans>=now) return;
ans=now;
p.l=l;
p.r=r;
p.x=x;
p.y=y;
}
void print(int i,int j,int l,int r,int x,int y){
if(j==0) return;
method &p=met[i][j][l][r][x][y];
print(i-1,j-(r-l+1),p.l,p.r,p.x,p.y);
for(int ii=l;ii<=r;ii++){
cout<<i<<" "<<ii<<endl;
}
}
int main() {
ios::sync_with_stdio(false);
// freopen("I-country.in","r",stdin);
cin>>n>>m>>k;
for(int i=1; i<=n; i++) {
for(int j=1; j<=m; j++) {
cin>>a[i][j];
s[i][j]=s[i][j-1]+a[i][j];
}
}
// cout<<s[2][3]<<endl;
for(int i=1; i<=n; i++) {
for(int j=0; j<=k; j++) {
for(int l=1; l<=m; l++) {
for(int r=l; r<=m; r++) {
if(j-(r-l+1)<0) break;
ai=i;
aj=j;
al=l;
ar=r;
ax=1;
ay=0;
for(int p=l; p<=r; p++) {
for(int q=p; q<=r; q++) {
update(f[i-1][j-(r-l+1)][p][q][1][0]+s[i][r]-s[i][l-1],p,q,1,0);
}
}
ax=1;
ay=1;
for(int p=l; p<=r; p++) {
for(int q=r; q<=m; q++) {
update(f[i-1][j-(r-l+1)][p][q][1][0]+s[i][r]-s[i][l-1],p,q,1,0);
update(f[i-1][j-(r-l+1)][p][q][1][1]+s[i][r]-s[i][l-1],p,q,1,1);
}
}
ax=0;
ay=0;
for(int p=1; p<=l; p++) {
for(int q=l; q<=r; q++) {
update(f[i-1][j-(r-l+1)][p][q][1][0]+s[i][r]-s[i][l-1],p,q,1,0);
update(f[i-1][j-(r-l+1)][p][q][0][0]+s[i][r]-s[i][l-1],p,q,0,0);
}
}
ax=0;
ay=1;
for(int p=1; p<=l; p++) {
for(int q=r; q<=m; q++) {
update(f[i-1][j-(r-l+1)][p][q][1][0]+s[i][r]-s[i][l-1],p,q,1,0);
update(f[i-1][j-(r-l+1)][p][q][1][1]+s[i][r]-s[i][l-1],p,q,1,1);
update(f[i-1][j-(r-l+1)][p][q][0][0]+s[i][r]-s[i][l-1],p,q,0,0);
update(f[i-1][j-(r-l+1)][p][q][0][1]+s[i][r]-s[i][l-1],p,q,0,1);
}
}
}
}
}
}
int ans=-INF;
for(int i=1; i<=n; i++) {
for(int l=1; l<=m; l++) {
for(int r=l; r<=m; r++) {
for(int x=0; x<=1; x++) {
for(int y=0; y<=1; y++) {
if(f[i][k][l][r][x][y]>ans){
ans=f[i][k][l][r][x][y];
ai=i;
al=l;
ar=r;
ax=x;
ay=y;
}
}
}
}
}
}
cout<<"Oil : "<<ans<<endl;
print(ai,k,al,ar,ax,ay);
return 0;
}
#endif
#ifdef method_3
/*
*/
#endif
5105 Cookies
引理:貪婪度大的孩子餅干多。
證明:將所有孩子按照貪婪度降序排序,獲得的餅干數單調遞減。若交換兩個相鄰的孩子g[i]和g[i+1](鄰項交換,滿足g[i]>g[i+1]),則原來兩個孩子產生的怨氣是g[i]×(i-1)+g[i+1]×i,交換后產生的怨氣是g[i+1]×(i-1)+g[i]×i,即會對答案產生g[i]-g[i+1]的貢獻,而其他孩子的怨氣不變,所以經過反證法,交換后的結果顯然不優于原單調序列。
根據引理,我們將所有孩子的貪婪度遞減排序,從左向右DP。
至此,我們已經通過額外的算法確定了DP順序,現在考慮如何表示狀態。
狀態表示:F[i,j]表示給前i個孩子分配j塊餅干的最小怨氣和
我們做出如下轉化
1 若第i個孩子餅干數為1,則枚舉i前面有多少孩子的餅干數也為1(這些孩子不會對第i個孩子產生貢獻)
2 若第i個孩子的餅干數大于1,則等價于分配j-i塊餅干給前i個孩子,每人少拿一塊餅干,餅干相對數量不變,故答案不變。
狀態表示:F[i]表示以A[i]為結尾的LIS長度
轉移方程:
PS:解釋下轉化1的轉移方程,枚舉k表示[k+1,i]的孩子也是一塊餅干,那么[k+1,i]的孩子總共獲得了i-k塊餅干,剩余餅干數就是j-(i-k)。與此同時,前k個孩子獲得的餅干數大于一塊,所以后面的[k+1,i]個孩子會因此產生怨氣,怨氣值為
邊界:
目標:
時間復雜度:
這題輸出方案的方式和上一題類似,故不再贅述
代碼如下 ,其中method_1沒有輸出方案,method_2輸出方案
/*
*/
#define method_2
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=30+5;
const int maxm=5000+5;
const int INF=0x3f3f3f3f;
int n,m,g[maxn],f[maxn][maxm],s[maxn];
bool cmp(int x,int y){
return x>y;
}
int main() {
ios::sync_with_stdio(false);
freopen("Cookies.in","r",stdin);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>g[i];
sort(g+1,g+n+1,cmp);
for(int i=1;i<=n;i++) s[i]=s[i-1]+g[i];
memset(f,INF,sizeof(f));
f[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=i;j<=m;j++){
f[i][j]=min(f[i][j],f[i][j-i]);
for(int k=0;k<i;k++){
f[i][j]=min(f[i][j],f[k][j-(i-k)]+k*(s[i]-s[k]));
}
}
}
cout<<f[n][m];
return 0;
}
#endif
#ifdef method_2
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=30+5;
const int maxm=5000+5;
const int INF=0x3f3f3f3f;
int n,m,g[maxn],f[maxn][maxm],s[maxn],c[maxn],a[maxn][maxm],b[maxn][maxm],ans[maxn];
bool cmp(int x,int y){
return g[x]>g[y];
}
void print(int n,int m){
if(n==0) return;
print(a[n][m],b[n][m]);
if(a[n][m]==n){
for(int i=1;i<=n;i++) ans[c[i]]++;
}
else{
for(int i=a[n][m]+1;i<=n;i++) ans[c[i]]=1;
}
}
int main() {
ios::sync_with_stdio(false);
// freopen("Cookies.in","r",stdin);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>g[i],c[i]=i;
sort(c+1,c+n+1,cmp);
for(int i=1;i<=n;i++) s[i]=s[i-1]+g[c[i]];
memset(f,INF,sizeof(f));
f[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=i;j<=m;j++){
f[i][j]=f[i][j-i];
a[i][j]=i;
b[i][j]=j-i;
for(int k=0;k<i;k++){
if(f[i][j]>f[k][j-(i-k)]+k*(s[i]-s[k])){
f[i][j]=f[k][j-(i-k)]+k*(s[i]-s[k]);
a[i][j]=k;
b[i][j]=j-(i-k);
}
}
}
}
cout<<f[n][m]<<endl;
print(n,m);
for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
return 0;
}
#endif
#ifdef method_3
/*
*/
#endif
背包DP
背包問題是線性DP中的一類特殊模型。
經典問題
01背包
狀態表示:F[i,j]表示從前i個物品中選出總體積為j的物品放入背包
轉移方程:
邊界:
目標:,其中
時間復雜度:
空間復雜度:
代碼如下
memset(f, 0xcf, sizeof(f)); // -INF
f[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++)
f[i][j] = f[i - 1][j];
for (int j = v[i]; j <= m; j++)
f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
考慮優化空間復雜度
因為每一階段的i的狀態之和i-1的狀態有關,所以可以用滾動數組優化
時間復雜度:
空間復雜度:
代碼如下
int f[2][MAX_M+1];
memset(f, 0xcf, sizeof(f)); // -INF
f[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++)
f[i & 1][j] = f[(i - 1) & 1][j];
for (int j = v[i]; j <= m; j++)
f[i & 1][j] = max(f[i & 1][j], f[(i - 1) & 1][j - v[i]] + w[i]);
}
int ans = 0;
for (int j = 0; j <= m; j++)
ans = max(ans, f[n & 1][j]);
考慮繼續優化
實現轉移方程時,每次都會進行一次從F[i-1][]到F[i][]的拷貝,這提示我們直接省略F數組的第一維
時間復雜度:
空間復雜度:
代碼如下
int f[MAX_M+1];
memset(f, 0xcf, sizeof(f)); // -INF
f[0] = 0;
for (int i = 1; i <= n; i++)
for (int j = m; j >= v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
int ans = 0;
for (int j = 0; j <= m; j++)
ans = max(ans, f[j]);
對于為什么j為倒序循環的解釋:
F數組的后半部分F[j...m]處于第i個階段,也就是已經考慮過放入第i個物品的情況。
F數組的前半部分F[0...j-1]處于第i-1個階段,也就是尚未考慮過放入第i個物品的情況。
隨著j的不斷減小,我們不斷用第i-1個階段的狀態F[j - v[i]]來更新F[j],就保證了第i個物品最多可能被放入背包一次。
如果j正序循環,可能導致如下情況,F[j]被F[j-v[i]]+w[i]更新,接下來j增大到j+v[i]時,F[j+v[i]]又被F[j]+w[i]更新,即兩個同時處于第i個階段的狀態產生了轉移,相當于第i個物品被使用了兩次。
完全背包
狀態表示:F[i,j]表示從前i個物品中選出總體積為j的物品放入背包
轉移方程:
邊界:
目標:,其中
時間復雜度:
根據上面對于01背包循環的推論,只要將原先程序中的j的循環順序改為正序,就對應了每個物品可以使用無限次的情況。
代碼如下
int f[MAX_M+1];
memset(f, 0xcf, sizeof(f)); // -INF
f[0] = 0;
for (int i = 1; i <= n; i++)
for (int j = v[i]; j <= m; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
int ans = 0;
for (int j = 0; j <= m; j++)
ans = max(ans, f[j]);
多重背包
將每種物品拆成C[i]個,類比01背包考慮,狀態轉移方程略。
代碼如下
unsigned int f[MAX_M+1];
memset(f, 0xcf, sizeof(f)); // -INF
f[0] = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= c[i]; j++)
for (int k = m; k >= v[i]; k--)
f[k] = max(f[k], f[k - v[i]] + w[i]);
int ans = 0;
for (int i = 0; i <= m; i++)
ans = max(ans, f[i]);
時間復雜度:O()
考慮優化時間復雜度
優化方法1 對于每種物品進行二進制拆分,將每種物品拆成O(log)個。
優化方法2 單調隊列(詳見單調隊列優化DP)
分組背包
問題描述:n組物品,第i組里有個物品,每個物品有各自的體積和價值,現在每組物品中最多選擇一個物品,在總體積不超過m的情況下,令價值和盡量大。
狀態表示:F[i,j]表示從前i組物品中選出總體積為j的物品放入背包
轉移方程:
邊界:
目標:,其中
時間復雜度:
考慮優化空間復雜度
和01背包一樣,由于每組只選最多一個物品,所以省略掉F數組的第一維
注意:
1 需令第二重倒序循環.
2 每一組內的c[i]個物品的循環k要放在循環j的內層,因為i是階段,i和j共同構成狀態,k是決策,即第i組內用哪個物品,三者順序不能錯亂。
3 分組背包是很多樹形DP的基本模型。
代碼如下
memset(f, 0xcf, sizeof(f));
f[0] = 0;
for (int i = 1; i <= n; i++)
for (int j = m; j >= 0; j--)
for (int k = 1; k <= c[i]; k++)
if (j >= v[i][k])
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
例題
5201 數字組合
01背包
狀態表示:F[j]表示當外層循環到第i個數時,和為j的方案數
轉移方程:
邊界:
目標:
時間復雜度:
空間復雜度:
代碼如下
/*
*/
#define method_1
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=100+5;
const int maxm=10000+5;
const int INF=0x3f3f3f3f;
int f[maxm],n,m,a[maxn];
int main() {
ios::sync_with_stdio(false);
// freopen("數字組合.in","r",stdin);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
f[0]=1;
for(int i=1;i<=n;i++)
for(int j=m;j>=a[i];j--) f[j]+=f[j-a[i]];
cout<<f[m];
return 0;
}
#endif
#ifdef method_2
/*
*/
#endif
#ifdef method_3
/*
*/
#endif
5202 自然數拆分Lunatic版
完全背包
狀態表示:F[j]表示當外層循環到第i個數時,和為j的方案數
轉移方程:
邊界:
目標:
時間復雜度:
空間復雜度:
代碼如下
/*
*/
#define method_1
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=4000+5;
const ll mod=2147483648;
const int INF=0x3f3f3f3f;
ll f[maxn],n;
int main() {
ios::sync_with_stdio(false);
// freopen("自然數拆分Lunatic版.in","r",stdin);
f[0]=1;
cin>>n;
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++) f[j]=(f[j]+f[j-i])%mod;
if(f[n]!=0) cout<<f[n]-1;
else cout<<mod-1;
return 0;
}
#endif
#ifdef method_2
/*
*/
#endif
#ifdef method_3
/*
*/
#endif
POJ1015 Jury Compromise
01背包
狀態表示:F[j,k]表示當外層循環到i時,選出了j個人,辯方總分和控方總分差為k時,雙方總分的最大值。
轉移方程:
邊界:
目標:,滿足|k|盡量小,|k|相同時,F[m,k]盡量大
時間復雜度:
注意:
1 j一維要倒序循環,滿足每個候選人只能選擇一次。
2 path[i,j,k]表示F[j,k]取得最優值時的候選人方案,輸出方案時,從F[j,k]不斷向F[j-1,k-(a[D[j,k]]-b[D[j,k]])]遞歸,一直到達F[0,0],遞歸過程中的所有path[i,j,k]就是答案。注意雖然背包DP中的F數組可以省略第一維,但是path數組為了保存方案,必須要加上第一維。
3 F[j-1,k-(a[i]-b[i])]中k-(a[i]-b[i])可能出現負數,導致數組下標越界,所以需要統一進行數組下標平移,詳見代碼中的zero常量。
代碼如下
/*
*/
#define method_1
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=200+5;
const int maxm1=20+5;
const int maxm=900+5;
const int zero=450;
const int INF=0x3f3f3f3f;
int n,m,a[maxn],b[maxn],f[maxm1][maxm],path[maxn][maxm1][maxm],kase=0,suma,sumb,c[maxm1];
void find(int x,int y,int z) {
if(!y) return;
int temp=path[x][y][z+zero];
suma+=a[temp];
sumb+=b[temp];
c[y]=temp;
find(temp-1,y-1,z-(a[temp]-b[temp]));
}
int main() {
ios::sync_with_stdio(false);
// freopen("Jury Compromise.in","r",stdin);
while(cin>>n>>m&&n&&m) {
memset(f,0xcf,sizeof(f));
memset(path,0,sizeof(path));
int mx=m*20;
f[0][zero]=0;
for(int i=1; i<=n; i++) cin>>a[i]>>b[i];
for(int i=1; i<=n; i++) {
for(int j=m; j>=1; j--) {
for(int k=-mx; k<=mx; k++) {
path[i][j][k+zero]=path[i-1][j][k+zero];
if((f[j-1][k+zero-(a[i]-b[i])]>=0) && (k-(a[i]-b[i])>=-mx) && (k-(a[i]-b[i])<=mx)) {
if(f[j-1][k+zero-(a[i]-b[i])]+a[i]+b[i]>f[j][k+zero]) {
f[j][k+zero]=f[j-1][k+zero-(a[i]-b[i])]+a[i]+b[i];
path[i][j][k+zero]=i;
}
}
}
}
}
int ans=mx;
for(int i=0; i<=mx; ++i) {
if(f[m][i+zero]>=0&&f[m][i+zero]>=f[m][-i+zero]) {
ans=i;
break;
}
if(f[m][-i+zero]>=0) {
ans=-i;
break;
}
}
// cout<<ans;
suma=sumb=0;
find(n,m,ans);
sort(c+1,c+m+1);
cout<<"Jury #"<<++kase<<endl;
cout<<"Best jury has value "<<suma<<" for prosecution and value "<<sumb<<" for defence: "<<endl;
for(int i=1; i<=m; i++) cout<<" "<<c[i];
cout<<endl;
}
return 0;
}
#endif
#ifdef method_2
/*
*/
#endif
#ifdef method_3
/*
*/
#endif
POJ1742 Coins
多重背包
狀態表示:F[j]表示當外層循環到i時,前i種硬幣能否拼成j
轉移方程:
邊界:
目標:
時間復雜度:
樸素算法代碼如下
bool f[100010];
memset(f, 0, sizeof(f));
f[0] = true;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= c[i]; j++)
for (int k = m; k >= a[i]; k--)
f[k] |= f[k - a[i]];
int ans = 0;
for (int i = 1; i <= m; i++)
ans += f[i];
考慮優化
優化方法1 二進制拆分或單調隊列
優化方法2 注意到這里僅關注“可行性”,不關注“最優性”,因此仔細分析DP過程,可以發現,若前i種硬幣能夠拼成面值j,只有兩種可能:
1 前i-1種硬幣能夠拼成面值j,第i重循環開始前,F[j]=1
2 使用了第i種硬幣,因為F[j-a[i]]=1,所以推出F[j]=1
于是考慮貪心:設used[j]表示F[j]在階段i時為1至少需要用多少枚第i種硬幣,并且盡量選擇第一種情況。具體地說,若F[j-a[i]]=1時,F[j]已經為1,則不進行DP,否則進行轉移F[j]|=F[j-a[i]],同時令used[j]=used[j-a[i]]+1
時間復雜度:
代碼如下
/*
*/
#define method_1
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=100+5;
const int maxm=100000+5;
const int INF=0x3f3f3f3f;
int n,m,c[maxn],f[maxm],used[maxm],v[maxn],ans;
int main() {
ios::sync_with_stdio(false);
// freopen("Coins.in","r",stdin);
while(cin>>n>>m&&n&&m){
for(int i=1;i<=n;i++) cin>>v[i];
for(int i=1;i<=n;i++) cin>>c[i];
memset(f,0,sizeof(f));
ans=0;
f[0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++) used[j]=0;
for(int j=v[i];j<=m;j++){
//貪心策略 //多重背包
if(!f[j]&&f[j-v[i]]&&used[j-v[i]]<c[i]){
f[j]=1;
used[j]=used[j-v[i]]+1;
}
}
}
for(int i=1;i<=m;i++) if(f[i]) ans++;
cout<<ans<<endl;
}
return 0;
}
#endif
#ifdef method_2
/*
*/
#endif
#ifdef method_3
/*
*/
#endif