状态机模型
状态机代表一系列有序的事件,我们通过状态机能够将一个复杂的状态拓展为几个简单过程
状态机是一种另类的状态表示方式,实际上是我们将每一个状态拓展成一个过程
任何一个方案都能唯一对应一个状态机
简单的说,我们可以通过状态机将一个复杂、混沌的状态细分成几个清晰的状态
状态机在任意时刻的状态大多为
0
或1
状态机需要考虑入口和出口
大盗阿福
阿福是一名经验丰富的大盗。趁着月黑风高,阿福打算今晚洗劫一条街上的店铺。
这条街上一共有 \(n\) 家店铺,每家店中都有一些现金。
阿福事先调查得知,只有当他同时洗劫了两家相邻的店铺时,街上的报警系统才会启动,然后警察就会蜂拥而至。
作为一向谨慎作案的大盗,阿福不愿意冒着被警察追捕的风险行窃。
他想知道,在不惊动警察的情况下,他今晚最多可以得到多少现金?
\(1≤T≤50,\)
\(1≤n≤10^5\)
题解:线性DP——状态机模型
第一种非线性\(dp\):
状态表示:\(f[i]\)代表只从前\(i\)个店铺中偷窃,能够获得的最大价值
状态属性:\(MAX\)
状态计算:
- 选择不偷窃第\(i\)个店铺:\(f[i-1]\)
- 选择偷窃第\(i\)个店铺,那我们一定无法偷窃第\(i-1\)个店铺:\(f[i-2] + w[i]\)
\[f[i] = max(f[i-1],f[i-2] + w[i]) \]
状态初始:\(f[0] = 0,f[1] = w[1]\)
答案呈现:\(f[n]\)
第二种线性\(dp\)(状态机模型):
状态表示:
\(f[i][0/1]\)代表只从前\(i\)个店铺中选,且\(0\)代表选择第\(i\)家店铺,\(1\)代表不选第\(i\)家店铺所能获得的最大价值
状态属性: \(MAX\)
状态计算:按照路径来划分集合,若选择某店铺,将其状态表示为\(1\),若不选某个店铺,将其状态表示为\(0\)
- 选择第\(i\)个店铺,那么我们肯定不选择第\(i-1\)家店铺,即只有一条边从状态\(0\)通向状态\(1\),即\(f[i-1][0]+w[i]\)
- 不选择第\(i\)个店铺,那么第\(i-1\)家店铺可以选择也可以不选择,即有两条路径可以从其他状态到状态\(0\),即\(max(f[i-1][0],f[i-1][1])\)
\[f[i][1] = f[i-1][0] + w[i] \\ f[i][0] = max(f[i-1][0],f[i-1][1]) \]
状态初始:\(f[1][0] = 0,f[1][1] = w[1]\)
状态机入口:\(1\),状态机出口:\(0\) 或 \(1\)
答案呈现:\(max(f[n][0],f[n][1])\)
//第一种dp
const int N = 2e5 + 10, M = 4e5 + 10;
int n;
int f[N];
int w[N];
void solve()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> w[i];
for (int i = 0; i <= n; ++i)
f[i] = 0;
f[0] = 0;
f[1] = w[1];
for (int i = 2; i <= n; ++i)
f[i] = max(f[i - 1], f[i - 2] + w[i]);
cout << f[n] << endl;
}
//第二种dp
const int N = 2e5 + 10, M = 4e5 + 10;
int n;
int w[N];
int f[N][2];
void solve()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> w[i], f[i][1] = f[i][0] = 0;
for (int i = 1; i <= n; ++i)
{
f[i][1] = max(f[i][1], f[i - 1][0] + w[i]);
f[i][0] = max(f[i - 1][1], f[i - 1][0]);
}
cout << max(f[n][0], f[n][1]) << endl;
}
股票买卖 IV
给定一个长度为 \(n\) 的数组,数组中的第 \(i\) 个数字表示一个给定股票在第 \(i\) 天的价格。
设计一个算法来计算你所能获取的最大利润,你最多可以完成 \(k\) 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
一次买入卖出合为一笔交易
\(1≤n≤10^5\)
\(1≤k≤100\)
题解:线性\(dp\)——状态机模型 \(O(nk)\)
- 状态表示
\(f[i][j][0/1]\)代表只在前\(i\)天中选择,且已经完成\(j\)笔交易,且\(1\)代表在第\(i\)天手中有股票,\(0\)代表第\(i\)天手中没有股票的最大收益
状态属性:\(MAX\)
状态计算:
- 如果第\(i\)天手中有股票:\(f[i][j][1] = max(f[i-1][j][1],f[i-1][j][0] - w[i])\)
- 如果第\(i\)天手中无股票:\(f[i][j][0] = max(f[i-1][j][0],f[i-1][j-1][1] + w[i])\)
- 状态优化:
因为是线性的,所以容易发现第一维可以优化掉,倒序遍历交易数量(第二维)
状态初始:\(f[i][0][0] = 0 / f[0][0] = 0\)
答案呈现:\(max\sum (f[n][j][0],f[n][j][1])\)
const int N = 2e5 + 10, M = 1e2 + 10;
int n, m;
int w[N];
int f[M][2];
// f[i][j][0/1]代表只在前i天中选择,且已经完成j笔交易,且1代表在第i天手中有股票,0代表第i天手中没有股票的最大收益
void solve()
{
cin >> n >> m;
for (int i = 1; i <= n; ++i)
cin >> w[i];
memset(f, -0x3f, sizeof f);
f[0][0] = 0;
for (int i = 1; i <= n; ++i)
for (int j = m; j >= 0; --j)
{
f[j][1] = max(f[j][1], f[j][0] - w[i]);
if (j >= 1)
f[j][0] = max(f[j][0], f[j - 1][1] + w[i]);
}
int ans = -INF;
for (int j = 0; j <= m; ++j)
ans = max({ans, f[j][0], f[j][1]});
cout << ans << endl;
}
股票买卖Ⅴ
给定一个长度为 \(n\) 的数组,数组中的第 \(i\) 个数字表示一个给定股票在第 \(i\) 天的价格。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 \(1\) 天)。
\(1 \leq n \leq 10^5\)
题解:线性\(dp\)——状态机模型
- 状态表示:
\(f[i][0/1/2]\):代表只在前\(i\)天中选,且\(0\)代表第\(i\)天手中有股票,1代表第\(i\)天是卖出股票后的第一天,2代表第\(i\)天卖出股票后的第\(k\)天(\(k>=2\))能够获得的最大利润
状态属性:\(MAX\)
状态计算:
\(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])\)
状态机出口:\(1/2\)
状态机入口:\(2\)
状态初始:\(f[0][2] = 0,f[i][j] = -INF\)
答案呈现:\(max(f[n][1],f[n][2])\)
const int N = 2e5 + 10, M = 1e2 + 10;
int n;
int w[N];
int f[N][3];
void solve()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> w[i];
for (int i = 0; i <= n; ++i)
f[i][1] = f[i][0] = -INF;
f[0][2] = 0;
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]);
}
cout << max(f[n][1], f[n][2]) << endl;
}
设计密码
你现在需要设计一个密码 \(S\),\(S\) 需要满足:
- \(S\) 的长度是 \(n\);
- \(S\) 只包含小写英文字母;
- \(S\) 不包含子串 \(T\);
例如:\(abc\) 和 \(abcde\) 是 \(abcde\) 的子串,\(abd\) 不是 \(abcde\) 的子串。
请问共有多少种不同的密码满足要求?
由于答案会非常大,请输出答案模 \(10^9+7\) 的余数。
\(1 \leq n \leq 50\)
题解:\(KMP\) + 状态机模型 \(O(26n^3)\)
- 状态表示:
\(f[i][j]\):代表已经生成了\(i\)位密码,且第\(i\)位密码已经匹配到子串中位置为\(j\),即第\(i + 1\)位密码正在和子串中\(j+1\)位进行匹配的方案数
状态属性:数量
状态转移:
因为字符串\(S\)中只存在小写字母,所以S的位置\(i + 1\)处的字符只有\(26\)种可能性
我们不妨枚举位置\(i + 1\)会出现的所有小写字母\(k\)
如果第\(i+1\)位的字母k不等于\(T[j + 1]\),我们总可以利用\(KMP\)的\(next\)数组找到一个位置\(u\)使得\(k=T[u+1]\)
我们令\(u:=u + 1\),字符串\(T\)的长度为\(m\),因为\(S\)中不能含有\(T\),所以只有\(u < m\)的时候才能转移
状态转移方程:\(f[i+1][u] = (f[i+1][u] + f[i][j])\ \ \% \ \ mod \ \wedge(u < m)\)
时间复杂度:\(O(26n)\)
状态初始:\(f[0][0] = 1\)
答案呈现:\(\sum f[n][i]\)
const int N = 50 + 10, M = 4e5 + 10;
int n;
int ne[N];
int f[N][N];
void solve()
{
cin >> n;
string t;
cin >> t;
int m = t.length();
t = " " + t;
ne[1] = 0;
for (int i = 2, j = 0; i <= m; ++i)
{
while (j && t[i] != t[j + 1])
j = ne[j];
if (t[i] == t[j + 1])
j++;
ne[i] = j;
}
f[0][0] = 1;
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
for (char k = 'a'; k <= 'z'; ++k)
{
int u = j;
while (u && t[u + 1] != k)
u = ne[u];
if (t[u + 1] == k)
u++;
if (u < m)
f[i + 1][u] = (f[i + 1][u] + f[i][j]) % mod;
}
int ans = 0;
for (int j = 0; j < m; ++j)
ans = (ans + f[n][j]) % mod;
cout << ans << endl;
}