动态规划-状态机模型专题(题目汇总)
状态机模型
状态机题面的显著特点:描述过程而非结果。
状态机的入口全部初始化为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)
前
天完成了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, INF = 0x3f3f3f3f;
int n;
int w[N];
int f[N][3];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
f[0][0] = f[1][0] = -INF;
for (int i = 1; i <= n; i ++ )
{
f[i][0] = max(f[i - 1][0], f[i - 1][2] - w[i]);
f[i][1] = f[i - 1][0] + w[i];
f[i][2] = max(f[i - 1][1], f[i - 1][2]);
}
printf("%d\n", max(f[n][1], f[n][2]));
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;
}