# 進階動態規劃
by: hush
---
## LIS
----
求一段長度為$n$的序列$a$的最長遞增子序列(Longest Increase Subsequence)的長度,這裡的子序列不一定要是連續的
----
- 定義狀態:
定義$dp[n+1]$,$dp[i]$代表以$a[i]$結尾的LIS長度
- 轉移式:$dp[i]=\max(dp[j])+1,\ j<i,\ a[j]<a[i]$
- 邊界條件(2選1):
1.$\not\exists j<i,\ a[j]<a[i]$ 時 $dp[i]=1$
2.設$a[0]=-INF,\ dp[0]=0$(序列從1開始)
- 答案:$\max(dp[i])$
----
- 時間複雜度分析:
有$O(n)$個狀態,每個的轉移時間$O(n)$
總共需要$O(n^2)$,還可以更快
----
- 用Greedy的想法來看:
1.相同$dp$值我們只關心結尾最小的,所以我們可以開一個陣列存每個$dp$值的最小結尾
2.陣列值一定是遞增的(序列才能往後接),預設為INF
- 所以可以對陣列做二分搜!可以$O(log n)$內找到符合條件的最大$dp$值,並更新最小結尾
總時間$O(nlogn)$
----
實作上我們只需要開一個陣列維護每個$dp$值的最小結尾即可,預設為INF,最後答案就是最後一個不是INF的陣列索引
----
Code
```cpp=
#include<bits/stdc++.h>
#define int long long
#define itn int
#define endl '\n'
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0);
using namespace std;
vector<int> dp(200005,1e18),a(200005);
signed main()
{
int n; cin >> n;
for (int i=0;i<n;i++) cin >> a[i];
for (int i=0;i<n;i++)
{
int j=lower_bound(dp.begin(),dp.begin()+n,a[i])-dp.begin();
//非嚴格遞增改為upper_bound
dp[j]=a[i];
}
cout << lower_bound(dp.begin(),dp.begin()+n,1e18)-dp.begin() << endl;
}
```
----
- 練習題:
[APCS2101 P4 zj_f608](https://zerojudge.tw/Submissions?problemid=f608&account=rayho0319@gmail.com)
----
AC code:
```cpp=
#include<bits/stdc++.h>
#define int long long
#define itn int
#define endl '\n'
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0);
#define pii pair<int,int>
#define x first
#define y second
using namespace std;
vector<int> dp(200005,1e18);
vector<pii> a(200005);
signed main()
{
int n; cin >> n;
for (int i=0;i<n;i++) cin >> a[i].x >> a[i].y;
sort(a.begin(),a.begin()+n);
for (int i=0;i<n;i++)
{
int x=upper_bound(dp.begin(),dp.begin()+n,a[i].y)-dp.begin();
dp[x]=a[i].y;
}
cout << lower_bound(dp.begin(),dp.begin()+n,1e18)-dp.begin() << endl;
}
```
---
## LCS
----
求兩段長度為$n,\ m$的序列$A,\ B$的最長遞增子序列(Longest Common Subsequence),這裡的子序列不一定要是連續的
----
- 定義狀態:
定義$dp[n+1][m+1]$,$dp[i][j]$代表考慮$A的前i項與B的前j項$的LCS長度
- 轉移式:
$\left\{
\begin{array}{l}
dp[i][j]=dp[i-1][j-1]+1,\ A[i]=B[j]\\
dp[i][j]=\max(dp[i-1][j],dp[i][j-1]),\ A[i]\not =B[j]
\end{array}
\right.$
----
- 邊界條件:
$dp[i][0]=dp[0][j]=0$
- 答案:$dp[n][m]$
----
- 時間複雜度分析:
有$O(nm)$個狀態,每個狀態轉移需要$O(1)$,總共是需要$O(nm)$時間
----
例題:[zj_a133](https://zerojudge.tw/ShowProblem?problemid=a133)
----
AC code
```cpp=
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0);
using namespace std;
int a[105],b[105],n,m,cnt=1;
vector<vector<int>> dp(105,vector<int>(105));
signed main()
{
//colinorz;
while (cin >> n >> m)
{
if (n==0 && m==0) break;
for (int i=1;i<=n;i++) cin >> a[i];
for (int i=1;i<=m;i++) cin >> b[i];
for (int i=0;i<=n;i++)
{
for (int j=0;j<=m;j++)
{
if (i==0 || j==0) dp[i][j]=0;
else if (a[i]==b[j]) dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
cout << "Twin Towers #"<<cnt++<<"\nNumber of Tiles : " << dp[n][m] << endl;
}
}
```
----
練習題:[zj_c001](https://zerojudge.tw/ShowProblem?problemid=c001)
----
AC code
```cpp=
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0);
using namespace std;
string s,t;
vector<vector<int>> dp(1005,vector<int>(1005));
signed main()
{
//colinorz;
while (cin >> s >> t)
{
int ss=s.size(),ts=t.size();
for (int i=0;i<=ss;i++)
{
for (int j=0;j<=ts;j++)
{
if (i==0 || j==0) dp[i][j]=0;
else if (s[i-1]==t[j-1]) dp[i][j]=max({dp[i-1][j-1]+1,dp[i-1][j],dp[i][j-1]});
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
cout << dp[ss][ts] << endl;
}
}
```
----
另外的作法:
- LCS轉成LIS(歸約法):
1.將$A$序列裡每個元素和它對應的index存到$M$(map)裡
2.構建一個$C$序列,依序將$B$序列的每個元素對應到$A$序列的index加入$C$序列的後方
3.$C$序列的LIS長度就是$A,\ B$的LCS長度
- p.s.: 若$A$有重複元素則將indexes存入vector,建構到$C$時以降冪順序建構
----
- 時間複雜度分析:
在重複數字較少時約為$O(n log n)$,不過最差情況會到$O(n^2logn^2)$,所以一般情況下較少用
$n大約是min(|A|,|B|)$
---
## 區間DP
----
- 例題:
[APCS2401_P4 合併成本](https://zerojudge.tw/ShowProblem?problemid=m934)
----
- 定義狀態:
定義$dp[n][n]$,$dp[i][j]代表合併i$~$j$的最小成本
- 轉移式:
$dp[l][r]=min(dp[l][i]+dp[i+1][r]+cost(l,i,r)),$
$i\in [l,r)$
- 邊界條件:
$dp[i][i]=0$
- 答案:
$dp[1][n]$
----
- 時間複雜度分析:
有$O(n^2)$個狀態,每次轉移需要$O(n)$的時間,總共是$O(n^3)$,可以更快但通常不需要
----
AC code
```cpp=
#include<bits/stdc++.h>
#define int long long
#define itn int
#define endl '\n'
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0);
using namespace std;
vector<int> pre(105,0);
vector<vector<int>> dp(105,vector<int>(105,-1));
inline int cost(int l,int i,int r)
{ return abs( (pre[r]-pre[i]) - (pre[i]-pre[l-1]) ); }
int mg(int l,int r)
{
if (l==r) return 0;
if (dp[l][r]!=-1) return dp[l][r];
int ans=1e18; //INF
for (int i=l;i<r;i++) //[l,r)
ans=min(ans,mg(l,i)+mg(i+1,r)+cost(l,i,r));
return dp[l][r]=ans;
}
signed main()
{
//colinorz;
int n; cin >> n;
for (int i=1;i<=n;i++) cin >> pre[i],pre[i]+=pre[i-1];
cout << mg(1,n) << endl;
}
```
----
- 練習題:
[atcoder_DP_N](https://atcoder.jp/contests/dp/tasks/dp_n)
----
AC code
```cpp=
#define int long long
#define itn int
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0);
#define endl '\n'
using namespace std;
vector<int> a(405),pre(405,0);
vector<vector<int>> dp(405,vector<int>(405,-1));
int slm(int l,int r) //[l,r]
{
if (l==r) return 0;
if (dp[l][r]!=-1) return dp[l][r];
int ans=1e18;
for (int i=l;i<r;i++) ans=min(ans,slm(l,i)+slm(i+1,r));
return dp[l][r]=ans+pre[r]-pre[l-1];
}
signed main()
{
//colinorz;
int n; cin >> n;
for (int i=1;i<=n;i++) cin >> a[i];
for (int i=1;i<=n;i++) pre[i]=pre[i-1]+a[i];
cout << slm(1,n) << endl;
}
```
---
## 背包問題
----
- 0/1背包:一個物品只能取一次
- 無限背包:一個物品能取無限次
- 有限背包:一個物品能取有限次
----
### 0/1背包
有$N$種物品,每種物品的重量為$w[i]價值為v[i]$**且只有一個**,要放進一個耐重$W$的背包裡,求背包在不超重的情況最多可以裝到多少總價值
$1\le N\le 100$,$1\le W\le 10^5$
----
- 定義狀態:
定義$dp[N+1][W+1]$,$dp[i][j]$代表考慮到第$i$種物品且背包可承受重量為$j$的最大價值
- 轉移式:
$dp[i][j]=\max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])$
$,\ j\ge w[i]$
$dp[i][j]=dp[i-1][j]$,$j<w[i]$
----
- 邊界條件:
$dp[0][j]=0,\ j\le W$
- 答案:
$dp[N][W]$
----
- 時間複雜度分析:
有$O(NW)$個狀態,每次轉移需要$O(1)$的時間,總共是$O(NW)$
----
- 例題:
[atcoder_DP_D](https://atcoder.jp/contests/dp/tasks/dp_d)
----
AC code
```cpp=
#include <bits/stdc++.h>
#define int long long
#define itn int
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(false);
#define vi vector<int>
using namespace std;
vi w(105),v(105);
vector<vi> dp(105,vi(100005));
signed main()
{
int n,m; cin >> n >> m;
for (int i=1;i<=n;i++) cin >> w[i] >> v[i];
for (int i=0;i<=n;i++)
{
for (int j=0;j<=m;j++)
{
if (i==0||j==0) dp[i][j]=0;
else
{
if (j-w[i]<0) dp[i][j]=dp[i-1][j];
else dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
}
}
}
cout << dp[n][m] <<'\n';
}
```
----
- 練習題:
[atcoder_DP_E](https://atcoder.jp/contests/dp/tasks/dp_e)
----
定義狀態為$dp[i][j]$:
考慮到第$i$項且背包裝了$j$單位價值所需最小重量
----
AC code
```cpp=
#include <bits/stdc++.h>
#define int long long
#define itn int
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(false);
#define vi vector<int>
#define INF numeric_limits<int>::max()
using namespace std;
vi w(105),v(105);
vi dp(100005);
signed main()
{
itn n,m,ans=0; cin >> n >> m;
for (int i=1;i<=n;i++) cin >> w[i] >> v[i];
int vsum=0; for (int i=1;i<=n;i++) vsum+=v[i];
for (int i=0;i<=n;i++)
{
for (int j=vsum;j>=0;j--)
{
if (i==0&&j==0) dp[j]=0;
else if (i==0) dp[j]=-1;
else if (j-v[i]<0 || dp[j-v[i]]==-1) continue;
else if (dp[j]==-1) dp[j]=dp[j-v[i]]+w[i];
else dp[j]=min(dp[j],dp[j-v[i]]+w[i]);
}
}
for (int i=0;i<=vsum;i++)
if (dp[i]>=0 && dp[i]<=m) ans=i;
cout << ans <<'\n';
}
```
----
### 無限背包
有$N$種物品,每種物品的重量為$w[i]價值為v[i]$**且有無限個**,要放進一個耐重$W$的背包裡,求背包在不超重的情況最多可以裝到多少總價值
$1\le N\le 100$,$1\le W\le 10^5$
----
- 定義狀態:
定義$dp[N+1][W+1]$,$dp[i][j]$代表考慮到第$i$種物品且背包可承受重量為$j$的最大價值
- 轉移式:
$dp[i][j]=\max(dp[i-1][j],dp[i][j-w[i]]+v[i])$
$,\ j\ge w[i]$
$dp[i][j]=dp[i-1][j]$,$j<w[i]$
----
- 邊界條件:
$dp[0][j]=0,\ j\le W$
- 答案:
$dp[N][W]$
----
- 時間複雜度分析:
有$O(NW)$個狀態,每次轉移需要$O(1)$的時間,總共是$O(NW)$
----
code
```cpp=
#include <bits/stdc++.h>
#define int long long
#define itn int
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(false);
#define vi vector<int>
using namespace std;
vi w(105),v(105);
vector<vi> dp(105,vi(100005));
signed main()
{
int n,m; cin >> n >> m;
for (int i=1;i<=n;i++) cin >> w[i] >> v[i];
for (int i=0;i<=n;i++)
{
for (int j=0;j<=m;j++)
{
if (i==0||j==0) dp[i][j]=0;
else
{
if (j-w[i]<0) dp[i][j]=dp[i-1][j];
else dp[i][j]=max(dp[i][j],dp[i-1][j-w[i]]+v[i]);
}
}
}
cout << dp[n][m] <<'\n';
}
```
----
### 有限背包
有$N$種物品,每種物品的重量為$w[i]價值為v[i]$且有$k[i]$個,要放進一個耐重$W$的背包裡,求背包在不超重的情況最多可以裝到多少總價值
----
- 定義狀態:
定義$dp[N+1][W+1]$,$dp[i][j]$代表考慮到第$i$種物品且背包可承受重量為$j$的最大價值
- 轉移式:
$dp[i][j]=\max(dp[i-1][j],dp[i][j-t\cdot w[i]]+$
$t\cdot v[i]),\ j\ge w[i],\ t\le k[i]$
$dp[i][j]=dp[i-1][j],\ j<w[i]$
- 邊界條件:
$dp[0][j]=0,\ j\le W$
----
- 時間複雜度分析:
有$O(NW)$個狀態,每次轉移需要$O(k[i])$的平均,總共是$O(NW\sum{k[i]})$,之後會教一些邪惡的優化手段讓$K$變不見
----
### 延伸問題
各種零錢問題像是:有幾種方法湊出指定價格、最少錢幣個數湊出指定價格、最少錢幣種類湊出指定價格...。(例:[CSES1634](https://cses.fi/problemset/task/1634/))
---
## 狀態壓縮
----
當你需要定義的狀態數量有太多個的時候,你可以將好幾個狀態使用一個整數(通常是利用二進制)來表示,例如$dp[2][2][2][2][2]可以變成dp[2^5]$
----
- 例題1:
[atcoder_DP_O](https://atcoder.jp/contests/dp/tasks/dp_o)
----
- 題意:
給定$n$個男人和$n$個女人,告訴你每個男人與每個女人是否相容,需要將所有人配對,使每對都是相容的,且每人只能屬於一對$(n\le 21)$
求可行配對的總數,結果對$10^9+7$取模。
----
- 定義狀態:
$\overbrace{dp[2][2]...[2]}^\text{n項}$,$dp[a_{n}]...[a_2][a_1],a_i\in\{0,1\}$代表考慮到前$\sum{a_i}$個男人且第$a_i$為$1$的女人被選走有幾種可能
例: $dp[0][1][0][1]$為考慮前$2$男且第$0$, $2$女被選走
但是要操作$n$維陣列非常困難,所以可以使用狀態壓縮技巧改成定義$dp[2^n]$,每個整數對應一種狀態$dp[5_{(2)}]$對應到$dp[0][1][0][1]$
----
- 轉移式:
$dp[S]=\sum\limits_{i=1}^{n}{dp[S\oplus i]},\forall\ a[|S|][i]=1$
- 邊界條件:
$dp[0]=1,其他預設為0$
- 答案:
$dp[2^n-1]$
- 時間複雜度分析:
有$O(2^n)$個狀態,每次轉移$O(n)$時間,共需要$O(2^n\cdot n)$時間
----
AC code
```cpp=
#include<bits/stdc++.h>
#define int long long
#define itn int
#define endl '\n'
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0);
using namespace std;
constexpr int MOD=1e9+7;
vector<vector<int>> edge(25);
int dp[(1<<21)+5]={1};
signed main()
{
//colinorz;
int n,ans=0; cin >> n;
for (int i=0;i<n;i++)
for (int j=0;j<n;j++)
{
bool tmp; cin >> tmp;
if (tmp) edge[i].emplace_back(j);
}
for (int msk=1;msk<(1<<n);msk++)
{
int i=__builtin_popcount(msk)-1; //二進制的msk有幾個1
for (int j : edge[i])
if ((msk>>j)&1) dp[msk]=(dp[msk]+dp[msk^(1<<j)])%MOD;
}
cout << dp[(1<<n)-1] << endl;
}
```
----
- 例題2:
[csdc93](https://csdc.tw/problem/93/)
----
- 題意:
求$p$位數且以$q$結尾的快樂數字有幾種,(快樂數字由$1\text{~}9$構成,相鄰兩位相差$\le2$,且$1\text{~}9$皆至少出現一次),有$n$筆詢問
$(p\le 10^3,\ q\in [1,9],\ n\le 10^4)$
----
- 定義狀態:
$dp[2^9][p][q]$,$dp[S][i][j]($其中$S$的第$k$位為$1$代表$k$有在快樂數字中$)$代表考慮$S$狀態$i$位數$j$結尾的答案
- 轉移式:
$dp[S][i][j]=$
$\sum\limits_{k=j-2}^{j+2}{dp[S][i-1][k]+dp[S\oplus j][i-1][k]}$
$如果S第j位為0,dp[S][i][j]=0$
----
- 邊界條件:
$dp[2^i][1][i]=1,\ i\le 9$,其他預設為0
- 答案:
$dp[2^9-1][p][q]$
- 時間複雜度分析:
預處理有$O(2^{\max(q)}\cdot \max(p)\cdot \max(q))$個狀態,$O(1)$轉移時間,乘起來略小於$10^8$,詢問只需要$O(n)$時間,輕鬆通過
----
AC code
```cpp=
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0);
using namespace std;
constexpr int MOD=1e9+7;
int dp[(1<<9)+3][1005][12]={};
signed main()
{
//colinorz;
for (int j=0;j<=8;j++) dp[(1<<j)][1][j]=1;
for (int msk=1;msk<(1<<9);msk++)
for (int i=2;i<1001;i++)
for (int j=0;j<=8;j++)
{
if (msk&(1<<j)==0) continue;
for (int k=max(0LL,j-2); k<=min(8LL,j+2); k++)
if (msk&(1<<k))
dp[msk][i][j]=(dp[msk][i][j]+dp[msk][i-1][k]+
p[msk^(1<<j)][i-1][k])%MOD;
}
int n; cin >> n;
while (n--)
{
int p,q; cin >> p >> q;
cout << dp[(1<<9)-1][p][q-1] << endl;
}
}
```
----
- 進階題:
[csdc329](https://csdc.tw/problem/329/)
[zj_a086](https://zerojudge.tw/ShowProblem?problemid=a086)
[atcoder_abc187_F](https://atcoder.jp/contests/abc187/tasks/abc187_f) (DP+子集枚舉)
----
csdc329 AC code
```cpp=
```
----
zj_a086 AC code
```cpp=
#include<bits/stdc++.h>
#define int long long
#define itn int
#define endl '\n'
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0);
using namespace std;
int n,m,cnt=0,ans=0,pcnt[65]={},dp[105][65][65]={};
bitset<12> msk[65]={},a[105]={};
string tmp;
void dfs(int now=-1,bitset<12> s={},int p=0)
{
if (now>=m-3)
{
if (now!=m) s|=(1<<now),p++;
msk[cnt]=s,pcnt[cnt++]=p;
return;
}
if (now!=-1)
{
s|=(1<<now),p++;
for (int i=now+3;i<=m;i++) dfs(i,s,p);
}
else for (int i=0;i<=m;i++) dfs(i,s,p);
}
inline bool lgl(const bitset<12>& a,const bitset<12>& b)
{
return (a&b).none();
}
signed main()
{
//colinorz;
cin >> n >> m; dfs();
for (int i=0;i<n;i++)
{
cin >> tmp;
for (int j=0;j<m;j++) a[i][j]=(tmp[j]=='H'?1:0);
}
for (int j=0;j<cnt;j++)
if (lgl(a[0],msk[j])) dp[0][j][cnt]=pcnt[j];
for (int j=0;j<cnt;j++)
if (lgl(a[1],msk[j])) for (int k=0;k<cnt;k++)
{
if (lgl(a[0],msk[k]) && lgl(msk[k],msk[j]))
dp[1][j][k]=max(dp[1][j][k],dp[0][k][cnt]);
dp[1][j][k]+=pcnt[j];
}
for (int i=2;i<n;i++)
for (int j=0;j<cnt;j++)
if (lgl(msk[j],a[i])) for (int k=0;k<cnt;k++)
{
if (!lgl(msk[k],a[i-1]) || !lgl(msk[k],msk[j])) continue;
for (int l=0;l<cnt;l++)
if (lgl(msk[l],a[i-2]) && lgl(msk[j],msk[l]) && lgl(msk[k],msk[l]))
dp[i][j][k]=max(dp[i][j][k],dp[i-1][k][l]);
dp[i][j][k]+=pcnt[j];
ans=max(ans,dp[i][j][k]);
}
cout << ans << endl;
}
```
----
abc187_F AC code
```cpp=
#include<bits/stdc++.h>
#define int long long
#define itn int
#define endl '\n'
#define colinorz cin.tie(0);cout.tie(0);ios::sync_with_stdio(0);
using namespace std;
int edge[20][20]={},dp[(1<<18)+5]={},n,m;
signed main()
{
//colinorz;
cin >> n >> m; //[0,n)
for (int i=0;i<n;i++) edge[i][i]=1;
while (m--)
{
int a,b; cin >> a >> b;
edge[a-1][b-1]=edge[b-1][a-1]=1;
}
for (int msk=1;msk<(1<<n);msk++)
{
dp[msk]=1;
for (int i=0;dp[msk]==1 && i<n;i++)
if (msk&(1<<i))
for (int j=0;dp[msk] && j<n;j++)
if (msk&(1<<j) && !edge[i][j]) dp[msk]=1e18;
if (dp[msk]==1) continue;
for (int s=(msk-1)&msk;s ;s=(s-1)&msk)
dp[msk]=min(dp[msk],dp[s]+dp[msk^s]);
}
cout << dp[(1<<n)-1] << endl;
}
```
---
## 練習資源
----
[atcoder_DP](https://atcoder.jp/contests/dp/tasks)
[CSDC_DP](https://csdc.tw/problems/?tag=dp)
[Leetcode_DP](https://leetcode.com/problemset/?topicSlugs=dynamic-programming&page=1)
---
# 謝謝大家
{"title":"進階動態規劃","description":"by: hush","contributors":"[{\"id\":\"b49547c8-0e7f-46d0-99b2-8a45dfee8e90\",\"add\":20512,\"del\":4259}]"}