dp動態規劃2
===
---
DP可以被分類成xDyD的形式
x代表有$O(n^x)$個狀態
y代表轉移式需要$O(n^y)$
像費氏數列就是1D0D的DP
---
### Longest Common Subsequence(LCS)
----
LCS是序列分析重要的問題
目標是找出最長共同子序列
像TeacherLikeEgg這個字串中
Tea、cher、TLE都是子序列
而ggE不是,因為不能調整序列順序
LCS可以拿來作為字串相似度的定義
有很多應用
----
[例題](https://judge.tcirc.tw/ShowProblem?problemid=d070)
給兩個字串,求兩字串的LCS
----
首先最簡單的方法還是暴力
但一個字串有$2^n$個子序列
很明顯時間不允許
----
假設兩個字串分別是s1,s2
定義$dp[i][j]$為s1的前$i$個字元與
s2的前$j$個字元LCS的長度
當搜尋到$i$,$j$時有兩種狀況,
$s1[i]$==$s2[j]$ 或 $s1[i]$!=$s2[j]$
把兩個分開來討論
----
當 $s1[i]$==$s2[j]$
代表這個字元可以加進LCS
所以 $dp[i][j]=dp[i-1][j-1]+1$
----
而 $s1[i]$!=$s2[j]$時
代表兩邊至少有一邊對LCS沒有幫助
而因為要取最大的
所以轉移式可以寫成
$dp[i][j]=max(dp[i-1][j],dp[i][j-1])$
----
確定好轉移式後
還要設定初始條件
$dp[i][0]$代表其中一邊長度是0的字串和
某字串的LCS長度
既然一邊是空字串,那LCS一定也是空字串
所以$dp[i][0]=dp[0][j]=0$
----
整理起來就是
\begin{aligned}
dp[i][j]=
\begin{cases}
0\quad if\quad i=0\ or\ j=0 \\
dp[i-1][j-1]+1\quad if\ s1[i]=s2[j] \\
max(dp[i-1][j],dp[i][j-1]) \ else
\end{cases}
\end{aligned}
----
code
```cpp=
//直接拿這個丟到judge不會過可以自己想想原因
#include<bits/stdc++.h>
using namespace std;
signed main(){
string s1,s2;
cin>>s1>>s2;
int n=s1.size(),m=s2.size();
int dp[105][105];
memset(dp,0,sizeof(dp));//把dp中所有元素設定成0
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(s1[i-1]==s2[j-1]){
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];
return 0;
}
```
----
這裡用到一個叫memset的東東
他可以用來初始化內存
通常被用來初始化0或極大、小值
之後圖論蠻容易用到的
用法:
```cpp=
memset(變數名,0,sizeof(變數名))//全設成0
memset(變數名,0x7f,sizeof(變數名))//全設成極大值
memset(變數名,0x8f,sizeof(變數名))//全設成極小值
```
---
1D1D問題
----
上次的apcs[第四題](https://zerojudge.tw/ShowProblem?problemid=m373)就是一個1D1D的問題
這裡來講解要如何寫這題
----
題敘
```
你擁有一個長度為n的陣列,代表每天的投資收益,以及k張金牌。
你可以自行決定投資的開始和結束日期。在你選擇投資的每一天
,你可以選擇消耗一張金牌來跳過當天,或者不使用金牌而拿取當天的收益。
你的目標是找出如何投資,以實現最大的總收益。
請注意,你只能在投資期間進出一次。
```
----
這題當k=0時其實就是 __最大連續子陣列__ 的問題
但他多了免死金牌
所以狀態必須加上"用了幾個金牌"
----
設$dp[i][j]$為
結束在第$i$天,用掉$j$個金牌所獲得利潤的最大值
邊界條件:第0天的利潤是0
所以$dp[0][j]$=0
----
轉移式:
首先我們先不管金牌只討論$dp[i][0]$
這跟最大子陣列一樣
$dp[i][0]=max(dp[i-1][0]+a[i],a[i])$
----
接下來當k>0時
轉移式會是
$dp[i][j]=max(dp[i-1][j-1],dp[i-1][j]+a[i] :j<=k)$
----
整理起來就是這樣
\begin{aligned}
dp[i][j]=
\begin{cases}
0\quad if\quad i=0 \\
max(dp[i-1][j-1],dp[i-1][j]+a[i] :0<j<=k)
\end{cases}
\end{aligned}
----
code
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+5e5+5;
int k,n,a[N],dp[N][25];
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++)
cin>>a[i];
int ans=0;
for(int i=1;i<=n;i++){
dp[i][0]=max(dp[i-1][0]+a[i],a[i]);
ans=max(ans,dp[i][0]);
for(int j=1;j<=k;j++){
dp[i][j]=max(dp[i-1][j-1],dp[i-1][j]+a[i]);
ans=max(ans,dp[i][j]);
}
}
cout<<ans;
}
```
---
### LIS
----
LIS是經典的1D1D問題
要找出最大的遞增或遞減子序列
----
[題目](https://judge.tcirc.tw/ShowProblem?problemid=d078)
這題就是要找最大遞增子序列
----
假設序列是$a[1:n]$
定義$dp[i]$為到$i$為止的LIS
轉移式就是
$dp[i]=max(dp[j]+1 :j<i \ and \ a[j]<a[i])$
這樣直接做複雜度會是$O(n^2)$
但還有更快的方法
----
設$last[i]$為 __LIS長度為$i$的最小可能結尾__
$L$為目前LIS的長度
那就可以進行以下操作
```cpp=
last[0]=a[1];
L=1;
for(int i=2;i<=n;i++){
for(int j=0;j<L;j++){
if(last[j]>= a[i]){
last[j]=a[i];
if(j==L) L++;
break;
}
}
}
```
----
但這樣在找$last[j]>=a[i]$複雜度還是$O(n)$
整體還是$O(n^2)$
不過其實last是一個遞增序列
所以可以用二分搜來找
這樣整體複雜度就是$O(nlogn)$
----
code
```cpp=
int n,last[100005],L;
cin>>n;
for(int i=1;i<=n;i++){
int a;
cin>>a;
int it=lower_bound(last,last+len,a)-last;
last[it]=a;
if(it==len) len++;
}
cout<<len;
```
----
code2
```cpp=
int n,a;
vector<int> v;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a;
auto it=lower_bound(v.begin(),v.end(),a);
if(it==v.end()) v.push_back(a);
else *it=a;
}
cout<<v.size();
```
---
### 小技巧
----
拿個題目舉例
```
給一個二維陣列,長寬為n和m,由大於等於0的數字組成,代表有幾個蘋果,
之後給個座標,問上下左右共有幾個蘋果
輸入例:
3 3
1 2 2
1 2 3
0 0 0
1 2
輸出例:
5
```
----
這時有人會寫四個式子像以下程式
----
```cpp=
int n,m,a[105][105],x,y;
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
cin>>x>>y;
int ans=0;
ans+=a[x+1][y];
ans+=a[x-1][y];
ans+=a[x][y-1];
ans+=a[x][y+1];
cout<<ans;
```
----
這裡光是加四個方向就寫了四行
如果要附近八個方向或做更複雜的計算
程式碼就會變很長
所以就可以用以下方法
----
```cpp=
int n,m,a[105][105],x,y;
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
cin>>x>>y;
int ans=0,nx[4]={0,0,1,-1},ny[4]={1,-1,0,0};
for(int i=0;i<4;i++)
ans+=a[x+nx[i]][y+ny[i]];
cout<<ans;
```
----
大家可以用這方法去寫APCS的
[這題](https://zerojudge.tw/ShowProblem?problemid=m932)只是需要一點變化
---
### 結束 Ya
{"title":"dp動態規劃2","description":"","contributors":"[{\"id\":\"443d51bf-5a61-411d-b3cb-c3371649227c\",\"add\":4871,\"del\":278}]"}