学习笔记:背包模型

上讲习题

AcWing 482

这个题和上一讲AcWing 1014没有任何区别。出队的越少留下的就越多,用 n n n减去最长的一个上升又下降的子序列即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int a[NN],f1[NN],f2[NN];
int main()
{
    
    
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	int ans=0;
	for(int i=1;i<=n;i++)
	{
    
    
		f1[i]=1;
		for(int j=1;j<=i-1;j++)
			if(a[j]<a[i])
				f1[i]=max(f1[i],f1[j]+1);
	}
	for(int i=n;i>=1;i--)
	{
    
    
		f2[i]=1;
		for(int j=i+1;j<=n;j++)
			if(a[j]<a[i])
				f2[i]=max(f2[i],f2[j]+1);
	}
	for(int i=1;i<=n;i++)
		ans=max(f1[i]+f2[i]-1,ans);
	printf("%d\n",n-ans);
	return 0;
}

AcWing 1016

这个题从原来的求最长上升子序列变成了求和最大的上升子序列。之前新加一个数 a i a_i ai子序列的长度长度加 1 1 1,现在和却增加了 a i a_i ai,所以状态转移方程就变成了 max ⁡ ( f j , a j < a i , j < i ) + a i \max(f_j,a_j<a_i,j<i)+a_i max(fj,aj<ai,j<i)+ai,只用自己(边界条件)也是 f i = a i f_i=a_i fi=ai

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

AcWing 187

这个题目可以考虑上讲拦截导弹那道题的贪心算法。但是这个题目可以选择上升或者下降,贪心无法在新加一个时确定是上升还是下降。于是我们再分析,发现题目中 n n n很小,不难想到可以暴搜。每次两种决策,在下降的选一个或者新加,在上升的选一个或者新加。这样分成了两种,就可以分别贪心了。注意,如果当前用的个数大于答案就可以退出了,因为最小的一定会是这个方案。用 u u u表示上升的序列的个数, d d d表示下降, t t t表示处理第几个。

#include<bits/stdc++.h>
using namespace std;
const int NN=54;
int a[NN],up[NN],down[NN],n,ans;
void dfs(int u,int d,int t)
{
    
    
    if(u+d>=ans)
        return;
    if(t==n)
    {
    
    
        ans=u+d;
        return;
    }
    int k=u+1;
    for(int i=1;i<=u;i++)
        if(up[i]<a[t])
        {
    
    
            k=i;
            break;
        }
    int temp=up[k];
    up[k]=a[t];
    dfs(max(u,k),d,t+1);
    up[k]=temp;
    k=d+1;
    for(int i=1;i<=d;i++)
        if(down[i]>a[t])
        {
    
    
            k=i;
            break;
        }
    temp=down[k];
    down[k]=a[t];
    dfs(u,max(d,k),t+1);
    down[k]=temp;
}
int main()
{
    
    
    while(scanf("%d",&n)!=EOF&&n)
    {
    
    
        ans=1e9;
        for(int i=0;i<n;i++)
            scanf("%d",&a[i]);
        dfs(0,0,0);
        printf("%d\n",ans);
    }
    return 0;
}

01背包

概念

有一个背包,最大容量是 m m m,有很多物品,重量是 w i w_i wi,价值是 c i c_i ci,要求把东西装进背包里且不超过最大容量能获得的最大价值是多少。

方法

这种可以定义 f i , j f_{i,j} fi,j为用 1... i 1...i 1...i的物品最多装 j j j的方案。首先,每个状态都可以一个不装,所以初始化为 0 0 0。如果题目要求恰好装多少,则只有最多装 0 0 0可以一个都不装,初始化为 0 0 0。然后, f i , j f_{i,j} fi,j可以不用 i i i f i , j = f i − 1 , j f_{i,j}=f_{i-1,j} fi,j=fi1,j,也可以用第 i i i个,则能得到 c i c_i ci的钱, f i , j = f i − 1 , j − w i + c i f_{i,j}=f_{i-1,j-w_i}+c_i fi,j=fi1,jwi+ci。然后我们发现,所有的 f i , j f_{i,j} fi,j都依赖于 f i − 1 f_{i-1} fi1的数。所以,可以滚动数组,从后往前枚举 j j j,因为状态转移用的 k k k都更小,这样就在存的 f i − 1 , k f_{i-1,k} fi1,k被更新成 f i , k f_{i,k} fi,k之前把 f i , j f_{i,j} fi,j计算了。

