動態規劃-狀態機模型專題(題目彙總)
狀態機模型
狀態機題面的顯著特點:描述過程而非結果,重點是描述狀態。
狀態機的入口全部初始化為0,非入口全初始化為無窮。
AcWing 1049. 大盜阿福
題意
給定長度為N的陣列,不能選擇相鄰的數,問可以求得的最大和?
題解
狀態機DP
狀態表示:
f[i] [0]:前i個數且不選當前數的最大和
f[i] [1]:前i個數且選當前數的最大和
狀態計算:
狀態劃分:最後一步
f[i] [0]:
- 0->0
- 1->0
f[i] [1]:
- 0 -> 1
狀態機的入口全部初始化為0,非入口全初始化為無窮。
#include <iostream> #include <algorithm> using namespace std; const int N = 100010; int n; int w[N], f[N][2]; int main() { int T; scanf("%d", &T); while (T -- ) { scanf("%d", &n); for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]); for (int i = 1; i <= n; i ++ ){ f[i][0] = max(f[i - 1][1], f[i - 1][0]); f[i][1] = f[i - 1][0] + w[i]; } printf("%d\n", max(f[n][0], f[n][1])); } return 0; }
滾動陣列優化
可以看出狀態只依賴於上一個狀態:
#include <iostream> #include <cstring> using namespace std; const int N = 1e5 + 10; int n; int w[N]; int f[2][2]; void solve() { memset(f, -0x3f, sizeof f); f[0][0] = 0; cin >> n; for (int i = 1; i <= n; ++ i) cin >> w[i]; for (int i = 1; i <= n; ++ i) { f[i & 1][0] = max(f[(i - 1) & 1][1], f[(i - 1) & 1][0]); f[i & 1][1] = f[(i - 1) & 1][0] + w[i]; } cout << max(f[n & 1][0], f[n & 1][1]) << endl; } int main() { int T = 1; cin >> T; while (T -- ) solve(); return 0; }
第二種思路
f[i]表示前 i 個店鋪的最大收益
狀態劃分:
-
不搶第 i個店鋪時的最大收益為f[i−1]
-
搶第 i 個店鋪時的最大收益位f[i−2]+w[i]
#include <iostream> #include <algorithm> using namespace std; const int N = 100010; int n; int w[N], f[N]; int main() { int T; scanf("%d", &T); while (T -- ) { scanf("%d", &n); for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]); int res = 0; f[1] = w[1]; for (int i = 2; i <= n; i ++ ){ f[i] = max(f[i - 2] + w[i], f[i - 1]); res = max(f[i], res); } printf("%d\n", res); } return 0; }
AcWing 1057. 股票買賣 IV
題意
給定一個長度為 N 的陣列w,陣列中的第 i個數字表示一個給定股票在第i天的價格。你可以在第i天買進,在第j天賣出,獲取利潤為w[j] - w[i](i <= j)。最多可以完成 k 筆交易所能獲取的最大利潤。
題解
狀態機DP
狀態表示:
f(i, j, 0)前i天完成了j次交易且手中沒有股票的最大利潤。
f(i, j, 1)前i天完成了j次交易且手中有股票的最大利潤。
狀態劃分:
線性DP
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 110, INF = 0x3f3f3f3f;
int n, m;
int w[N];
int f[N][M][2];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
memset(f, -0x3f, sizeof f);
for (int i = 0; i <= n; i ++ ) f[i][0][0] = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
f[i][j][0] = max(f[i - 1][j][0], f[i - 1][j][1] + w[i]);
f[i][j][1] = max(f[i - 1][j][1], f[i - 1][j - 1][0] - w[i]);
}
}
int res = 0;
for (int i = 0; i <= m; i ++ ) res = max(res, f[n][i][0]);
printf("%d\n", res);
return 0;
}
AcWing 1058. 股票買賣 V
題意
給定一個長度為 N 的陣列w,陣列中的第 i個數字表示一個給定股票在第i天的價格。你可以在第i天買進,在第j天賣出,獲取利潤為w[j] - w[i](i <= j),不能同時參與多筆交易。賣出股票後,你無法在第二天買入股票 (即冷凍期為1天),求所能獲取的最大利潤?
題解
f(i, 0)表示前i天,手裡有股票的最大利潤。
f(i, 1)表示前i天,手裡沒有股票第一天的最大利潤。
f(i, 2)表示前i天,手裡沒有股票第>= 2天的最大利潤。
狀態劃分:
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 110, INF = 0x3f3f3f3f;
int n, m;
int w[N];
int f[N][M][2];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
memset(f, -0x3f, sizeof f);
for (int i = 0; i <= n; i ++ ) f[i][0][0] = 0;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
f[i][j][0] = max(f[i - 1][j][0], f[i - 1][j][1] + w[i]);
f[i][j][1] = max(f[i - 1][j][1], f[i - 1][j - 1][0] - w[i]);
}
int res = 0;
for (int i = 0; i <= m; i ++ ) res = max(res, f[n][i][0]);
printf("%d\n", res);
return 0;
}
Acwing 1059. 股票買賣 VI
題意
給定一個長度為 N 的陣列,陣列中的第 i 個數字表示一個給定股票在第 i 天的價格,再給定一個非負整數 f,表示交易股票的手續費用。
題解
f(i, 0)表示前i天,手裡有股票的最大利潤。
f(i, 1)表示前i天,手裡沒有股票的最大利潤。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 110, INF = 0x3f3f3f3f;
int n, m;
int w[N];
int f[N][2];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
memset(f, -0x3f, sizeof f);
for (int i = 0; i <= n; i ++ ) f[i][0] = 0;
for(int i = 1; i <= n; i++){
f[i][0] = max(f[i - 1][0], f[i - 1][1] + w[i] - m);
f[i][1] = max(f[i - 1][1], f[i - 1][0] - w[i]);
}
printf("%d\n", f[n][0]);
return 0;
}
Acwing 1583. PAT 計數
題意
現在給定一個字串,請你求出字串中包含的
PAT
子序列的數量。
題解
f(i, j)表示前i個字元走到狀態j的所有路線的數量。
狀態j如下圖:0 1 2 3
f(i, j)來源:
- 當前字母不用:f(i - 1, j)
- 匹配當前字母:f(i - 1, j - 1),當s[i] == p[j]
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int MOD=1000000007;
const int N=100010,M=110;
int f[N][M],n;
char p[]=" PAT";
char s[N];
int main()
{
cin>>s+1;
f[0][0]=1;
n=strlen(s+1);
for(int i=1;i<=n;i++)
{
for(int j=0;j<=3;j++)
{
f[i][j]=f[i-1][j];
if(s[i]==p[j])
f[i][j]=(f[i][j]+f[i-1][j-1])%MOD;
}
}
cout<<f[n][3];
return 0;
}
Acwing 3195. 有趣的數
題意
我們把一個數稱為有趣的,當且僅當:
- 它的數字只包含 0,1,2,3,且這四個數字都出現過至少一次。
- 所有的 0 都出現在所有的 1 之前,而所有的 2 都出現在所有的 3之前。
- 最高位數字不為 0。
請計算恰好有 n 位的有趣的數的個數。
4 位:2013、2031 和 2301
題解
f(i, j)表示填充了i位密碼對應狀態j的方案數
狀態設計:
-
使用了2,剩下 0,1,3
-
使用了2,0,剩下 1,3
-
使用了2,3,剩下0,1
-
使用了2,0,1,剩下3
-
使用了2,0,3, 剩下1
-
使用了全部
狀態劃分:
劃分依據:最後一位填充什麼
f(i, 0) = 1, 第i位只有填2一種選擇
f(i, 1) = f(i - 1, 0) (第i位填2)+ f(i - 1, 1) * 2 (第i位填2/0)
f(i, 2) = f(i - 1, 0) (第i位填3) + f(i - 1, 2) (第i位只能填2)
f(i, 3) = f(i - 1, 1) (第i位填1) + f(i - 1, 3) * 2 (第i位填2/1)
f(i, 4) = f(i - 1, 1) (第i位填3) + f(i - 1, 2) (第i位填0) + f(i - 1, 4) * 2 (第i位填0/3)
f(i, 5) = f(i - 1, 3)(第i位填3) + f(i - 1, 4)( 第i位填1) + f(i - 1, 5) * 2(第i位填1/3)
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1010, MOD = 1000000007;
LL f[N][7];
int main(){
LL n;
cin>>n;
for(LL i=1;i<=n;i++)
{
LL j = i-1;
f[i][0] = 1;
f[i][1] = (f[j][0] + f[j][1] * 2) % MOD;
f[i][2] = (f[j][0] + f[j][2]) % MOD;
f[i][3] = (f[j][1] + f[j][3] * 2) % MOD;
f[i][4] = (f[j][1] + f[j][2] + f[j][4] * 2) % MOD;
f[i][5] = (f[j][3] + f[j][4] + f[j][5] * 2) % MOD;
}
cout<<f[n][5]<<endl;
return 0;
}
AcWing 1052. 設計密碼
題意
你現在需要設計一個密碼 S,S 需要滿足:
- S 的長度是 N;
- S 只包含小寫英文字母;
- S 不包含子串 T;
題解
參考:https://www.acwing.com/solution/content/28022/
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=55,MOD=1e9+7;
int f[N][N],ne[N];
char str[N];//子串
int main()
{
int n,m;
cin>>n>>str+1;
m=strlen(str+1);
for(int i=2,j=0;i<=m;i++)//求出ne陣列(kmp模板)
{
while(j&&str[j+1]!=str[i]) j=ne[j];
if(str[j+1]==str[i]) j++;
ne[i]=j;
}
f[0][0]=1;//已經匹配了0位,且匹配的子串的位置是0時的方案數為1;(初始化)
for(int i=0;i<n;i++)//列舉密碼位
for(int j=0;j<m;j++)//把第i位密碼匹配到的子串位置都列舉一遍
//j表示第i位密碼匹配到的位置,因為不能包含子串,所以不能匹配到m這個位置
for(char k='a';k<='z';k++)//把第i+1所有可能的字母都列舉一遍
{
//匹配過程:尋找當第i+1的位置是k時,並且密碼已經生成了第i位,匹配的子串的位置是j時,能跳到哪個位置
int u=j;
while(u&&str[u+1]!=k) u=ne[u];
if(str[u+1]==k) u++;
if(u<m) f[i+1][u]=(f[i+1][u]+f[i][j])%MOD;
//因為是從f[i][j](i+1的位置為k)跳到f[i+1][u]這個位置,所以f[i+1][u]=f[i+1][u]+f[i][j];
/*
注:可能存在重邊,因為j不同但ne[j]是相同的,並且k是相同的,所以此時
f[i][j1]和f[i][j2]跳到的位置是一樣的(k相同,ne[j1]=ne[j2])
*/
}
int res=0;
for(int i=0;i<m;i++) res=(res+f[n][i])%MOD;
//將所有的方案數加起來即為總方案數
printf("%d",res);
return 0;
}