动态规划-状态机模型专题(题目汇总)

动态规划-状态机模型专题(题目汇总)

状态机模型

状态机题面的显著特点:描述过程而非结果。

状态机的入口全部初始化为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次交易且手中有股票的最大利润。

状态划分:

cb8092d74acad480814865d74231b98.png

线性DP

0bcc00fac54e40e4bf43d4080f6d88b.png

#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天的最大利润。

状态划分:

image-20211006212604969

#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

image-20211006232358390

f(i, j)来源:

  1. 当前字母不用:f(i - 1, j)
  2. 匹配当前字母: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. 有趣的数

题意

我们把一个数称为有趣的,当且仅当:

  1. 它的数字只包含 0,1,2,3,且这四个数字都出现过至少一次。
  2. 所有的 0 都出现在所有的 1 之前,而所有的 2 都出现在所有的 3之前。
  3. 最高位数字不为 0。

请计算恰好有 n 位的有趣的数的个数。

4 位:2013、2031 和 2301

题解

f(i, j)表示填充了i位密码对应状态j的方案数

状态设计:

  1. 使用了2,剩下 0,1,3

  2. 使用了2,0,剩下 1,3

  3. 使用了2,3,剩下0,1

  4. 使用了2,0,1,剩下3

  5. 使用了2,0,3, 剩下1

  6. 使用了全部

状态划分:

划分依据:最后一位填充什么

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/

7FDF0E458B0E28FD2BA8FB09F9497B8F.jpg

QQ浏览器截图20210101073445.png

#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;
}
posted @ 2021-10-07 16:37  pxlsdz  阅读(115)  评论(0编辑  收藏  举报