学习笔记:状态机模型

概念

状态机模型,有一点不同于我们平常意义的状态机,这里指的是每一个子问题会有多种状态可以选择的一种模型,比如股票,可以在某一时刻选择买入或者卖出。

做法

这种类型的问题一般没有什么特殊的做法。但是有一个技巧,因为子问题有多种状态,所以可以用 f i , k f_{i,k} fi,k表示第 i i i个问题的第 k k k个状态。这样两个状态之间的关系就会简单明了一些。

例题

AcWing 1049

法一

这个题就是两个之间只能选一个或者不选。因为有两种状态,而且状态之间的关系不明显,考虑把状态分开观察。首先,拆成 0 , 1 0,1 0,1两个状态, 0 0 0表示不抢, 1 1 1表示抢。首先 f i , 0 f_{i,0} fi,0,那么前面的就可以选择抢或者不抢, f i , 0 = max ⁡ ( f i − 1 , 1 , f i − 1 , 0 ) f_{i,0}=\max(f_{i-1,1},f_{i-1,0}) fi,0=max(fi1,1,fi1,0)。那么 f i , 1 f_{i,1} fi,1呢?首先上一个不能抢,然后这个抢了就有 a i a_i ai的收益, f i − 1 , 1 = f i − 1 , 0 + a i f_{i-1,1}=f_{i-1,0}+a_i fi1,1=fi1,0+ai

#include<bits/stdc++.h>
using namespace std;
const int NN=100004;
int a[NN],f[NN][5];
int main()
{
	int t;
	scanf("%d",&t);
	while(t--)
	{
		int n;
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
			scanf("%d",&a[i]);
		f[1][1]=a[1];
		for(int i=2;i<=n;i++)
		{
			f[i][0]=max(f[i-1][0],f[i-1][1]);
		    f[i][1]=f[i-1][0]+a[i];
		}
		printf("%d\n",max(f[n][0],f[n][1]));
	}
	return 0;
}

法二

考虑状态合并。假设 f i f_i fi就是选择了抢还是不抢的最优方案。那么 f i f_i fi有可能是 f i , 0 f_{i,0} fi,0 f i , 1 f_{i,1} fi,1。首先考虑第一种,不抢。所以我们不用担心 f i − 1 f_{i-1} fi1是用的 f i , 0 f_{i,0} fi,0还是 f i , 1 f_{i,1} fi,1,而且 f i − 1 f_{i-1} fi1使用的是两个的最大值,相当于前面的 f i , 0 = max ⁡ ( f i − 1 , 1 , f i − 1 , 0 ) f_{i,0}=\max(f_{i-1,1},f_{i-1,0}) fi,0=max(fi1,1,fi1,0),则 f i = f i − 1 f_i=f_{i-1} fi=fi1。那么如果选择了抢呢?首先,前面一个有可能使用的是抢,所以我们不能用它来迭代。发现 f i − 2 f_{i-2} fi2的选择对于 f i f_i fi的选择是没有影响的,则考虑 f i − 2 f_{i-2} fi2是不是覆盖了全部的状态。首先,原来是 f i − 1 , 1 = f i − 1 , 0 + a i f_{i-1,1}=f_{i-1,0}+a_i fi1,1=fi1,0+ai,那么看看 f i − 1 , 0 f_{i-1,0} fi1,0是怎么迭代的。原来 f i − 1 , 0 = m a x ( f i − 2 , 1 , f i − 2 , 0 ) f_{i-1,0}=max(f_{i-2,1},f_{i-2,0}) fi1,0=max(fi2,1,fi2,0),刚好是新定义的 f i − 2 f_{i-2} fi2,所以新的选择抢的状态转移方程就是 f i − 2 + a i f_{i-2}+a_i fi2+ai,因为要选择两个方案的最大值,则状态转移方程 f i = max ⁡ ( f i − 1 , f i − 2 + a i ) f_i=\max(f_{i-1},f_{i-2}+a_i) fi=max(fi1,fi2+ai)

#include<bits/stdc++.h>
using namespace std;
const int NN=100004;
int a[NN],f[NN];
int main()
{
	int t;
	scanf("%d",&t);
	while(t--)
	{
		int n;
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
			scanf("%d",&a[i]);
		f[1]=a[1];
		for(int i=2;i<=n;i++)
			f[i]=max(f[i-1],f[i-2]+a[i]);
		printf("%d\n",f[n]);
	}
	return 0;
}

