目录
一、原理阐述
之前所讲的动态规划问题,都是在不同“点”之间转移。状态机模型主要是将“点”扩展成几个状态,在状态之间互相转移。具体内容在以下例题中体现。
二、例题
(1)大盗阿福
我们先尝试用过去dp的思路分析:
1.用f(i)表示在前i个店铺里面偷窃,能获得的最大现金数量
2.则状态划分为偷不偷第i间店铺。则转移方程为:
仔细一看发现,这样思考有很大问题,因为前i个店铺里偷窃不代表选择了第i间店铺,因此f[i-1]不能代表不偷第i间店铺的最大值,同样的f[i-2]+w[i]也不能代表偷第i间店铺的最大值。
综上,如果这么考虑问题的话,是没办法进行状态转移的,同时发现一条线索:我们需要更新状态时,会遇到“在前i间店铺选择,不选择第i间店铺”和“在前i间店铺选择,选择第i间店铺”这两种情况来更新下一步状态。
因此我们考虑状态机模型,在“第i间店铺”这一个点上,拓展出“选择第i间”,“不选择第i间”两种状态。
画出状态机的状态转移图:
即选择最后一个店铺,对下一个i,只能转移去不选择最后一个店铺。不选择最后一个店铺,可以转移两种状态。
则状态转移方程为(0表示不选择,1表示选择):
定义一下状态机的入口,即(意义是选择第0个是非法的,不选择0个是状态机的入口)
代码如下:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N =100010;
int f[N][2];
int n;
int w[N];
int main()
{
int T;
cin>>T;
while(T--)
{
cin>>n;
for(int i=1;i<=n;i++) cin>>w[i];
//f[0][0]=0;
//f[0][1]=-0x3f3f3f3f; //定义入口
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];
}
cout<<max(f[n][0],f[n][1])<<endl;
}
return 0;
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4123495/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
(2)股票买卖4(二维)
在第i天时,容易发现有两种状态:当前手上有股票,当前手上无股票。
画出状态转移图:
同时题目要求,最多进行k次交易,因此需要记录进行了几笔交易,体现在代码中为买入时直接算开始了第j笔交易,卖出时无变化;
状态表示为:在第i天,恰好正在进行第j次交易,手上有无股票 的收益最大值
状态转移方程:
代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N =1e5+10,M=110;
int f[N][M][2]; //第i天 恰好正在进行第j笔交易 手上有无股票
int n,m;
int w[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>w[i];
memset(f,0xcf,sizeof f);
for(int i=0;i<=n;i++) f[i][0][0]=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
f[i][j][0]=max(f[i-1][j][1]+w[i],f[i-1][j][0]);
f[i][j][1]=max(f[i-1][j-1][0]-w[i],f[i-1][j][1]);
}
int res=0;
for(int i=1;i<=m;i++) res=max(res,f[n][i][0]);
cout<<res<<endl;
return 0;
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4123824/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
(3)股票买卖5
相较于上题,本题不限制交易次数,但是要求卖出股票的第二天不能进行买入操作。
那么我们就舍弃记录进行了几次交易的状态,增加一个记录是否在冷冻期的状态。
为了简洁代码,是否在冷冻期可以和手里有无股票合并表示。
状态表示:0表示有货 1表示无货在冷冻期 2表示无货且不再冷冻期。
状态机转移图:
由于第一天可以购买股票,因此入口只有f[0,2],另两个状态不合法,置为-INF。
状态转移方程:
最后答案在fn1和fn2中产生。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
//0表示有货 1表示无货在冷冻期 2表示无货且不再冷冻期
const int N =100010;
int f[N][3];
int n;
int w[N];
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>w[i];
f[0][2]=0;
f[0][1]=f[0][0]=-1e9;
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]);
}
cout<<max(f[n][1],f[n][2]);//两个出口
return 0;
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4123931/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
(4)设计密码(与KMP结合)
先复习一下KMP字符串匹配算法:
对于字符串匹配过程,我们用i指示匹配串下标,j指示模板串下标(事先声明,为了方便起见,模板串和匹配串的下标从1开始)。
i从1开始,j从0开始。每次我们比较j的下一个位置是否和i相同,如果相同则j++,如果不同则j倒回到某一个位置。使得p[1~j]=p[i-j+1,i]。如果j走到n,则代表匹配成功。
就本题而言,我们可以一步步填写S,并且用子串去匹配S,如果j走到子串长度m了,则代表匹配成功,状态不合法,走到其他任何一个位置的下标都是可以允许的。
因此我们状态拓展为,填了i个字母时,j所处的下标。共有m+1中状态,状态转移由新填的字母来转移。
接着来考虑状态机模型的细节:入口是选了0个字母,j的初始下标是0,由于本题dp属性是“数量”,因此f[0][0]=1。答案是填了n个字母,j的下标是0~n。
代码如下:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
//状态机状态为kmp中j的位置,有0~m共m+1种
//枚举26个字母,若j跳到m表示匹配成功-不合法
const int N =55,mod=1e9+7;
int n,m;
int f[N][N];
char str[N];
int ne[N];
int main()
{
cin>>n>>str+1;
m=strlen(str+1);
for(int i=2,j=0;i<=m;i++)
{
while(j&&str[i]!=str[j+1]) j=ne[j];
if(str[i]==str[j+1]) j++;
ne[i]=j;
}
f[0][0]=1;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
//实测这里写<min(i+1,m)也能ac,,写<m可能是为了方便,因为i较小的时候,j>i+1的情况也不会算到
{
for(char k='a';k<='z';k++)
{
int u=j;
while(u&&k!=str[u+1]) u=ne[u];
if(k==str[u+1]) u++;
if(u<m) f[i+1][u]=(f[i+1][u]+f[i][j])%mod;
}
}
int res=0;
for(int i=0;i<m;i++) res=(res+f[n][i])%mod;
cout<<res;
return 0;
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4145629/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
声明:内容来自acwing算法提高课,文章为本人的个人学习理解