Dynamic programming
動態規劃
基礎觀念
把一個問題分成很多子問題
解決子問題
最後再由子問題的答案 得到原本問題的答案
把一個問題分成很多子問題
解決子問題
最後再由子問題的答案 得到原本問題的答案
跟分治有點像
如何從子問題的答案得到原本問題的答案?
如何從子問題的答案得到原本問題的答案?
首先要先依據題目,設計適合的狀態
如何從子問題的答案得到原本問題的答案?
首先要先依據題目,設計適合的狀態
以及狀態與狀態間的關係
如何從子問題的答案得到原本問題的答案?
首先要先依據題目,設計適合的狀態
以及狀態與狀態間的關係
A
B
+10
ex:
再藉由我們推得的狀態間關係
(稱為轉移方程)
依適合的順序一一算出不同狀態的答案
再藉由我們推得的狀態間關係
(稱為轉移方程)
依適合的順序一一算出不同狀態的答案
最後解決原本的問題
爬樓梯
有一隻青蛙一開始在地上
要跳到第n個階梯
他一次可以跳一階or兩階(往上)
請問他有幾種跳到n階的方法
首先要給這個問題設定狀態
首先要給這個問題設定狀態
定義 dp[i] = 青蛙跳到第 i 階的方法數
首先要給這個問題設定狀態
定義 dp[i] = 青蛙跳到第 i 階的方法數
因此答案就等於dp[n]
我們要找出dp[n]的數值
接著尋找狀態與狀態的關係
如果青蛙要跳到第i階,那他必須要先跳到 i-1 or i-2
因此 dp[i] = dp[i-1]+dp[i-2]
我們知道轉移方程了 !
dp[i] = dp[i-1] + dp[i-2]
依據轉移方程
可以看出適合的方向是從小到大算
我們知道轉移方程了 !
dp[i] = dp[i-1] + dp[i-2]
依據轉移方程
可以看出適合的方向是從小到大算
因為這樣當我在算 dp[i] 時
需要的dp[i-1] 、 dp[i-2]都已經算好了
最後就是我們要先找出轉移的起頭
不然只有轉移式是沒有用的
最後就是我們要先找出轉移的起頭
不然只有轉移式是沒有用的
Text
因此我們可以先手動算出較簡單的子問題(狀態)
最後就是我們要先找出轉移的起頭
不然只有轉移式是沒有用的
Text
因此我們可以先手動算出較簡單的子問題(狀態)
dp[1] = 1
dp[2] = 2
接著就可以快樂的一路用轉移方程求出答案了 !
別忘了答案就是dp[n]
#include<bits/stdc++.h>
using namespace std;
#define maxn 100005
int dp[maxn],n;
main(){
cin>>n;
dp[1] = 1;
dp[2] = 2;
for(int i=3;i<=n;++i){
dp[i] = dp[i-1] + dp[i-2];
}
cout<<dp[n]<<endl;
}有沒有發現其實這個問題就是費事數列 !
Dp 的複雜度?
Dp 的複雜度?
狀態數量 X 每次轉移
Dp 的複雜度?
狀態數量 X 每次轉移
剛剛的題目為例
狀態數量 : O(n)
每次轉移 : O(1)
O(n) * O(1) = O(n)
各種例題
有 NNN 顆石頭,每顆石頭的高度為 hih_ihi
青蛙從第 1 顆石頭出發,要跳到第 NNN 顆石頭
如果青蛙目前在第 iii 顆石頭,可以跳到第 i+1i+1i+1 或第 i+2i+2i+2 顆石頭
跳到某顆石頭會產生花費,花費為 ∣hi−hj∣|h_i - h_j|∣hi−hj∣
請計算青蛙到達第 NNN 顆石頭的最小總花費
有 NNN 顆石頭,每顆石頭的高度為 hih_ihi
青蛙從第 1 顆石頭出發,要跳到第 NNN 顆石頭
如果青蛙目前在第 iii 顆石頭,可以跳到第 i+1i+1i+1 或第 i+2i+2i+2 顆石頭
跳到某顆石頭會產生花費,花費為 ∣hi−hj∣|h_i - h_j|∣hi−hj∣
請計算青蛙到達第 NNN 顆石頭的最小總花費
ex :
4
10 30 40 20
as : 30
code
#include<bits/stdc++.h>
using namespace std;
int n,h[100005],dp[100005];
main(){
cin>>n;
for(int i=1;i<=n;++i) cin>>h[i];
dp[1] = 0;
dp[2] = abs(h[1]-h[2]);
for(int i=3;i<=n;++i){
dp[i] = min(dp[i-1]+abs(h[i]-h[i-1]),dp[i-2]+abs(h[i]-h[i-2]));
}
cout<<dp[n]<<endl;
}code
#include<bits/stdc++.h>
using namespace std;
#define maxn 100005
#define inf 1e18
#define int long long
int n,k,h[maxn],dp[maxn];
main(){
cin>>n>>k;
for(int i=1;i<=n;++i) cin>>h[i];
for(int i=1;i<=n;++i) dp[i] = inf;
dp[1] = 0;
for(int i=2;i<=n;++i){
for(int j=1;j<=k;++j){
if(i-j <= 0) continue;
dp[i] = min(dp[i],dp[i-j]+abs(h[i-j]-h[i]));
}
}
cout<<dp[n]<<endl;
}code
#include<bits/stdc++.h>
using namespace std;
#define maxn 100005
#define inf 1e18
#define int long long
int n,a[maxn],b[maxn],c[maxn],dp[maxn][3];
main(){
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i]>>b[i]>>c[i];
dp[1][0] = a[1];
dp[1][1] = b[1];
dp[1][2] = c[1];
for(int i=2;i<=n;++i){
dp[i][0] = a[i] + max(dp[i-1][1],dp[i-1][2]);
dp[i][1] = b[i] + max(dp[i-1][0],dp[i-1][2]);
dp[i][2] = c[i] + max(dp[i-1][1],dp[i-1][0]);
}
cout<<max({dp[n][0],dp[n][1],dp[n][2]})<<endl;
}
最大連續和 ( O(n) )
code
#include<bits/stdc++.h>
using namespace std;
#define maxn 100005
#define inf 1e18
#define int long long
int n,a[maxn],dp[maxn];
main(){
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i];
dp[1] = a[1];
int as = dp[1];
for(int i=2;i<=n;++i){
dp[i] = max(a[i],dp[i-1]+a[i]);
as = max(as,dp[i]);
}
cout<<as<<endl;
}各種背包問題
什麼是背包問題?
有很多物品,依據題目規則,可能有不同的數值,例如重量、價值,目的就是要將物品放入一個有重量限制的背包,最後讓背包中裝的物品價值最大
什麼是背包問題?
有很多物品,依據題目規則,可能有不同的數值,例如重量、價值,目的就是要將物品放入一個有重量限制的背包,最後讓背包中裝的物品價值最大
背包問題有非常多變形,
對經典的 0-1背包、無限背包、有限背包
回朔解等
0-1背包
有n個物品,每個物品重量為mi,價值為vi,
背包重量限制為w
求在符合重量限制時,最大價值是多少
0-1背包
有n個物品,每個物品重量為mi,價值為vi,
背包重量限制為w
求在符合重量限制時,最大價值是多少
ex:
三個物品,背包線種為8
三種物品 m 、 v分別如下
1 : 3 30
2 : 4 50
3 : 5 60
0-1背包
有n個物品,每個物品重量為mi,價值為vi,
背包重量限制為w
求在符合重量限制時,最大價值是多少
ex:
三個物品,背包線種為8
三種物品 m 、 v分別如下
1 : 3 30
2 : 4 50
3 : 5 60
ans : 90
設定狀態
在這個問題,前面的一維狀態已經無法表示此問題的狀態
需要加入更多的訊息
設定狀態
在這個問題,前面的一維狀態已經無法表示此問題的狀態
需要加入更多的訊息
定義
dp[i][j] = 考慮前 i 個物品,背包重量限制為 j 的最大價值
轉移方程
求dp[i][j]可以有兩種情況
轉移方程
求dp[i][j]可以有兩種情況
case 1 : 我沒拿第 i 個物品到背包中 -> dp[i][j] = dp[i-1][j]
轉移方程
求dp[i][j]可以有兩種情況
case 1 : 我沒拿第 i 個物品到背包中 -> dp[i][j] = dp[i-1][j]
case 2 : 我拿了第 i 個物品到背包中 -> dp[i][j] = dp[i-1][j-mi]+vi
轉移方程
求dp[i][j]可以有兩種情況
case 1 : 我沒拿第 i 個物品到背包中 -> dp[i][j] = dp[i-1][j]
case 2 : 我拿了第 i 個物品到背包中 -> dp[i][j] = dp[i-1][j-mi]+vi
我要選擇價值大的case:
dp[i][j] = max( dp[i-1][j] , dp[i-1][j-mi]+vi )
轉移順序
dp[i][j] = max( dp[i-1][j] , dp[i-1][j-mi]+vi )
觀察到計算dp[i][j]時,需要用到的是
dp[i-1]的結果
因此轉移順序為
for i = 1~n :
for j = 1~w :
(轉移)
using namespace std;
#define int long long
#define inf 1e18
#define maxn 1000006
int n,w,dp[105][100005];
pair<int,int> p[105];
main(){
cin>>n>>w;
for(int i=1;i<=n;++i){
cin>>p[i].first>>p[i].second;
}
for(int i=1;i<=n;++i){
for(int j=1;j<=w;++j){
if(j-p[i].first >= 0){
dp[i][j] = max(dp[i-1][j],dp[i-1][j-p[i].first]+p[i].second);
}else{
dp[i][j] = dp[i-1][j];
}
}
}
cout<<dp[n][w];
}目前空間複雜度是 O(nw)
還可以用更少嗎?
1 : 滾動dp
對於一些只取到 i-1 資訊的轉移,i-2以前的資訊不會再被用到
1 : 滾動dp
對於一些只取到 i-1 資訊的轉移,i-2以前的資訊不會再被用到
dp[i][j] = max(dp[i-1][j],dp[i-1][j-mi]+vi)
1 : 滾動dp
對於一些只取到 i-1 資訊的轉移,i-2以前的資訊不會再被用到
dp[i][j] = max(dp[i-1][j],dp[i-1][j-mi]+vi)
觀察到轉移式中 dp[i] ,都只有用到 dp[i-1] 的資訊
因此我可以只記錄 dp[0][j] 、 dp[1][j]
交替使用
滾動dp 0-1背包
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define inf 1e18
#define maxn 1000006
int n,w,dp[2][100005];
pair<int,int> p[105];
main(){
cin>>n>>w;
for(int i=1;i<=n;++i){
cin>>p[i].first>>p[i].second;
}
for(int i=1;i<=n;++i){
for(int j=1;j<=w;++j){
if(j-p[i].first >= 0){
dp[i%2][j] = max(dp[(i+1)%2][j],dp[(i+1)%2][j-p[i].first]+p[i].second);
}else{
dp[i%2][j] = dp[(i+1)%2][j];
}
}
}
cout<<dp[n%2][w];
}2 : 依照轉移順序壓狀態
2 : 依照轉移順序壓狀態
dp[i][j] = max(dp[i-1][j],dp[i-1][j-mi]+vi)
每次計算dp[i][j]
只會用到 dp[i-1][k] (k<j)
dp[j] = max(dp[j],dp[j-mi]+vi)
因此可以變成一維
dp[j] : 重量限制為 j 考慮前 i 項物品的答案
但是沒有狀態 i
這裡的dp[j]、dp[j-mi]
是上一次的dp[j] 、dp[j-mi] (考慮 1~i-1項物品的)
因為dp[i]、dp[i-1]現在是共用同一個陣列
因此枚舉 j (重量限制) 的方向要相反
才可以確保每次取到的資訊都是dp[i-1]的
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define inf 1e18
#define maxn 1000006
int n,w,dp[100005];
pair<int,int> p[105];
main(){
cin>>n>>w;
for(int i=1;i<=n;++i){
cin>>p[i].first>>p[i].second;
}
for(int i=1;i<=n;++i){
for(int j=w;j>=p[i].first;--j){
dp[j] = max(dp[j],dp[j-p[i].first]+p[i].second);
}
}
cout<<dp[w];
}無限背包
有n種物品,每種物品可以拿任意個,每個物品重量為mi,價值為vi,
背包重量限制為w
求在符合重量限制時,最大價值是多少
ex:
三個物品,背包線種為8
三種物品 m 、 v分別如下
1 : 3 30
2 : 4 50
3 : 5 60
as : 100
設定狀態
定義
dp[i][j] = 考慮前 i 個物品,背包重量限制為 j 的最大價值
(狀態與0-1背包一樣)
轉移
求dp[i][j]可以有兩種情況
case 1 : 我沒拿第 i 個物品到背包中 -> dp[i][j] = dp[i-1][j]
case 2 : 我再拿了一個第 i 個物品到背包中 ->
dp[i][j] = dp[i][j-mi]+vi
綜合兩種情況 dp[i][j] = max(dp[i-1][j],dp[i][j-mi]+vi)
其實整題的解法與0-1背包就只有差別在
我是拿了第i個物品
還是我再多拿一個第i個物品
與0-1背包比較
其實整題的解法與0-1背包就只有差別在
我是拿了第i個物品
還是我再多拿一個第i個物品
與0-1背包比較
轉移式也只差了 i-1 -> i
dp[i][j] = max(dp[i-1][j],dp[i][j-mi]+vi)
dp[i][j] = max(dp[i-1][j],dp[i-1][j-mi]+vi)
好好思考兩者的差異
與轉移式修改的意義
LCS
有兩個字串,長度分別為n,m,
要求兩個字串的最長共同子序列
的長度,並印出任何一個LCS
axyb
abyxb
ans : 長度 : 3, axb
有兩個字串,長度分別為n,m,以下稱S, T
要求S、T的最長共同子序列
的長度,並印出任何一個LCS
axyb
abyxb
ans : 長度 : 3, axb
先解決第一個問題
LCS多長 ?
定義狀態
定義 dp[i][j] =
考慮 S 的前 i 個字元
考慮 T 的前 j 個字元
的LCS長度
轉移方程 dp[i][j] = ?
考慮兩種情況 :
case1 : S[i] == T[i] :
dp[i][j] = dp[i-1][j-1] + 1
case2 : S[i] != T[i] :
dp[i][j] = max(dp[i-1][j] , dp[i][j-1])
最後答案即為
dp[n][m]
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define maxn 3005
int n,m,dp[maxn][maxn];
string S,T;
main(){
cin>>S>>T;
n = S.size();
m = T.size();
S = ' '+S;
T = ' '+T;
for(int i=1;i<=n;++i){
for(int j=1;j<=m;++j){
if(S[i] == T[j]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
cout<<dp[n][m]<<endl;
}
接下來要來找LCS實際長怎樣
我們紀錄每個dp[i][j]是從哪裡轉移過來的
若 S[i] == T[j] : 從 dp[i-1][j-1]
若 S[i] != T[j] 且 dp[i-1][j] > dp[i][j-1] : 從 dp[i-1][j]
若 S[i] != T[j] 且 dp[i-1][j] < dp[i][j-1] : 從 dp[i][j-1]
記錄好轉移來源後,我可以從dp[n][m]
一路依照轉移來源往回走
每次遇到轉移來源為 dp[i-1][j-1]
就代表我將 S[i] 與 T[j] 配對再一起
記錄好轉移來源後,我可以從dp[n][m]
一路依照轉移來源往回走
每次遇到轉移來源為 dp[i-1][j-1]
就代表我將 S[i] 與 T[j] 配對再一起
因此把答案加入 S[i] or T[j]
最後將答案反轉再印出就可以了
(因為找答案時是從後面往前找)
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define maxn 3005
string s,t,as;
int ls,lt,dp[maxn][maxn],d[maxn][maxn];
main(){
cin>>s>>t;
ls = s.size(), lt = t.size();
s = ' '+s, t = ' '+t;
for(int i=1;i<=ls;++i){
for(int j=1;j<=lt;++j){
if(s[i] == t[j]) dp[i][j] = dp[i-1][j-1] + 1, d[i][j] = 1;
else dp[i][j] = max(dp[i-1][j],dp[i][j-1]), d[i][j] = (dp[i-1][j] > dp[i][j-1])?2:3;
}
}
int x = ls,y = lt;
while(x >= 1 && y >= 1){
if(d[x][y] == 1) as += s[x], x--, y--;
else if(d[x][y] == 2) x--;
else y--;
}
reverse(as.begin(),as.end());
cout<<as<<endl;
}扣