状态机模型
1.大盗阿福(线性dp|状态机|空间优化O(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。
解题思路
这道题可以直接用线性dp来做,也能用自动机来做
1.线性dp:
状态表示
:f[i]
集合
:表示选择前i家店铺行窃所能获得的最大现金数量
属性
:MAX
状态计算
:f[i]=max(f[i-2]+w[i],f[i-1])
解释:考虑前i家店铺,对于第i家店铺,我们可以选择行窃或者不行窃,如果行窃第i家店铺,那么根据题意,我们就不能行窃第i-1家,那么最大值就是前i-2家行窃的最大值加上第i家,如果不行窃第i家店铺,那么第i-1家可以行窃,最大价值就是前i-1家行窃的最大值f[i-1],二者取最大就行,最后的结果就是f[n]。
2.状态机
状态表示
:f[i][0],f[i][1]
集合
:所有走了i步,当前位于状态j的走法,j取0或者1
属性
:MAX
状态计算
:f[i][0]=(f[i-1][1],f[i-1][0]), f[i][1]=f[i-1][0]+w[i]
解释:用0表示不行窃,1表示行窃,那么走的方式就可以用一个状态图来表示,如下:
有三种情况0->1 0->0 1->0,每次我们都只考虑最后一个店铺选与不选,当我们不选最后一个店铺,那么状态就转化成0,而转化成0有两种方式,0->0 1->0,根据状态表示的属性可以得到状态转移方程就是f[i][0]=max(f[i-1][0],f[i-1][1]),同理,当我们选择最后一个店铺,那么状态就转化成1,而转化成1只有一种方式,0->1,根据状态表示的属性可以得到状态转移方程f[i][1]=f[i-1][0]+w[i],最后的最大值应该是max(f[n][0],f[n][1])
这就结束了??no no no 😃
空间也能压缩的哦
从O(N)到O(1)
我们可以发现,第i家店铺行窃或者不行窃,都只跟第i-1家或者第i-2家有关,所以我们可以只记录这两个值!!
代码实现
线性dp:
#include<iostream>
using namespace std;
const int N=100010;
int w[N],f[N];
int main()
{
int t,n;
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++) cin>>w[i];
for(int i=1;i<=n;i++) f[i]=max(f[i-2]+w[i],f[i-1]);
printf("%d\n",f[n]);
}
return 0;
}
状态机
#include<iostream>
#include<cstring>
using namespace std;
const int N=100010,NIF=0x3f3f3f3f;
int w[N],f[N][2];
int n,t;
int main()
{
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++) cin>>w[i];
f[0][0]=0,f[0][1]=-NIF;//初始化,前0件状态为0,就是没有行窃,最大价值为0,前0家状态为1是不可能的 没有店铺状态怎么能是行窃呢?莫非..
for(int i=1;i<=n;i++){
f[i][0]=max(f[i-1][0],f[i-1][1]);
f[i][1]=f[i-1][0]+w[i];
}
printf("%d\n",max(f[n][0],f[n][1]));
}
return 0;
}
空间压缩
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int w[N];
int main()
{
int yd=0,byd=0;//第一家 第二家
int n,t;
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++) cin>>w[i];
byd=w[1];//如果只有一家店铺 偷它!
yd=max(w[1],w[2]);//如果只有两家店铺 偷现金多的那个 :)
for(int i=3;i<=n;i++){
int tmp=yd;
yd=max(yd,byd+w[i]);
byd=tmp;
}
cout<<yd<<endl;
}
return 0;
}
2.股票买卖Ⅳ
题目描述
给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润,你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。一次买入卖出合为一笔交易。
输入格式
第一行包含整数 N 和 k,表示数组的长度以及你可以完成的最大交易数量。
第二行包含 N 个不超过 10000 的正整数,表示完整的数组。
输出格式
输出一个整数,表示最大利润。
数据范围
1≤N≤105,
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.
解题思路
状态机用来表示两种状态,用1表示手中有股票,0表示手中无股票,转换如下图:
状态表示
:f[i][j][0] f[i][j][1]
集合
:f[i][j][0] 表示从前i件股票中选,正在进行第j次交易且手中无货的状态,f[i][j][1] 表示从前i件股票选,正在进行第j次交易且手中有货的状态。
属性
:MAX
状态计算
: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])
解释:有四种转换 先看转换为有货的两个状态:从有货到有货
:也就是说我们没有第 i 次我们没有进行任何交易,所以交易花费 0,状态表示为为f[i][j][1]->f[i-1][j][1],从无货到有货
,也就是第i次我们买入了股票,花费为w[i],状态表示为:f[i][j][0]=f[i-1][j-1]-w[i]。然后再看转换为无货的两种状态:从无货到无货
,也就是我们没有进行任何交易,所以交易花费0,状态表示为f[i][j][0]->f[i-1][j][0],从有货到无货
:也就是说我们将股票卖出了,状态表示为f[i][j][1]->f[i-1][j-1][0]+w[i]
最后的答案是枚举所有可能的交易f[n][i][0],(i从0~k),然后取最大,因为我们不确定交易多少次是最大值,而且最后一定是手中无货才能取最大值
代码实现
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=100010,M=110;
int w[N];
int f[N][M][2];
int main()
{
int n,k;
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
memset(f,-0x3f,sizeof f);//初始状态肯定是手中无货,f[i][0][1]一定是不可能的
for(int i=0;i<=n;i++) f[i][0][0]=0;//前i个股票 ,0次交易 手中一定无货,价值为0
for(int i=1;i<=n;i++){
for(int j=1;j<=k;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 j=0;j<=k;j++) res=max(res,f[n][j][0]);
printf("%d\n",res);
return 0;
}
3.股票买卖Ⅴ
题目描述
给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
输入格式
第一行包含整数 N,表示数组长度。
第二行包含 N 个不超过 10000 的正整数,表示完整的数组。
输出格式
输出一个整数,表示最大利润。
数据范围
1≤N≤105
输入样例:
5
1 2 3 0 2
输出样例:
3
样例解释
对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出],第一笔交易可得利润 2-1 = 1,第二笔交易可得利润 2-0 = 2,共得利润 1+2 = 3。
解题思路
这题有三个状态,其实就是将上一题无货的状态拆分成两个状态,无货第一天和无货大于等于2天
状态表示
:f[i][j]
集合
:表示考虑到第i个股票,状态为j的利润最大值,j可以取0(有货),1(无货第一天),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])
解释,这次的状态机有三种状态,如下图:
我们只考虑最后一次交易:
①如果最后一次交易是手中有货即f[i][0]
,那么它的最大值有两个来源,也就是状态机里有两个转换可以到这个状态:手中有货到手中有货,也就是交易花费为0,状态表示为f[i-1][0]
,手中无货大于等于2天到手中有货,那么一定是买入股票,花费为 w[i],状态表示为f[i-1][2]-w[i]
两者取最大
②如果最后一次交易是手中无货第一天即f[i][1]
,那么它的最大值只有一个来源,就是从手中有货到手中无货第一天,此时股票一定卖出,收益w[i],状态表示为f[i-1][0]+w[i]
③如果最后一次交易是手中无货第n天(n>=2)即f[i][2]
,那么它的最大值有两个来源,也就是状态机有两个转换可以到这个状态:手中无货第一天到手中无货第二天,状态表示为f[i-1][1]
,手中无货第n天到手中无货第n天,状态表示为f[i-1][2]
,二者取最大即可
最后的最大值是在max(f[n][1],f[n][2])
,因为有两个出口
代码实现
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=100010;
int w[N];
int f[N][3];//0表示手中有货 1表示手中无货第一天 2表示手中无货天数大于等于2
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>w[i];
f[0][0]=-0x3f3f3f3f;
for(int i=1;i<=n;i++){
f[i][0]=max(f[i-1][2]-w[i],f[i-1][0]);
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;
}
还有一个比较难的(我觉得)状态机的题 设计密码大家有兴趣可以去做 我看了一个多小时 理解不了 所以肯定讲不清楚 时间不多了 只能先放弃