AcWing 1057

这个题每个时刻都有两种状态:手中没有股票和没股票。因为本题可以买多股股票,设 j j j为交易了几组(一次买入卖出为一组)股票的情况。设为 f i , j , 0 f_{i,j,0} fi,j,0 f i , j , 1 f_{i,j,1} fi,j,1表示 1... i 1...i 1...i时刻最多买 j j j组股票的两种状态。先考虑不买,则可以选择继续不买或者买一个,买一个就要花费当天的股价 ( a i ) (a_i) (ai),则 f i , j , 0 = max ⁡ ( f i − 1 , j , 0 , f i − 1 , j , 1 − a i ) f_{i,j,0}=\max(f_{i-1,j,0},f_{i-1,j,1}-a_i) fi,j,0=max(fi1,j,0,fi1,j,1ai)。考虑手中有股票,则不能购买股票,选择继续等待或者卖出去,卖出去会获得 a i a_i ai且使用了一次交易,则 f i , j , 1 = max ⁡ ( f i − 1 , j , 1 , f i − 1 , j − 1 , 0 + a i ) f_{i,j,1}=\max(f_{i-1,j,1},f_{i-1,j-1,0}+a_i) fi,j,1=max(fi1,j,1,fi1,j1,0+ai)。然后我们发现, i i i只需要 i − 1 i-1 i1的状态,且 j j j只会用更小的,则可以考虑滚动数组。 j j j只会用更小的,可以从大到小枚举 j j j以防用到的 f f f被提前更新。最后输出最大买 m m m个股票且手中没有持有股票的值(因为持有股票在之前卖了肯定更划算)。需要注意的是,要把第 0 0 0天购买的状态设为负无穷,因为第 0 0 0天没办法买。

#include<bits/stdc++.h>
using namespace std;
int w[100004],f[104][2];
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&w[i]);
    for(int i=1;i<=m;i++)
        f[i][1]=-1e9;
    for(int i=1;i<=n;i++)
        for(int j=m;j>=1;j--)
        {
            f[j][0]=max(f[j][0],f[j][1]+w[i]);
            f[j][1]=max(f[j][1],f[j-1][0]-w[i]);
        }
    printf("%d",f[m][0]);
    return 0;
}

AcWing 1058

这个题目可以把上一题的思路中卖出的情况拆成两种:刚卖出、出了冷冻期。则设 f i , 0 , f i , 1 , f i , 2 f_{i,0},f_{i,1},f_{i,2} fi,0,fi,1,fi,2分别表示手中持有股票、今天卖出和过了冷冻期的方案。首先考虑买入,买入必须过了冷冻期,所以不能用刚卖出的方案更新买入, f i , 0 = max ⁡ ( f i − 1 , 0 , f i − 1 , 2 − w i ) f_{i,0}=\max(f_{i-1,0},f_{i-1,2}-w_i) fi,0=max(fi1,0,fi1,2wi)。然后考虑刚卖出的方案,只能选择卖出, f i , 1 = f i − 1 , 0 + w i f_{i,1}=f_{i-1,0}+w_i fi,1=fi1,0+wi。接着考虑出了冷冻期,则前一天要么已经出了冷冻期,要么刚卖出,则 f i , 2 = max ⁡ ( f i − 1 , 1 , f i − 1 , 2 ) f_{i,2}=\max(f_{i-1,1},f_{i-1,2}) fi,2=max(fi1,1,fi1,2)。最后考虑边界,第 0 0 0天不能买入或者是卖出,那么就不能用它们更新,设为负无穷即可。其实这个题用的全部都是 i − 1 i-1 i1的状态,所以可以设两个数组,一个存 i − 1 i-1 i1,一个存 i i i来滚动数组。但是本题数据范围不大,滚动数组细节太多还是少用为妙。

#include<bits/stdc++.h>
using namespace std;
const int NN=100004;
int w[NN],f[NN][3];
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&w[i]);
    f[0][0]=f[0][1]=-1e9;
    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][2],f[i-1][1]);
    }
    printf("%d",max(f[n][1],f[n][2]));
    return 0;
}

AcWing 1052

