動態規劃
用表格紀錄programming
dp:用變動的表格來求解
比較像一種思考模式、手段
而不是演算法
跟分治有點像
將問題分成子問題
接著合併、解決子問題
最後得到答案
1.定義子問題
2.找到問題和子問題間的(遞迴)關係
3.找出計算的順序
並避免用遞迴計算
要如何從子問題得到原本問題的答案?
首先子問題我們可以稱為狀態
看到題目的時候需要設計適合的狀態
接著要去想狀態間的關係(轉移方程)
ex:
A
B
+10
接著依照順序
把問題解決
得到最後的答案
有個小朋友要上樓梯
每次可以往上走1階或2階(往上)
有幾種可以到n階的方法
首先
來設定狀態
我們設定dp[i]是走到第i格的方法數
因此最後要求的答案
要算出dp[n]的數值
找出狀態間的關係
今天要走到第i階之前
要先走到i-1跟i-2階
因此dp[i]=dp[i-1]+dp[i-2]
用遞迴式表示
發現在用遞迴的時候要相信呼叫完他會幫你算好
如果從小開始算的話?
一般人正常算費式數列應該都是這樣算的吧
其實就是費事數列
用到dp[i]時 dp[i-1],dp[i-2]的值已經算好了
一開始要有個狀態
才能一直轉移上去
dp[1]=1;
dp[2]=2;
接著轉移
for(int i=3;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
答案
cout<<dp[n]<<'\n';有一個m*n的表格
從左上角走到右下角
每一步只能往右或往下一格
有幾種走法
先定義子問題
dp[i][j]=走到(i,j)的方法數
接著是轉移式
由於只能往右或往下走
所以當i>1 && j>1的時候
這一格可以從上面或左邊走過來
答案:dp[n][m]
初始狀態
要把所有沒有上面或左邊的先手算完
也就是這兩排
而這兩排都只有一條路
所以都是1
這是轉移的方式
方法數是從上面和左邊走下來的總和
初始狀態
for(int i=1;i<=n;i++)dp[i][1]=1;
for(int i=1;i<=m;i++)dp[1][i]=1;轉移式
for(int i=2;i<=n;i++){
for(int j=2;j<=m;j++){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}答案
cout<<dp[n][m]<<'\n';這幾題讓你們稍微感受一點點
dp的感覺
後面的題目會一步步建立起dp的思維模式
跟一開始的費事數列有點像?
看到dp題目要怎麼開始想
狀態!
今天定義dp[i]是走到第i格最小成本
答案:dp[n]
要怎麼轉移?
轉移式!
今天我在第i格
我能從我下面一格或兩格走來
那既然dp[i]是第i格的最小
dp[i-1],dp[i-2]就是i-1,i-2格的最小
那今天我們要接著走的應該要選小的繼續走
所以轉移式就出來了!
轉移式!
dp[i]=min(dp[i-1],dp[i-2])+cost[i];選小的(取min)、繼續走(+這格成本)
初始狀態
走第1格只有1個成本->cost[1]
走第2格 也能只有1個成本->cost[2]
dp[1]=cost[1];
dp[2]=cost[2];答案
cout<<dp[n]<<'\n';每次轉移:O(1)
n次:O(n)
時間複雜度:
#include<bits/stdc++.h>
#define maxn 100005
using namespace std;
int dp[maxn],cost[maxn];
int main(){
int n;cin>>n;
for(int i=1;i<=n;i++)cin>>cost[i];
dp[1]=cost[1];
dp[2]=cost[2];
for(int i=3;i<=n;i++){
dp[i]=min(dp[i-1],dp[i-2])+cost[i];
}
cout<<dp[n]<<'\n';
}
把上面題目刷完就能高中競程輕鬆一半
這個裸題蠻多的
都寫
dp多刷題就能比較抓得住感覺
基本每次北市賽、TOI、APCS基本都會考
每次可以1格或2格
但花費|h[i]-h[j]|
狀態!
dp[i]=跳到第i格最小花費
答案:dp[n]
接著要想轉移式
轉移式!
跟上一題想法差不多
要找前兩個中最小
但是要考慮他們的高度
因為跳過來也有花費
dp[i]=min(從前一格跳過來,從前兩格跳過來)
轉移式!
for(int i=3;i<=n;i++){
dp[i]=min(dp[i-1]+abs(h[i-1]-h[i]),dp[i-2]+abs(h[i-2]-h[i]));
}初始狀態
(dp[1]=0;)
dp[2]=abs(h[2]-h[1]);答案
cout<<dp[n]<<'\n';最多走k格
這題的想法應該不難
就使原本的2個轉移變成k個
狀態一樣
來想轉移式
轉移式
今天可以從前k個跳過來
那我希望走的就是最小的路徑
取min 然後算跳過來的成本
for(int i=k+1;i<=n;i++){
for(int j=1;j<=k;j++){
dp[i]=min(dp[i],dp[i-j]+abs(h[i]-h[i-j]));
}
}每次都取一次min
看從哪一格成本比較低
初始狀態!
因為我們會用到前k個
所以要先手算好1~k
那1~k都可以1次跳
dp[i]=從第1格跳過來的成本
for(int i=1;i<=k;i++)dp[i]=abs(h[i]-h[1]);CODE
#include<bits/stdc++.h>
#define maxn 100005
using namespace std;
int dp[maxn],h[maxn];
int main(){
fill(dp,dp+maxn,1e9);
int n,k;cin>>n>>k;
for(int i=1;i<=n;i++)cin>>h[i];
for(int i=1;i<=k;i++)dp[i]=abs(h[i]-h[1]);
for(int i=k+1;i<=n;i++){
for(int j=1;j<=k;j++){
dp[i]=min(dp[i],dp[i-j]+abs(h[i]-h[i-j]));
}
}
cout<<dp[n]<<'\n';
}
由於每次都取min
如果預設為0
全部都會是0
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,m;
cin>>n>>m;
int g[205][205],dp[205][205];
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>g[i][j];
}
}
dp[1][1]=g[1][1];
for(int i=1;i<=n;i++)dp[i][1]=dp[i-1][1]+g[i][1];
for(int i=1;i<=m;i++)dp[1][i]=dp[1][i-1]+g[1][i];
for(int i=2;i<=n;i++){
for(int j=2;j<=m;j++){
dp[i][j]=max(dp[i][j-1],dp[i-1][j])+g[i][j];
}
}
cout<<dp[n][m];
}#include<bits/stdc++.h>
using namespace std;
int main(){
int n;
cin>>n;
int s[200005],dp[200005];
for(int i=1;i<=n;i++){
cin>>s[i];
}
dp[1]=s[1];
dp[2]=max(dp[1],s[2]);
for(int i=3;i<=n;i++){
dp[i]=max(dp[i-1],dp[i-2]+s[i]);
}
cout<<dp[n];
}#include<bits/stdc++.h>
using namespace std;
#define int long long
int32_t main(){
int n;
cin>>n;
int c[100005],dp[100005];
for(int i=1;i<=n;i++){
cin>>c[i];
}
dp[1]=c[1];
dp[2]=c[2];
dp[3]=c[3]+min(c[1],c[2]);
for(int i=4;i<=n;i++){
dp[i]=c[i]+min({dp[i-1],dp[i-2],dp[i-3]});
}
if(n!=1)cout<<min(dp[n],dp[n-1]);
else cout<<c[1];
}#include<bits/stdc++.h>
using namespace std;
int main(){
int n,t,dp[2][200005];
cin>>n>>t;
vector<pair<int,int>>v;
v.push_back({0,0});
for(int i=0;i<n;i++){
int a,b;
cin>>a>>b;
v.push_back({a,b});
}
dp[0][1]=(abs(t-v[1].first));
dp[1][1]=(abs(t-v[1].second));
for(int i=2;i<=n;i++){
dp[0][i]=min(dp[0][i-1]+abs(v[i-1].first-v[i].first),dp[1][i-1]+abs(v[i-1].second-v[i].first));
dp[1][i]=min(dp[0][i-1]+abs(v[i-1].first-v[i].second),dp[1][i-1]+abs(v[i-1].second-v[i].second));
}
cout<<min(dp[0][n],dp[1][n])<<'\n';
}LIS?
Longest increasing subsequence
最長遞增子序列
子序列:之前枚舉有講過
取跟不取的那題
要找數列中
最長且符合遞增的子序列
舉例:
1 3 5 2 9
3 9
遞增子序列
1 5 2 9
不是遞增子序列
要找出最長的(可能不只一個)
1 3 5 2 9
其中一種最長的:1 3 5 9
接下來要用dp的想法來解
有兩種做法
設定狀態
dp[i]是以第i項作為結尾的最長子序列長度
答案:dp[1~n]中最長的
接著要來想轉移式
每次都掃過前面的看能不能接
只求長度
由於前面dp[j]存的都是最長的子序列長度
每次往前找到如果比現在位置的值小就接看看
看怎麼接長度最長
for(int i=2;i<=n;i++){
for(int j=1;j<i;j++){
if(arr[i]>arr[j]){
dp[i]=max(dp[i],dp[j]+1);
}
}
}j每次往前掃
遇到可以接的 看看接上去會不會比原本大
CODE
#include<bits/stdc++.h>
#define maxn 200005
using namespace std;
int arr[maxn],dp[maxn];
int main(){
int n;cin>>n;
for(int i=1;i<=n;i++)cin>>arr[i];
fill(dp,dp+maxn,1);
for(int i=2;i<=n;i++){
for(int j=1;j<i;j++){
if(arr[i]>arr[j]){
dp[i]=max(dp[i],dp[j]+1);
}
}
}
int ans=1;
for(int i=1;i<=n;i++)ans=max(ans,dp[i]);
cout<<ans<<'\n';
}
複雜度:
很遺憾
這個方式過不了這一題的
看一下測資
2e5->n log n
接著用一點特殊的算法
Robinson–Schensted–Knuth algorithm
用的是貪心想法
直接來看怎麼做
利用binary search加速
如果聽不懂可以看這個
今天我們要來做一個LIS
如果a比陣列所有值大
那我們就可以放在最後
長度+1
要放入一個數字a
否則找一個它應該在的位置
將原有值替換掉
要放入一個數字2
ex
1 3 5 2 9
1 3 5
假如目前LIS做到第3
它沒比5大
找到適合它的位置
->把3換掉
LIS:1 2 5
要放入一個數字9
ex
1 3 5 2 9
1 2 5
接著LIS做到第4
它比5大
推到最後
LIS:1 2 5 9
長度=4
由於是遞增序列
是不是就有單調性
所以在找要怎麼替換的時候
可以快速找到
二分搜!
問題瞬間變簡單很多
CODE
#include<bits/stdc++.h>
#define maxn 200005
using namespace std;
int arr[maxn],dp[maxn];
vector<int>v;
int main(){
int n;cin>>n;
for(int i=1;i<=n;i++){
cin>>arr[i];
}
for(int i=1;i<=n;i++){
if(lower_bound(v.begin(),v.end(),arr[i])==v.end())v.push_back(arr[i]);
else{
auto it=lower_bound(v.begin(),v.end(),arr[i]);
*it=arr[i];
}
}
cout<<v.size();
}利用二分搜
要注意 題目是嚴格遞增
複雜度:O(n log n)
最後vector裡面的並不是真正的LIS
要利用pos[]陣列來存
再用特殊的找法
詳情上面的連結有
求最長共同子序列長度
並找一種輸出
ex
s1 : 5 7 9 3 1 2
s2 : 3 5 3 2 8
LCS=5 3 2
先來講長度
狀態
dp[i][j]=s1前i個跟s2前j個的LCS長度
答案=dp[n][m]
接著要來想轉移式
考慮兩種情況
1. s1[i]==s2[j]
2. s1[i]!=s2[j]
LCS長度是上面跟左邊格的mx
dp[i][j]=max(dp[i-1][j],dp[i][j-1])
則LCS長度+1
dp[i][j]=dp[i-1][j-1]+1
長度求完了 要怎麼找子序列
要來找怎麼轉移過來的
如果dp[i][j]是由dp[i-1][j-1]轉移過來的
代表s[i]==s[j]
用反推的
然後記錄怎麼來的
用一個pre陣列存起來
0 1 2分別是三種轉過來的路徑左上 左 上
最後用遞迴走回去並輸出
直接看code
#include<bits/stdc++.h>
#define maxn 1005
using namespace std;
int dp[maxn][maxn],pre[maxn][maxn];
vector<int>s1,s2;
void print(int i,int j){
if(i==0 || j==0)return;
if(pre[i][j]==1){
print(i-1,j-1);
cout<<s1[i]<<' ';
}
else if(!pre[i][j]){
print(i,j-1);
}
else print(i-1,j);
return;
}
int main(){
int n,m;cin>>n>>m;
s1.push_back(0);
s2.push_back(0);
for(int i=0;i<n;i++){
int a;cin>>a;
s1.push_back(a);
}
for(int i=0;i<m;i++){
int a;cin>>a;
s2.push_back(a);
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(s1[i]==s2[j]){
dp[i][j]=dp[i-1][j-1]+1;
pre[i][j]=1;
}
else{
if(dp[i][j-1]>dp[i-1][j]){
dp[i][j]=dp[i][j-1];
pre[i][j]=0;
}
else{
dp[i][j]=dp[i-1][j];
pre[i][j]=2;
}
}
}
}
cout<<dp[n][m]<<'\n';
print(n,m);
}有一個可以耐重W的背包,N個物品
每個物品有各自的wi,vi
求放進價值最大為多少?
0/1背包問題
暴力枚舉?->TLE
設定dp狀態
dp[i][j]是從前i個物品中,取到重量恰為j時的最大價值
答案=dp[n][m]
狀態轉移
從前i項選擇物品的最佳方案一定是
(有選第 i 項物品)或(沒有選第 i 項物品)
其中一種
1.假如今天有選第i項物品
那可以從前 i-1項 j-w[i] 項轉移過來
2.假如沒有選
可以從前i-1項 j 項轉移
code
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
if(j-w[i]<0){
dp[i][j]=dp[i-1][j];
continue;
}
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
}
}如果拿了第i 重量超過j就不能拿
沒選 有選
code不難 但想法要理解一下
實作
今天轉移的時候
最多只會用到 [i-1][j] [i-1][j-w[i]]
在這些之前的都不會用到
所以可以用滾動方式優化
dp[i%2][j]=max(dp[(i+1)%2][j-w[i]]+v[i], dp[(i+1)%2][j];只用兩排陣列解決
其實可以用一維陣列解決?
空間複雜度少很多
將j由m到0跑
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){
if(j-w[i]<0)continue;
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}當要更新dp[j]的時候
使用左邊的dp[j-w[i]] 和 這格的dp[j]
都還沒跑第i次(是i-1次的計算結果)
所以不會出錯
Code
#include<bits/stdc++.h>
#define maxn 105
using namespace std;
long long w[maxn],v[maxn],dp[100005];
int main(){
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++)cin>>w[i]>>v[i];
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){
if(j-w[i]<0)continue;
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
cout<<dp[m];
}
我們是透過從二維一路優化過來
而不是直接只記一維的做法
否則遇到變形題就沒了
如果W很大?
1e9 -> [n][w] = 1e9*100 -> (MLE)
想想看其他種定狀態的方法
改成用價值來定
狀態
dp[i][j]=從前i項物品
取到總價值恰為j的最小重量
答案=當dp[i][j]<=m時的最大j
接著要來想轉移式
轉移式
dp[i][j] = dp[i-1][j-v[i]]+w[i] 選擇第i項
dp[i][j] = dp[i-1][j] 不選擇第i項
我們希望在裝同價值物品的情況下
重量越少越好
所以取min
for(int i=1;i<=n;i++){
for(int j=0;j<=sum;j++){
if(j-v[i]<0){
dp[i][j]=dp[i-1][j];
continue;
}
dp[i][j]=min(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
if(dp[i][j]<=m)ans=max(ans,j);
}
}
價值不能<0
這樣其實還不完整 思考一下如果前i-1項都沒拿->重量=0 這樣第i取min時 就會一直不拿
(全部都不拿重量最小)
當i=0 j=0 dp[i][j]=0
但i=0 j>0時 不可能發生 所以沒取的時候我們把dp[0][1~mx]重量設inf 極大的數字
這樣在轉移的時候 才能正確找到有取且重量最小的
CODE
#include<bits/stdc++.h>
#define ll long long
#define maxn 105
using namespace std;
ll dp[maxn][100005],v[maxn],w[maxn],sum;
int main(){
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>w[i]>>v[i];
sum+=v[i];
}
fill(dp[0]+1,dp[0]+sum+5,1e9);
int ans=0;
for(int i=1;i<=n;i++){
for(int j=0;j<=sum;j++){
if(j-v[i]<0){
dp[i][j]=dp[i-1][j];
continue;
}
dp[i][j]=min(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
if(dp[i][j]<=m)ans=max(ans,j);
}
}
cout<<ans<<'\n';
}
j的範圍
最多有可能全部選
所以是0~sum(v[1~n])
有一個可以耐重W的背包,N個物品
每種可以拿任意數量
每個物品有各自的wi,vi
求放進價值最大為多少?
定義狀態
狀態
dp[i][j] = 從前i個物品中,取到重量恰為j時的最大價值
跟0/1背包一模一樣
但轉移式要怎麼想
想法:
不取這一項or這一項再取一個
其中一種
轉移
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]]+v[i], dp[i-1][j-w[i]*2]+2*v[i], ....., dp[i-1][j-w[i]*k]+v[i]
且w[i]*k<=j
那經過整理:
dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]]+v[i])
相當於再拿一次第i項
#include<bits/stdc++.h>
#define maxn 1005
#define ll long long
using namespace std;
ll w[maxn],v[maxn],dp[maxn][maxn];
int main(){
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++)cin>>w[i];
for(int i=1;i<=n;i++)cin>>v[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
if(j-w[i]<0){
dp[i][j]=dp[i-1][j];
continue;
}
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);
}
}
cout<<dp[n][m]<<'\n';
}
CODE
只有轉移式的部分有改
可以發現只會用到這一排跟上一排
->可以滾動
也可以用一維來寫
j從0~m跑
需要用到上面那格跟這一輪更新過的j-w[i] 按照0~m的順序剛好不會覆蓋到
空間優化CODE
#include<bits/stdc++.h>
#define maxn 1005
#define ll long long
using namespace std;
ll w[maxn],v[maxn],dp[maxn];
int main(){
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++)cin>>w[i];
for(int i=1;i<=n;i++)cin>>v[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
if(j-w[i]<0)continue;
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
cout<<dp[m]<<'\n';
}
有一個可以耐重W的背包,N個物品
每種可以拿ki個
每個物品有各自的wi,vi
求放進價值最大為多少?
把每個物品數量當成不同物品
然後做0/1背包
K為重複數量最大值
有
的作法
今天我們教這個 另外的要用到dp優化的內容
來設定狀態
狀態
跟無限背包很像
dp[i][j] = max(dp[i-1][j], dp[i-1][j-k*w[i]]+k*v[i]
1<=k<=重複數量
這樣轉移所花的時間差不多
想想怎麼優化
今天如果要組合出0到49
需要幾個數字?
49個1?
二進位!
我們可以把k[i]分成{1,2,4,8,2ᵖ,q}
其中p為使 不大於k[i]的最大整數
ex.21 可以分成{1,2,4,8,6}
為甚麼這樣可以湊出所有種組合?
x< 則一定可以用{1,2,4,8,2ᵖ}湊出來
x> 則先取q,一定可以用{1,2,4,8,2ᵖ}湊出來
(剩下x-q< )
最多分出log(k[i])+1堆
那再用0/1背包
就能取到所有組合
複雜度O(N*W*log(k[i]))
一些背包問題的題目
去刷題吧
課程到這邊