概念
状态机模型,有一点不同于我们平常意义的状态机,这里指的是每一个子问题会有多种状态可以选择的一种模型,比如股票,可以在某一时刻选择买入或者卖出。
做法
这种类型的问题一般没有什么特殊的做法。但是有一个技巧,因为子问题有多种状态,所以可以用 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(fi−1,1,fi−1,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 fi−1,1=fi−1,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} fi−1是用的 f i , 0 f_{i,0} fi,0还是 f i , 1 f_{i,1} fi,1,而且 f i − 1 f_{i-1} fi−1使用的是两个的最大值,相当于前面的 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(fi−1,1,fi−1,0),则 f i = f i − 1 f_i=f_{i-1} fi=fi−1。那么如果选择了抢呢?首先,前面一个有可能使用的是抢,所以我们不能用它来迭代。发现 f i − 2 f_{i-2} fi−2的选择对于 f i f_i fi的选择是没有影响的,则考虑 f i − 2 f_{i-2} fi−2是不是覆盖了全部的状态。首先,原来是 f i − 1 , 1 = f i − 1 , 0 + a i f_{i-1,1}=f_{i-1,0}+a_i fi−1,1=fi−1,0+ai,那么看看 f i − 1 , 0 f_{i-1,0} fi−1,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}) fi−1,0=max(fi−2,1,fi−2,0),刚好是新定义的 f i − 2 f_{i-2} fi−2,所以新的选择抢的状态转移方程就是 f i − 2 + a i f_{i-2}+a_i fi−2+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(fi−1,fi−2+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(fi−1,j,0,fi−1,j,1−ai)。考虑手中有股票,则不能购买股票,选择继续等待或者卖出去,卖出去会获得 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(fi−1,j,1,fi−1,j−1,0+ai)。然后我们发现, i i i只需要 i − 1 i-1 i−1的状态,且 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(fi−1,0,fi−1,2−wi)。然后考虑刚卖出的方案,只能选择卖出, f i , 1 = f i − 1 , 0 + w i f_{i,1}=f_{i-1,0}+w_i fi,1=fi−1,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(fi−1,1,fi−1,2)。最后考虑边界,第 0 0 0天不能买入或者是卖出,那么就不能用它们更新,设为负无穷即可。其实这个题用的全部都是 i − 1 i-1 i−1的状态,所以可以设两个数组,一个存 i − 1 i-1 i−1,一个存 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=0∑m−1fn,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+=fi−1,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;
}