例题

AcWing 423

这个题是典型的 01 01 01背包。把总的采药时间看成背包的容量,采一个药的时间看成物品的重量。

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

AcWing 734

这个题目发现每分钟会有价值流失,所以必须要决定顺序。首先想到的是暴力枚举顺序,但是发现时间完全不够。所以考虑寻找特点,设两个石头 x , y x,y x,y,先用 x x x和先用 y y y少去的能量的比为 ( s x × l y ) : ( s y × l x ) (s_x\times l_y):(s_y\times l_x) (sx×ly):(sy×lx),则只有左边得到的值比右边的大先后顺序就要交换。于是,决定了顺序就是一个背包了。我们为了方便计算少去的能量,设 f j f_j fj为刚好用 j j j的时间得到的最大能量。则按前面的来说,要先设为负无穷,只有 f 0 f_0 f0设为 0 0 0。答案就是所有时刻结束的最大值。需要注意的是,一个石头不可能变成负的能量。那么少去的能量呢?最后用的第 i i i个,少了 j − s i j-s_i jsi分钟的能量,乘上每分钟少的值即可。

#include<bits/stdc++.h>
using namespace std;
struct node
{
    
    
    int s,e,l;
    bool operator<(const node&it)const
    {
    
    
        return s*it.l<it.s*l;
    }
}stone[104];
int f[10004];
int main()
{
    
    
    int t;
    scanf("%d",&t);
    for(int kase=1;kase<=t;kase++)
    {
    
    
        int n,m=0;
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
        {
    
    
            scanf("%d%d%d",&stone[i].s,&stone[i].e,&stone[i].l);
            m+=stone[i].s;
        }
        sort(stone+1,stone+1+n);
        memset(f,-0x3f,sizeof(f));
        f[0]=0;
        for(int i=1;i<=n;i++)
            for(int j=m;j>=stone[i].s;j--)
                f[j]=max(f[j],f[j-stone[i].s]+max(0,stone[i].e-stone[i].l*(j-stone[i].s)));
        int ans=0;
        for(int i=0;i<=m;i++)
            ans=max(ans,f[i]);
        printf("Case #%d: %d\n",kase,ans);
    }
    return 0;
}

完全背包

概念

01 01 01背包的概念完全一样,就是可以用的物品个数变成了无限的。

方法

注意,完全背包是可以用多个的,所以在更新我之前可以先把之前的 f f f用一个该物品更新,所以二维数组改成用 f i f_i fi更新,滚动数组也是从小到大枚举。

例题

AcWing 532

这个题不难发现,如果能被几个面值的钱表示出来的钱是完全没有必要存在的。那么是不是只有这些面值的能取掉呢?答案是肯定的。至于证明我不会,有大佬会的私信或者评论区留言,我会第一时间回复并修改的。回到题目,首先一个小的是肯定不能被更大的表示出来的,所以可以从小到大枚举,如果所有更小的都没有覆盖它那么就必须留下了,然后去表示别人。至于表示怎么弄,只要一个面值在之前可以被表示出来,那么我们再加上一个这个面值的货币肯定也是能够表示的。因为货币是无限的,所以像完全背包一样从小到大枚举并表示。

#include<bits/stdc++.h>
using namespace std;
const int NN=50004;
int vis[NN],a[104];
int main()
{
    
    
    int t;
    scanf("%d",&t);
    while(t--)
    {
    
    
        memset(vis,false,sizeof(vis));
        int n,ans=0;
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);
        sort(a+1,a+n+1);
        for(int i=1;i<=n;i++)
        {
    
    
            if(vis[a[i]])
                continue;
            ans++;
            vis[a[i]]=true;
            for(int j=1;j<=a[n];j++)
                if(vis[j])
                    vis[j+a[i]]=true;
        }
        printf("%d\n",ans);
    }
    return 0;
}

多重背包

概念

01 01 01背包也一样,但是每个物品有多个且有使用数量限制。

方法

法一

可以想到一种方法,把这个拆成多个只能用一个的物品,改用 01 01 01背包求,但是时间复杂度较高,拆出来的个数都是 n × s n\times s n×s个,再加上背包,很多题是不够用的。

法二