这个题目要求一个字符串不包含另一个字符串,我们称其为串 b b b,设长度为 m m m。肯定需要匹配两个字符串,则可以考虑 K M P KMP KMP。首先 n e ne ne数组是可以先求出来的,因为题目给出了用于匹配的串。现在可以考虑,每次加一个数,如果匹配的长度就不可以用该方案了。于是,我们就可以按匹配长度设计状态。设 f i , j f_{i,j} fi,j为已经设计了 i i i位可行的密码,最后有 j j j位匹配串 b b b的方案数。只有最后 j < m j<m j<m才是可行的方案,所以最终答案是 ∑ j = 0 m − 1 f n , j \displaystyle\sum_{j=0}^{m-1}f_{n,j} j=0m1fn,j。考虑状态转移,首先因为 n n n很小,枚举 i i i j j j,也可以枚举现在加上哪个字母。首先之前匹配的指的是串 b b b的前缀(如果从中间匹配就不可能出现包含的情况, K M P KMP KMP也不会从中间匹配),所以匹配以前匹配的串加上当前枚举的新加的字母组成的串。如果匹配长度大于 m m m直接跳过,否则就可以用原串加上该字母得到一个新串,所以 f i , n e w j + = f i − 1 , j f_{i,newj}+=f_{i-1,j} fi,newj+=fi1,j

#include<bits/stdc++.h>
using namespace std;
const int NN=54,P=1e9+7;
int ne[NN],f[NN][NN];
char str[54];
int main()
{
    int n,m;
    scanf("%d%s",&n,str+1);
    m=strlen(str+1);
    int j=0;
    for(int i=2;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=1;i<=n;i++)
        for(int j=0;j<m;j++)
            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][u]+=f[i-1][j])%=P;
            }
    int res=0;
    for(int i=0;i<m;i++)
        (res+=f[n][i])%=P;
    printf("%d",res);
    return 0;
}

AcWing 1053

这个题目和上一题非常像,就是匹配的串变成了多个。多个字符串匹配,可以用 A C AC AC自动机匹配,本题把上一题的 K M P KMP KMP改成 A C AC AC自动机就行了。目前我还没有写 A C AC AC自动机的学习笔记,可以先在 C S D N CSDN CSDN上搜一下。

#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int ne[NN],tr[NN][4],f[NN][NN],cnt;
char s[NN];
bool num[NN];
int get(char c)
{
    if(c=='A')
        return 0;
    if(c=='T')
        return 1;
    if(c=='G')
        return 2;
    return 3;
}
void insert()
{
    int u=0,len=strlen(s+1);
    for(int i=1;i<=len;i++)
        if(tr[u][get(s[i])])
            u=tr[u][get(s[i])];
        else
            u=tr[u][get(s[i])]=++cnt;
    num[u]=true;
}
void build()
{
    queue<int>q;
    for(int i=0;i<4;i++)
        if(tr[0][i])
            q.push(tr[0][i]);
    while(q.size())
    {
        int t=q.front();
        q.pop();
        for(int i=0;i<4;i++)
        {
            int u=tr[t][i];
            if(!u)
                tr[t][i]=tr[ne[t]][i];
            else
            {
                ne[u]=tr[ne[t]][i];
                q.push(u);
                num[u]|=num[ne[u]];
            }
        }
    }
}
int main()
{
    int kase=0,n;
    while(scanf("%d",&n)&&n)
    {
        memset(num,false,sizeof(num));
        memset(tr,0,sizeof(tr));
        memset(ne,0,sizeof(ne));
        cnt=0;
        for(int i=1;i<=n;i++)
        {
            scanf("%s",s+1);
            insert();
        }
        build();
        scanf("%s",s+1);
        int len=strlen(s+1);
        memset(f,0x3f,sizeof(f));
        f[0][0]=0;
        for(int i=1;i<=len;i++)
            for(int j=0;j<=cnt;j++)
                for(int k=0;k<4;k++)
                {
                    int u=tr[j][k],t=get(s[i])!=k;
                    if(!num[u])
                        f[i][u]=min(f[i][u],f[i-1][j]+t);
                }
        int ans=1e9;
        for(int i=0;i<=cnt;i++)
            ans=min(ans,f[len][i]);
        if(ans==1e9)
            ans=-1;
        printf("Case %d: %d\n",++kase,ans);
    }
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值