状态机模型-dp

状态机介绍

百度百科:

状态机由状态寄存器和组合逻辑电路构成,能够根据控制信号按照预先设定的状态进行状态转移,是协调相关信号动作、完成特定操作的控制中心。有限状态机简写为 \(\text{FSM}\)\(\text{Finite State Machine}\)),主要分为 \(2\) 大类:

  • 第一类,若输出只和状态有关而与输入无关,则称为 \(\text{Moore}\) 状态机。

  • 第二类,输出不仅和状态有关而且和输入有关系,则称为 \(\text{Mealy}\) 状态机。


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

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

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

\(\text{OpenJ_NOI - CH0206-8462}\)

给定一个长度为 \(n\) 的序列 \(a_i\),任意相邻的两个 \(a_i,a_{i+1}\) 不能同时选,求选的最大总和。

\(1 \le T \le 50\)\(1 \le n \le 10^5\)\(1 \le a_i \le 1000\)


从一般 \(\text{dp}\) 思路来想:

设状态 \(f_i\) 表示选了前 \(i\) 个数能获得的最大值,对于第 \(i\) 个数,可以选或者不选,如果选,则第 \(i-1\) 个数不能选,于是可以从 \(f_{i-2}\) 转移过来;如果不选,则从 \(f_{i-1}\) 转移过来:

\[f_i = \max(f_{i-1}, f_{i-2} + a_i) \]

从状态机的角度想:

根据题意,可以总结出两种状态:选了上一个数,没选上一个数。

  • 状态 \(1\) 可以由状态 \(2\) 转移过来,但不能由它自己转移过来(因为不能选两个相邻的数)。
  • 状态 \(2\) 既可以由状态 \(1\) 转移过来,也可以由它自己转移过来。

\(f_{i,0/1}\) 表示已经处理了前 \(i\) 个数,不选/选第 \(i\) 个数,则状态转移方程为:

\[\begin{align} f_{i,0} &= \max(f_{i-1,0}, f_{i-1,1}) \\ f_{i,1} &= f_{i-1,0} + a_i \end{align} \]

每层状态只由上一层状态得到,可以加滚动数组优化空间复杂度为 \(O(1)\)

#include<iostream>
#include<cstdio>
using namespace std;

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long T, n, dp[2];

int main() {
	T = read();
	while(T --) {
		n = read(), dp[0] = dp[1] = 0;
		for(int i = 1; i <= n; i ++) {
			long long x = read(), y = dp[0];
			dp[0] = max(dp[0], dp[1]);
			dp[1] = y + x;
		}
		cout << max(dp[0], dp[1]) << "\n";
	}
	return 0;
}

\(\text{acwing-1057}\)

给定一个长度为 \(n\) 的序列,\(a_i\) 表示一个给定股票在第 \(i\) 天的价格。

你最多可以完成 \(k\) 笔交易,求能获取的最大利润。

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

一次买入卖出合为一笔交易

\(1 \le n \le 10^5\)\(1 \le k \le 100\)\(1 \le a_i \le 10^4\)


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

显然这道题有两种状态:持股 \((1)\) 和不持股 \((0)\)。因此操作可分为四种:

  • 买入:\(p = 0 \to p = 1\),持有金额 \(-a_i\)
  • 卖出:\(p = 1 \to p = 0\),持有金额 \(+a_i\)
  • 持仓:\(p = 1 \to p = 1\)
  • 空仓:\(p = 0 \to p = 0\)

\(f_{i,j,p}\) 表示处理完前 \(i\) 天的股票,第 \(i\) 天决策是 \(p\),完成的完整交易数为 \(j\) 的最大利润。

状态转移方程就是这样:

\[\begin{align} f_{i,j,0} &= \max(f_{i-1,j,0}, f_{i-1,j,1}+a_i) \\ f_{i,j,1} &= \max(f_{i-1,j,1}, f_{i-1,j-1,0}-a_i) \end{align} \]

注意:因为一次买入卖出合为一笔交易,因此买入记为一次交易消耗次数,而卖出不消耗次数。

\[\text{Answer} = \max_{j=1}^k f_{n,j,0} \]

滚动数组可以优化 \(i\) 这一维,空间复杂度为 \(O(k)\),答案为 \(f_{n\%2,j,0}\)

注意:记得初始化 \(f\) 数组为 \(- \infty\)\(f_{1,0,0}=f_{0,0,0}=0\)

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define MAXN 100005
#define MAXM 105

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, k, a[MAXN], dp[2][MAXM][2], ans;

int main() {
	n = read(), k = read();
	for(int i = 1; i <= n; i ++) a[i] = read();
	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(1ll * 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]);
	for(int i = 1; i <= k; i ++) ans = max(ans, dp[n & 1][i][0]);
	cout << ans << "\n";
	return 0;
}

\(\text{acwing-1058}\)

给定一个长度为 \(n\) 的序列,\(a_i\) 表示一个给定股票在第 \(i\) 天的价格。

注意:

  • 不能同时参与多笔交易(必须在再次购买前出售掉之前的股票)。

  • 一次买入卖出合为一笔交易

  • 卖出股票后,无法在第二天买入股票(冷冻期为 \(1\) 天)

求能获取的最大利润。

\(1 \le n \le 10^5\)\(1 \le a_i \le 10^4\)


显然这道题不是简单的 \(01\) 状态。但可归纳出 \(3\) 种状态:持股 \((0)\)、冷冻期 \((1)\)、未持股 \((2)\)

操作可分为 \(5\) 种:

  • 买入:\(2 \to 0\),持有金额 \(-a_i\)
  • 卖出:\(0 \to 1\),持有金额 \(+a_i\)
  • 持仓:\(0 \to 0\)
  • 空仓:\(2 \to 2\)
  • 冷冻期结束:\(1 \to 2\)

\(f_{i,p}\) 表示处理完前 \(i\) 天的股票,第 \(i\) 天状态是 \(p\) 的最大利润。状态转移方程为:

\[\begin{align} f_{i,0} &= \max(f_{i-1,0}, f_{i-1,2} - a_i) \\ f_{i,1} &= f_{i-1,0} + a_i \\ f_{i,2} &= \max(f_{i-1,1}, f_{i-1,2}) \end{align} \]

答案为 \(\max(f_{n,1}, f_{n,2})\),因为最后一天不能持股。

滚动数组可以优化空间复杂度至 \(O(1)\),答案为 \(\max(f_{n\%2,1}, f_{n\%2,2})\)

注意:记得初始化 \(f\) 数组为 \(- \infty\)\(f_{0,2}=f_{1,2}=0\)

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, dp[2][3];

int main() {
	n = read();
	memset(dp, -0x3f, sizeof dp);
	dp[1][2] = dp[0][2] = 0;
	for(int i = 1; i <= n; i ++) {
		long long x = read();
		dp[i & 1][0] = max(dp[i - 1 & 1][0], dp[i - 1 & 1][2] - x);
		dp[i & 1][1] = dp[i - 1 & 1][0] + x;
		dp[i & 1][2] = max(dp[i - 1 & 1][1], dp[i - 1 & 1][2]);
	}
	cout << max(dp[n & 1][1], dp[n & 1][2]) << "\n";
	return 0;
}
posted @ 2025-11-13 19:02  So_noSlack  阅读(5)  评论(0)    收藏  举报