考虑在上面优化,我们知道,每一个数都可以拆成一个二进制表达式,比如 13 = 1101 = 1 + 4 + 8 13=1101=1+4+8 13=1101=1+4+8,所以我们可以考虑把一个的个数拆成这样 1 , 2 , 4 , 8... 1,2,4,8... 1,2,4,8...的形式,可以把每一个数表示出来,如买 13 13 13个就相当于买 1 1 1个、 2 2 2个打包和 8 8 8个打包的东西。如果 1 , 2 , 4 , 8... 1,2,4,8... 1,2,4,8...这样加下来有剩余没办法再打包的就再打一个包,这样可以用前面能表达的加上该包得到之前不能表示的 2 k + 1 2^k+1 2k+1 s i s_i si。要拆出来 n × log ⁡ s n\times \log s n×logs个左右。

法三

虽然这样拆会快很多,那万一 m m m(背包容量)很大呢?只能考虑不用 01 01 01背包。我们可以发现, f j f_j fj最多只会用 f j − s × v f_{j-s\times v} fjs×v更新,而且只会用最大值。于是我们惊奇地发现,这不就是滑动窗口吗?于是我们就可以用单调队列解决。但是我们发现,每加上一个 v v v,对应的 w w w也要多加一个,所以我们在入队的时候可以减去 k × w k\times w k×w,计算的时候直接加上 k × w k\times w k×w。注意,这里两个 k k k是在循环实时更新的, k k k的值不一样。最后,我们注意,这样每次加一个 v v v会有一些不会更新,但是发现通过余数刚好分成了 v v v组,所以还要循环 v v v种余数。

例题

AcWing 1019

这个题范围都很小,直接按第一种方法做即可。

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

AcWing 6

这个题因为 m m m(在本题为 v v v)的范围很大,所以直接按第三种方法做即可。

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

混合背包

概念

上述的多个背包模型混在一起。

方法

分成两个情况分别讨论即可。完全背包就做完全背包, 01 01 01背包和多重背包放在一起即可。

例题

AcWing 7

这个题目就是个混合背包的板子。注意本题多重背包的范围较大,要用第二种方法。

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

二维费用背包

概念

背包问题的限制变成了两个,体积和重量。

方法

那么就设 f i , j f_{i,j} fi,j为第一个限制最多用 i i i,第二个最多 j j j的最大收益。恰好的同理。用了两个得到收益,状态转移方程 f i , j = max ⁡ ( f i , j , f i − x , j − y + c ) f_{i,j}=\max(f_{i,j},f_{i-x,j-y}+c) fi,j=max(fi,j,fix,jy+c)

例题

AcWing 8

这个题直接套板子即可。

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

分组背包

概念

将很多物品分成多组,每组中只能选一个装进背包。求最大收益。

方法

f i , j f_{i,j} fi,j表示前 i i i组最大用 j j j个的方案。首先可以不选,状态转移: f i , j = f i − 1 , j f_{i,j}=f_{i-1,j} fi,j=fi1,j,也可以选一个, f i , j = max ⁡ ( f i − 1 , j − v + w ) f_{i,j}=\max(f_{i-1,j-v}+w) fi,j=max(fi1,jv+w)。当然,也可以像 01 01 01背包一样用滚动数组。

例题

AcWing 1013

这个题目一个公司只能选择一种机器的数量,所以可以看成一组中的物品。总机器数看成背包的大小,用的个数看成重量。于是我们考虑如何输出方案。因为最优方案一定会用完所有的机器,所以先记录 j j j为机器数,每次看看是从哪个迭代的,设是从 f i , j − k f_{i,j-k} fi,jk迭代的,则该公司就用了 k k k个机器,剩余的机器也只有 j − k j-k jk个了。因为本题要用到前面的 f f f,所以本题就不能滚动数组。

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

AcWing 10

这个题目只是两两之间有依赖,不像分组背包。但是仔细观察发现,我们可以对某一个点的所有子树使用的容积分组,必须选该点,所以剩下的容积也要减去默认选择的自己,这个题就完美地转换成了上一道题目。在每一个子树内分别计算并决策每个子树分多少即可。注意,我们这里 f f f设定的是子树的容积,然而父节点在用我更新时用的是我的总容积,所以最后要加上自己的容积和贡献,且要把前面多出来(小于自己的容积)的部分清 0 0 0

