状态机模型 dp

0x00 状态机介绍

百度百科

简单来说,状态机就是一个数学模型,不是一个实际的机器。它含有几个状态,还有几个函数(或者通俗一点叫“桥梁”),使得这几种状态可以在一定条件下实现相互转化。

是不是和动态规划的状态转移过程很像?

所以有一类型的动态规划题目,它的状态可以在一定的条件下相互转化,这时从状态机的角度来分析会比较好想。

0x01 例题

一. 01 两种状态

AcWing 1049. 大盗阿福

题目描述

街道上有 N 家店铺,按照顺序从 1N 排好,
i 家店铺的财产是 wi,大盗阿福如果偷了第 i 家店铺,则他不能偷相邻的店铺,否则会被抓起来。
帮助大盗阿福找出一种偷盗方案,使得获得的总财产最大。

思路

从一般的 dp 思路来思考:

f(i) 表示偷了前 i 家店铺能获得的最大财产,对于第 i 家店铺,可以选择偷或不偷,若偷,则第 i1 家店铺就不能偷则应该从 f(i2) 转移过来;不偷,则由 f(i1) 转移过来。

综上,状态转移方程为:

f(i)=max{f(i1),f(i2)+wi}

从状态机的角度思考:

根据题意,可以总结出两种状态:偷了上一家店铺,没偷上一家店铺。

状态 1 可以由状态 2 转移过来,但不能由它自己转移过来(因为不能偷两家相邻的店铺)。
状态 2 既可以由状态 1 转移过来,也可以由它自己转移过来。

画个图就是这样的:

f(i,0) 表示偷前 i 家店铺且不偷第 i 家店铺能获得的最大财产,f(i,1) 表示偷前 i 家店铺且偷第 i 家店铺能获得的最大财产,则状态转移方程为:

f(i,0)=max{f(i1,0),f(i1,1)}

f(i,1)=f(i1,0)+wi

发现每层状态只会由他的上一层状态得到,所以可以加滚动数组优化。

Code:


#include <iostream>
using namespace std;

int T;
int n, a;
int f[2][2];

int main() {
    scanf("%d", &T);
    while(T--) {
        scanf("%d", &n);
        f[0][0] = f[0][1] = 0;
        for(int i = 1; i <= n; i++) {
            scanf("%d", &a);
            f[i & 1][0] = max(f[i - 1 & 1][0], f[i - 1 & 1][1]);
            f[i & 1][1] = f[i - 1 & 1][0] + a;
        }
        printf("%d\n", max(f[n & 1][0], f[n & 1][1]));
    }
    return 0;
}

AcWing 1057. 股票买卖 IV

题目大意:

给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。
你最多可以完成 k 笔交易,求能获取的最大利润。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。一次买入卖出合为一笔交易

思路:

这道题用一般的方法分析就不太容易了,直接用状态机的思路分析。

不难总结出这道题有两种状态:持股 (1) 和不持股 (0)

操作可分为四种:

  1. 买入行为:k=0k=1
  2. 卖出行为:k=1k=0
  3. 持仓行为:k=1k=1
  4. 空仓行为:k=0k=0

画图就是这样的:

f(i,j,k) 表示:考虑前 i 天的股票,第 i 天的决策是 k,且完成的完整交易数为 j 的方案。

状态转移方程为:

f(i,j,0)=max{f(i1,j,0),f(i1,j,1)+ai}

f(i,j,1)=max{f(i1,j,1),dp(i1,j1,0)ai}

注意:因为一次买入卖出合为一笔交易,这里将买入算作开始一次交易,所以要消耗性次数,而卖出是一次交易的结束,不消耗次数。

同样可以加滚动数组优化。

Code:

#include <cstring>
#include <iostream>
using namespace std;
const int N = 100010, M = 110;

int n, k;
int a[N];
int dp[2][M][2];
int ans;

int main() {
    scanf("%d%d", &n, &k);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
    memset(dp, -0x3f, sizeof dp);
    dp[0][0][0] = dp[1][0][0] = 0;
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= min(i, k); j++) {
            dp[i & 1][j][0] = max(dp[i - 1 & 1][j][0], dp[i - 1 & 1][j][1] + a[i]);
            dp[i & 1][j][1] = max(dp[i - 1 & 1][j][1], dp[i - 1 & 1][j - 1][0] - a[i]);
            ans = max(ans, max(dp[i & 1][j][0], dp[i & 1][j][1]));
        }
    }
    printf("%d", ans);
    return 0;
}

P1352 没有上司的舞会

题目大意

给定一棵有 n 个节点的树,每个节点有一个权值 wi,现在要从这棵树上选出一些节点。若选择了一个节点,那么它的子节点就不能被选,问能获得的最大权值和。

思路

f(i) 为以 i 为根的子树能获得的最大权值和,那么很明显,我们应该先算儿子子树的 f 值,然后才能算 f(i),更新顺序为从下往上

但是如果我们这样设计状态会发现无法转移——你不知道当前儿子节点的 f 是选儿子得到的还是没选得到的,所以你也就无法确定节点 i 选还是不选。

根据状态机的思路,本题有两个状态:选当前节点 (1) 和不选当前节点 (0)

选当前节点的前提是不能选儿子,01
不选当前节点那么两种都能转移,10,00

f(i,j) 表示考虑以 i 为根的子树且 i 的策略为 j 能获得的最大权值和,则状态转移方程为:

f(i,0)=max{f(j,0),f(j,1)}

f(i,1)=f(j,0)+wi

其中 ji 的子节点。

然后就可以快乐地树上递归转移了。

Code:

#include <cstring>
#include <iostream>

using namespace std;

const int N = 6010;
typedef long long ll;
int n, ori;
int h[N], e[N], ne[N], idx;
int w[N], f[N][2]; //0 不选,1 选
bool have_father[N];
int root;

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dp(int u) {
    f[u][1] = w[u];
    for(int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        dp(j);
        f[u][0] += max(f[j][0], f[j][1]);
        f[u][1] += f[j][0];
    }
}

int main() {
    memset(h, -1, sizeof h);
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", &w[i]);
    int a, b;
    for(int i = 1; i < n; i++) {
        scanf("%d%d", &a, &b);
        add(b, a);
        have_father[a] = true;
    }

    for(int i = 1; i <= n; i++)
        if(!have_father[i]) {
            root = i;
            break;
        }

    dp(root);
    printf("%d", max(f[root][0], f[root][1]));
    return 0;
}

二. 多种状态

AcWing 1058. 股票买卖 V

题目大意:

给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。

注意:

  1. 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。一次买入卖出合为一笔交易
  2. 卖出股票后,无法在第二天买入股票(冷冻期为 1 天)。

求能获得的最大利润。

思路:

这道题的状态不再是简单的 01 状态。

但还是一样,可以归纳为 3 种状态:持股 (0)、冷冻期 (1)、未持股 (2)

操作可分为五种:

  1. 买入行为:20
  2. 卖出行为:01
  3. 持仓行为:00
  4. 空仓行为:22
  5. 冷冻期结束:12

画图来看就是这样:

f(i,j) 表示考虑前 i 天的股票且在第 i 天时是状态 j 的最大利润。

则状态转移方程为:

f(i,0)=max{f(i1,0),f(i1,2)ai}

f(i,1)=f(i1,0)+ai

f(i,2)=max{f(i1,1),f(i1,2)}

Code:

#include <cstring>
#include <iostream>

using namespace std;


const int N = 100010;

int n;
int a[N];
int dp[N][3];

int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
    memset(dp, -0x3f, sizeof dp);
    for(int i = 0; i <= n; i++) dp[i][2] = 0;
    for(int i = 1; i <= n; i++) {
        dp[i][0] = max(dp[i - 1][0], dp[i - 1][2] - a[i]);
        dp[i][1] = dp[i - 1][0] + a[i];
        dp[i][2] = max(dp[i - 1][2], dp[i - 1][1]);
    }
    printf("%d", max(dp[n][1], dp[n][2]));
    
    return 0;
}

AcWing 1052. 设计密码

题目大意

你现在需要设计一个密码 S,满足:

  1. S 的长度是 N
  2. S 只包含小写英文字母;
  3. S 不包含子串 T

请问共有多少种不同的密码满足要求?

由于答案会非常大,请输出答案模 109+7 的余数。

思路

考虑从前向后构造密码,所以可以将目前构造到第几位作为阶段来 dp。

模板串不能作为字串出现在密码中,考虑 KMP 的过程,可以将目前匹配到了模板串的第几位作为第二维。

f(i,j) 表示目前构造到 i 位且第 i 位匹配到模板串的第 j 位的密码数。

由题可知,不能匹配到第 |T| 位,所以答案应该为 0j<|T|{f(n,j)}

接下来考虑状态转移。

首先需要了解一下自动机的概念

这道题考虑一个状态能更新那些状态要好想很多。

假设我们现在已经构造好了前 i 位,要去构造第 i+1 位了。因为第 i+1 位可以放 26 种字母,而每种字母都会有不同的匹配的结果,所以共有 26 种状态。

然后枚举第 i+1 位的字母,对每个字母找到能匹配的最大后缀长度 δ(π(i),chi+1),即构造第 i+1 位后该密码能匹配到的模板串中的位置,也就找到了状态 j 应该更新的状态。

综上所述,状态转移方程为:

f(i+1,δ(π(j),ch))f(i,j)

Code:

#include <cstring>
#include <iostream>

using namespace std;

const int N = 55, mod = 1e9 + 7;

int n, len;
char s[N];
int dp[N][N];
int ne[N];

int main() {
    scanf("%d%s", &n, s + 1);
    len = strlen(s + 1);
	//预处理 ne 数组
    for(int i = 2, j = 0; i <= len; i++) {
        while(j && s[j + 1] != s[i]) j = ne[j];
        if(s[j + 1] == s[i]) j++;
        ne[i] = j;
    }
    dp[0][0] = 1;
    for(int i = 0; i < n; i++)
        for(int j = 0; j < len; j++)
            for(char k = 'a'; k <= 'z'; k++) {
		//计算应该更新的状态
                int tmp = j;
                while(tmp && s[tmp + 1] != k) tmp = ne[tmp];
                if(s[tmp + 1] == k) tmp++;
                if(tmp < len) dp[i + 1][tmp] = (dp[i + 1][tmp] + dp[i][j]) % mod;
            }
    int res = 0;
    for(int i = 0; i < len; i++) res = (res + dp[n][i]) % mod;
    printf("%d", res);
    return 0;
}
posted @   Brilliant11001  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示