学习笔记:状态机模型

上讲习题

AcWing 1024

要求剩余的空间尽量少,那么就是使用的空间尽量多。所以就变成了每个物品有重量,求最多装多少的问题。发现这个题目就是一个背包问题,但是没有价值,因为是求最多装多少,很像背包问题求最大价值,则可以把体积同时当成价值,这样就是求的最多装多少了。每个物品只有一个,这个题就是一个 01 01 01背包。最后用总的减去最多装的就是最少剩余的。

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

AcWing 1022

这个题目就是一个有二维费用的背包问题。精灵球的数量是第一维费用,皮卡丘的体力是第二维费用。因为皮卡丘的体力等于零时无法收服使其体力等于 0 0 0的怪物,所以第二维为 m m m f f f都不计算。最后皮卡丘剩余的最大体力就找一个是最小的 k k k使得 f n , k = f n , m − 1 f_{n,k}=f_{n,m-1} fn,k=fn,m1。每个怪物只能打一次, 01 01 01背包。

#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int f[NN][NN];
int main()
{
    
    
    int n,m,num;
	scanf("%d%d%d",&n,&m,&num);
	for(int i=1;i<=num;i++)
	{
    
    
	    int x,y;
		scanf("%d%d",&x,&y);
	    for(int j=n;j>=x;j--)
	        for(int k=m-1;k>=y;k--)
	            f[j][k]=max(f[j][k],f[j-x][k-y]+1);
	}
	printf("%d ",f[n][m-1]);
	int k=m-1;
	while(k&&f[n][k-1]==f[n][m-1])
	    k--;
	printf("%d",m-k);
	return 0;
}

AcWing 1023

这个题目就是用几个固定的物品,每种物品可以用多个,要求装满 n n n的方案数。求方案数可以参考上讲AcWing 278。因为是要装满 n n n,所以方案数最开始只有 f 0 = 1 f_0=1 f0=1,可以什么都不装。

#include<bits/stdc++.h>
using namespace std;
int a[10]={
    
    0,10,20,50,100};
long long f[100004];
int main()
{
    
    
	int n;
	scanf("%d",&n);
	f[0]=1;
	for(int i=1;i<=4;i++)
		for(int j=a[i];j<=n;j++)
			f[j]+=f[j-a[i]];
	printf("%d",f[n]);
	return 0;
}

AcWing 1021

这个题和上一题唯一的区别就在于物品的重量(面值)不一定了。注意本题的数据范围并不大,但是方案数较多,有可能爆 i n t int int

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

AcWing 1020

这个题就是有二维费用的背包问题。注意,本题完全都是反着的,背包的容量变成了至少,价值(本题为重量)也变成了最少。有两种方法:一是全部变成负数计算,但是细节处理较多。第二种办法是每次的状态变成了加上物品的重量(本题为氧、氮),然后加上价值时要求 min ⁡ \min min。因为可以用多个,所以本应是从小到大枚举,但是状态变成了加上重量,所以从大到小才是本题的完全背包的做法。

#include<bits/stdc++.h>
using namespace std;
const int NN=104,MM=1004;
int f[NN][NN],a[MM],b[MM],c[MM];
int main()
{
    
    
	int n,m,k;
	scanf("%d%d%d",&n,&m,&k);
	memset(f,0x3f,sizeof(f));
	f[0][0]=0;
	for(int i=1;i<=k;i++)
		scanf("%d%d%d",&a[i],&b[i],&c[i]);
	for(int i=1;i<=k;i++)
		for(int j=n;j>=0;j--)
			for(int x=m;x>=0;x--)
			{
    
    
				int q=min(j+a[i],n),p=min(x+b[i],m);
				f[q][p]=min(f[q][p],f[j][x]+c[i]);
			}
	printf("%d",f[n][m]);
	return 0;
}

AcWing 426

这个题目就是 01 01 01背包的板子。它要求价值是要花的钱乘上重要度那就这样算就行了。

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

AcWing 487

这个题目有依赖关系,可以按照上一讲的分组背包的做法来做。但是我们发现,这个的依赖关系很简单,没有更深层的依赖关系,而且最多只会有两个附件,则可以暴力枚举每一种选择状态,代码就简单地多了。

#include<bits/stdc++.h>
using namespace std;
const int NN=64;
int v[NN][5],w[NN][5],f[32004];
int main()
{
    
    
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
    
    
		int u,p,q;
		scanf("%d%d%d",&u,&p,&q);
		if(!q)
		{
    
    
			v[i][0]=u;
			w[i][0]=u*p;
		}
		else if(!v[q][1])
		{
    
    
			v[q][1]=u;
			w[q][1]=u*p;
		}
		else
		{
    
    
			v[q][2]=u;
			w[q][2]=u*p;
		}
	}
	for(int i=1;i<=m;i++)
		for(int j=n;j>=0;j--)
		{
    
    
			if(j>=v[i][0])
				f[j]=max(f[j],f[j-v[i][0]]+w[i][0]);
			if(j>=v[i][0]+v[i][1])
				f[j]=max(f[j],f[j-v[i][0]-v[i][1]]+w[i][0]+w[i][1]);
			if(j>=v[i][0]+v[i][2])
				f[j]=max(f[j],f[j-v[i][0]-v[i][2]]+w[i][0]+w[i][2]);
			if(j>=v[i][0]+v[i][1]+v[i][2])
				f[j]=max(f[j],f[j-v[i][0]-v[i][1]-v[i][2]]+w[i][0]+w[i][1]+w[i][2]);
		}
	printf("%d",f[n]);
	return 0;
}

概念

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

做法

这种类型的问题一般没有什么特殊的做法。但是有一个技巧,因为子问题有多种状态,所以可以用 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 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 f n , j \displaystyle\sum_{j=0}^mf_{n,j} j=0mfn,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 1058

AcWing 1053

解析和代码在下一篇博客——状态压缩 d p dp dp给出

猜你喜欢

转载自blog.csdn.net/weixin_44043668/article/details/108862488