#include<bits/stdc++.h>
using namespace std;
const int NN=104;
int f[NN][NN],v[NN],w[NN],n,m;
vector<int>g[NN];
void dfs(int u)
{
    
    
    for(int i=0;i<g[u].size();i++)
    {
    
    
        int son=g[u][i];
        dfs(son);
        for(int j=m-v[u];j>=0;j--)
            for(int k=0;k<=j;k++)
                f[u][j]=max(f[u][j],f[u][j-k]+f[son][k]);
    }
    for(int i=m;i>=v[u];i--)
        f[u][i]=f[u][i-v[u]]+w[u];
    for(int i=0;i<v[u];i++)
        f[u][i]=0;
}
int main()
{
    
    
    scanf("%d%d",&n,&m);
    int root;
    for(int i=1;i<=n;i++)
    {
    
    
        int fa;
        scanf("%d%d%d",&v[i],&w[i],&fa);
        if(fa==-1)
            root=i;
        else
            g[fa].push_back(i);
    }
    dfs(root);
    printf("%d",f[root][m]);
    return 0;
}

背包求方案数

概念

如标题,背包问题改成了按题目要求求方案数。

例题

AcWing 278

这个题目要求装满的方案,则设 f i f_i fi为装满容量为 i i i的方案数。 f 0 = 1 f_0=1 f0=1其余等于 0 0 0,只有装满 0 0 0有一个什么都不装的方案。每一次有一个新物品,可以用它来装则又有了更多方案,则 f i + = f i − v f_i+=f_{i-v} fi+=fiv。因为只有一个物品,所以要按 01 01 01背包的方式从大到小枚举。

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

AcWing 11

这个题目要求在价值最大的前提下的方案数。设 f i f_i fi为恰好装 i i i的最大收益, g i g_i gi为恰好装 i i i且收益最大的方案数。 f f f的计算方法和以前的一样。 g g g首先只有 i = 0 i=0 i=0时有一种什么都不装的方案。如果 f f f是从 f i − v f_{i-v} fiv转移的,则就可以用得到 f i − v f_{i-v} fiv的方案( g i − v g_{i-v} giv)再选上该物品,方案数根据乘法原理, g i − v g_{i-v} giv乘上新加的方案(只有一种,用该物品),那么方案数加上 g i − v g_{i-v} giv的方案即可。如果 f f f没有变,那么加上原来的方案数即可,因为如果两个都恰好相等,则两种方案都能得到最大的结果,那么两种方案都是可行的,所以要加上而不是直接赋值。最后看所有的 f f f的最大值,并加上所有能得到最大收益的方案数。

#include<bits/stdc++.h>
using namespace std;
const int NN=1004,P=1e9+7;
int f[NN],g[NN];
int main()
{
    
    
    int n,m;
    scanf("%d%d",&n,&m);
    memset(f,-0x3f,sizeof(f));
    g[0]=1;
    f[0]=0;
    for(int i=1;i<=n;i++)
    {
    
    
        int v,w;
        scanf("%d%d",&v,&w);
        for(int j=m;j>=v;j--)
        {
    
    
            int maxx=max(f[j],f[j-v]+w),sum=0;
            if(maxx==f[j])
                sum=g[j];
            if(maxx==f[j-v]+w)
                sum=(g[j-v]+sum)%P;
            f[j]=maxx;
            g[j]=sum;
        }
    }
    int maxx=0,ans=0;
    for(int i=0;i<=m;i++)
        if(f[i]>maxx)
        {
    
    
            ans=g[i];
            maxx=f[i];
        }
        else if(f[i]==maxx)
            ans=(ans+g[i])%P;
    printf("%d",ans);
    return 0;
}

背包求方案

概念

题目中会要求把方案输出。

方法

可以参考前面分组背包中第一题输出方案的方式,最后背包的总和是 m m m,每次看是从哪里迭代的,下标里少的就是用的容量,输出对应的是第几个物品即可。因为我们要看是从哪里迭代的,所以需要前面的东西参加计算,不能滚动数组。

例题

AcWing 12

这个题就是背包问题要求输出方案,按上述方法做即可。

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

习题

AcWing 1024

AcWing 1022

AcWing 1023

AcWing 1021

AcWing 1020

AcWing 426

AcWing 487

解析和代码在下一篇博客——状态机模型给出

猜你喜欢

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