1、什么是状态机模型
可以参考这个文章OI wiki
状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型
状态机描述的是事物的不同状态之间转化的关系。举一个门的例子。门有两种状态,开着和关着。其中开着可以继续保持开着的状态,也可以转换到关着的状态;关着可以继续保持关着的状态,也可以转换到开着状态。
这样就把门的运行规则抽象成了这样一个模型。这种模型可以很清楚的表示出来不同状态下,门可以做哪些操作,可以转移到什么状态。
2、例题
1. 大盗阿福
1. 题目描述
阿福是一名经验丰富的大盗。趁着月黑风高,阿福打算今晚洗劫一条街上的店铺。
这条街上一共有 N 家店铺,每家店中都有一些现金。
阿福事先调查得知,只有当他同时洗劫了两家相邻的店铺时,街上的报警系统才会启动,然后警察就会蜂拥而至。
作为一向谨慎作案的大盗,阿福不愿意冒着被警察追捕的风险行窃。
他想知道,在不惊动警察的情况下,他今晚最多可以得到多少现金?
输入格式
输入的第一行是一个整数 T,表示一共有 T 组数据。
接下来的每组数据,第一行是一个整数 N ,表示一共有 N 家店铺。
第二行是 N 个被空格分开的正整数,表示每一家店铺中的现金数量。
每家店铺中的现金数量均不超过1000。
输出格式
对于每组数据,输出一行。
该行包含一个整数,表示阿福在不惊动警察的情况下可以得到的现金数量。
数据范围
1≤T≤50,
1≤N≤105
输入样例:
2
3
1 8 2
4
10 7 6 14
输出样例:
8
24
样例解释
对于第一组样例,阿福选择第2家店铺行窃,获得的现金数量为8。
对于第二组样例,阿福选择第1和4家店铺行窃,获得的现金数量为10+14=24。
2. 分析
按照题目要求,我们确定有两个状态
- 抢了状态
代表抢劫了第i家店铺,根据题意,不可以再抢第i+1个店铺,也就是其的状态不可以指向自己 - 没抢状态
代表没抢第i家店铺,根据题意,其可以继续不抢或者抢第i+1个店铺
那么状态机如下所示
i每走一步,要走状态机的一条边。每一步走一条边。题目里任何一个抢劫的方案都能对应一个长度为n的走法,任何一个长度为n的状态机走法,也都对应一个抢劫方案。
我们用0代表不抢劫的状态,用1代表抢劫的状态。
根据状态机,f[i,0]代表从1到i个店铺中选抢劫对象,且不抢劫第i个店铺。f[i,1]同理。f[i,1]可以由f[i - 1,0]和f[i - 1,1]转移过来,而对于f[i,1]只可以由f[i - 1,0]转移过来。
由属性max可以得到状态转移方程
f
[
i
,
0
]
=
m
a
x
(
f
[
i
−
1
,
0
]
,
f
[
i
−
1
,
1
]
)
f[i,0] = max(f[i - 1,0],f[i - 1,1])
f[i,0]=max(f[i−1,0],f[i−1,1])
f
[
i
,
1
]
=
f
[
i
−
1
,
1
]
+
w
[
i
]
f[i,1] = f[i - 1,1] + w[i]
f[i,1]=f[i−1,1]+w[i]
w[i]代表抢劫第i家店铺的收益。
我自己是这样理解的。如果没有不能连着抢两家的限制,那么每个店铺是否被抢的决定都是独立的。也就是i-1的任何状态都可以转移给i。但是加了这个限制之后,每个店铺的状态就不是任何的前一个状态都能转移的了,只有符合状态机描述的转移是合法的。也就是第i个商店是否被抢是有不同的转移方式的。状态机就是描述这个状态转移的模型。
最后还有个入口的问题。我们初始的时候一定是什么也没抢。也就是虚构一个第0家商铺,并且不抢这个商铺。为了这个目的,我们将f[0][1]设为负收益。以便不能从这个状态向后转移。
3. 代码
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int N = 100010;
int a[N],f[N][2];
int main()
{
int T;
cin >> T;
while(T --)
{
int n;
cin >> n;
//设置入口条件
f[0][1] = -0x3f3f3f3f;
for(int i = 1; i <= n; i ++)
{
int w;
cin >> w;
f[i][0] = max(f[i - 1][0],f[i - 1][1]);
f[i][1] = f[i - 1][0] + w;
}
cout << max(f[n][0],f[n][1]) << endl;
}
}
2. 股票买卖IV
1. 题目描述
给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润,你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。一次买入卖出合为一笔交易。
输入格式
第一行包含整数 N 和 k,表示数组的长度以及你可以完成的最大交易数量。
第二行包含 N 个不超过 10000 的正整数,表示完整的数组。
输出格式
输出一个整数,表示最大利润。
数据范围
1≤N≤10^5,
1≤k≤100
输入样例1:
3 2
2 4 1
输出样例1:
2
输入样例2:
6 2
3 2 6 5 0 3
输出样例2:
7
样例解释
样例1:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
样例2:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。共计利润 4+3 = 7.
2. 分析
与上题的店铺有多种状态类似,我们看这里每一笔交易也有多种状态。一开始是手中无货,然后买入,然后是手中有货,然后卖出。这样是一笔交易。
用状态机描述如上图所示。状态机主要说明了状态转移的相关信息。下面我们用0代表空仓状态,1代表持仓状态。
- f[i,j,0]从i-1的持仓卖出来,代表完成了第j笔交易(从空仓到持仓到空仓),所以这部分应该表示为f[i-1,j,1] + w[i](w[i]代表该股票第i天的价格)
- f[i,j,0]从i-1的空仓来,应表示为f[i - 1,j,0]
- f[i,j,1]从i-1的持仓来,应表示为f[i - 1,j,1]
- f[i,j,1]从i-1的空仓来,代表开启了一笔新的交易,买入了股票,也就是j是一笔新的交易(j代表当前正在进行第j笔交易),应表示为f[i - 1,j - 1,0] - w[i]。
所以有
f
[
i
,
j
,
0
]
=
m
a
x
(
f
[
i
−
1
,
j
,
0
]
,
f
[
i
−
1
,
j
,
1
]
+
w
[
i
]
)
f[i,j,0] = max(f[i - 1,j,0],f[i - 1,j,1] + w[i])
f[i,j,0]=max(f[i−1,j,0],f[i−1,j,1]+w[i])
f
[
i
,
j
,
1
]
=
m
a
x
(
f
[
i
−
1
,
j
,
1
]
,
f
[
i
−
1
,
j
−
1
,
0
]
−
w
[
i
]
)
f[i,j,1]=max(f[i - 1,j,1],f[i - 1,j - 1,0] - w[i])
f[i,j,1]=max(f[i−1,j,1],f[i−1,j−1,0]−w[i])
对于入口条件,也就是进行第0笔交易的情况,因为要从空仓状态进入状态机,除了f[i][0][0]其他的状态都是非法的。所以要给其他状态赋负权益。
3. 代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 100010, M = 110;
int f[N][M][2];
int main()
{
int n,k;
cin >> n >> k;
memset(f, -0x3f, sizeof f);
for (int i = 0; i <= n; i ++ ) f[i][0][0] = 0;
for(int i = 1; i <= n; i ++)
{
int w;
cin >> w;
for(int j = 1; j <= k; j ++)
{
f[i][j][0] = max(f[i - 1][j][0],f[i - 1][j][1] + w);
f[i][j][1] = max(f[i - 1][j - 1][0] - w,f[i - 1][j][1]);
}
}
int res = 0;
for(int i = 0; i <= k; i ++)
{
res = max(res,max(f[n][i][0],f[n][i][1]));
}
cout << res;
}
3. 股票买卖V
1. 题目
给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
输入格式
第一行包含整数 N,表示数组长度。
第二行包含 N 个不超过 10000 的正整数,表示完整的数组。
输出格式
输出一个整数,表示最大利润。
数据范围
1≤N≤10^5
输入样例:
5
1 2 3 0 2
输出样例:
3
样例解释
对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出],第一笔交易可得利润 2-1 = 1,第二笔交易可得利润 2-0 = 2,共得利润 1+2 = 3。
2. 分析
根据本题目的情况,每笔交易被分成了三种状态,第一种是持仓状态,第二种是手中无货的第一天(不可以交易),第三种是手中无货的第二天(可以任意交易)。
按照题目要求我们画出如下的状态机。
我们用2代表空仓第二天,1代表空仓第一天,0代表持仓状态。
根据状态机
- f[i,2]可以从空仓第一天和空仓第二天来,所以可以表示为max(f[i-1,1],f[i-1,2])
- f[i,1]只能从持仓来,所以表示为f[i - 1,0] + w[i]
- f[i,0]可以从持仓和空仓第二天来,所以可以表示为max(f[i - 1,0],f[i - 1,2] - w[i])
因为第一天交易不需要冷却时间,所以应当从第0天的空仓第二天转移过去才符合要求。所以需要给f[0][0]和f[0][1]赋予负权益。
3. 代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100010;
int f[N][3];
int main()
{
int n;
cin >> n;
f[0][0] = -0x3f3f3f3f;
f[0][1] = -0x3f3f3f3f;
f[0][2] = 0;
for(int i = 1; i <= n; i ++)
{
int w;
cin >> w;
f[i][0] = max(f[i - 1][0],f[i - 1][2] - w);
f[i][1] = f[i - 1][0] + w;
f[i][2] = max(f[i - 1][1],f[i - 1][2]);
}
cout << max(f[n][1],f[n][2]);
}
4. 设计密码(状态机+KMP)
1. 题目
你现在需要设计一个密码 S,S 需要满足:
S 的长度是 N;
S 只包含小写英文字母;
S 不包含子串 T;
例如:abc 和 abcde 是 abcde 的子串,abd 不是 abcde 的子串。
请问共有多少种不同的密码满足要求?
由于答案会非常大,请输出答案模 10^9+7 的余数。
输入格式
第一行输入整数N,表示密码的长度。
第二行输入字符串T,T中只包含小写字母。
输出格式
输出一个正整数,表示总方案数模 10^9+7 后的结果。
数据范围
1≤N≤50,
1≤|T|≤N,|T|是T的长度。
输入样例1:
2
a
输出样例1:
625
输入样例2:
4
cbc
输出样例2:
456924
2. 分析
对于一个n位的密码,他有
2
6
n
26^n
26n中情况(每个位置都可以填一个小写字母),肯定不能暴力枚举。我们使用dp来处理。
对于母串去匹配一个字符串t(设其长度为m)的过程中有m个状态,分别是匹配到第一个字符,匹配到前两个字符,匹配到前i个字符,匹配到前m个字符。对于状态之间的转移,在母串中的i与子串中的j+1相等时,j会向后一位转移,如果不匹配的话,j就会按照next数组一直向前转移( kmp算法可参考 KMP)。对于不同的t,其状态机模型不同。
举一个具体的例子来说
对于子串t = ababc来说,其next数组为[0,0,1,2,0]
对于f[i,3]来说
- 如果母串第i个字符为b,状态机将会从3走到4
- 如果母串第i个字符不为b,他将按照next数组走到1的位置进行对比,如果成功则走到1状态。否则就继续按照next数组进行移动,这里我们知道,就到达0,此时就走到0状态。
3. 代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 55, mod = 1e9 + 7;
int ne[N],f[N][N];
char p[N];
int main()
{
int n,m;
cin >> n >> p + 1;
m = strlen(p + 1);
//预处理next数组
for(int i = 2, j = 0; i <= m; i ++)
{
while(j && p[i] != p[j + 1]) j = ne[j];
if(p[i] == p[j + 1]) j ++;
ne[i] = j;
}
f[0][0] = 1;
//第i个位置
for(int i = 0; i < n; i ++)
{
//后缀最长匹配的长度
for(int j = 0; j < m; j ++)
{
//枚举k的所有情况
for(char k = 'a'; k <= 'z'; k ++)
{
//ptr实际上就是在寻找下一个状态我要奔赴状态机的哪一个状态
int ptr = j;
while(ptr && k != p[ptr + 1]) ptr = ne[ptr];
if(k == p[ptr + 1]) ptr ++;
//确定之后,就把f[i][j]的数量加到f[i+1][ptr]上
f[i + 1][ptr] = (f[i + 1][ptr] + f[i][j]) % mod;
}
}
}
int res = 0;
for(int i = 0; i < m; i ++) res = (res + f[n][i]) % mod;
cout << res;
}