Bootstrap

背包问题进阶总结(笔记未完工,持续更新)

由于笔记尚未完工,内容散碎而无体系,有价值的内容在于题目的推荐,之后会逐渐完善

写在前面

       背包问题是我所了解的dp问题中最简单的一类,因为实在是有太多的套路已经被世人知晓,背包九讲中除了一些背包的变型(也称为变型背包)就都全部涉及了,所以今天打算把0-1,完全,多重,分组,依赖(以树上背包为例),变型,优化等背包问题做一次刷题和总结。其中优化和变型会穿插在前面的背包类型中。

       本文章是笔者自己的笔记总结,不会涉及基础知识,全片内容以题目和思想为主,不是面向新手和高手的文章,面向学习完背包基础内容后想要进一步提高背包思想的同学,如果您还未学会背包问题(指尚不能稳定独立解决基础的各种背包问题)请先学习站内背包九讲

0-1背包

作为背包问题的始祖,0-1背包是最为根本的,之后的所有背包都是依靠转化为01背包来解决的。

  1. 恰好装满0-1背包问题
    背包的恰好装满问题通常也会和各种变型结合在一起,比如:数字的组合方案等

题目:给出n个物品的体积,要在正好装满背包的情况下求最多能装多少个,背包容量为n

#include <bits/stdc++.h>
using namespace std;
int dp[4000+1];
int main()
{
	int n,a[4];
	cin>>n;
	for(int i=1;i<=n;i++)
        cin>>a[i];
    for(int i=1;i<=n;i++)//初始化
        dp[i]=-1;
     //注意dp[0]=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=n;j>=a[i];j++)
        {
            if(dp[j-a[i]]<0)//初始化在这里起的作用
                continue;
            dp[j]=max(dp[j],dp[j-a[i]]+1);
        }
    }
    cout<<dp[n]<<endl;
}

注意点:初始化问题

  1. 0-1背包变型问题

二维变型
不开心的金明
说来惭愧,这题我第二次做居然又没做出来,笑死
变型:容量过大,将容量修改后变为两维
dp记录的是修改后背包占用容量为的情况下我选了多少个

#include<bits/stdc++.h>
using namespace std;
long long dp[330][110];
long long p[110],v[110];
int main()
{
    //freopen("in.txt","r",stdin);
	long long n,w;
	long long minn=1e10,sum=0;
	cin>>n>>w;
	for(int i=1;i<=n;i++)
    {
        cin>>v[i]>>p[i];
        minn=minn>v[i]?v[i]:minn;
        sum+=v[i];
    }
    minn--;
    for(int i=1;i<=n;i++)
        v[i]-=minn;
    sum-=n*minn;
    long long ans=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=sum;j>=v[i];j--)
        {
           for(int k=n;k>=1;k--)
           {
               if(j+k*minn<=w)
               dp[j][k]=max(dp[j][k],dp[j-v[i]][k-1]+p[i]);
               ans=max(ans,dp[j][k]);
           }
        }
    }
    cout<<ans<<endl;
	return 0;
}

P1466 [USACO2.2]集合 Subset Sums
变型:价值恰好为k的方案数
第一维代表着前n件物品,第二维代表着价值,dp记录的是方案数
dp[i][j]=dp[i-1][j];没选第i件物品时价值为j的方案数自然也是考虑第i件物品时的方案基数

if(j>=a[i])
dp[i][j]+=dp[i-1][j-a[i]];
如果容量能够装下的话,应当加上前i-1件物品价值为j-a[i]的情况

#include<bits/stdc++.h>
using namespace std;
int n,a[40],dp[45][2010];
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		a[i]=i;
	dp[0][0]=1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n*(n+1)/2;j++)
        {
			dp[i][j]=dp[i-1][j];
			if(j>=a[i])
			dp[i][j]+=dp[i-1][j-a[i]];
		}
	if(n*(n+1)%4==0)
	cout<<dp[n][n*(n+1)/4]<<endl;
	else 
	cout<<0<<endl;
	return 0;
}

这能算是变型吗这
B. Checkout Assistant
总之背包变成了奇怪的在一定容量下的最小价值

#include <bits/stdc++.h>

using namespace std;
#define int long long
struct node
{
    int t,v;
};
node obj[2000+100];
int dp[5000];
signed main()
{
	int n;
	//freopen("in.txt","r",stdin);
	cin>>n;
	int maxxt=-1;
	for(int i=1;i<=n;i++)
    {
        cin>>obj[i].t>>obj[i].v;
        obj[i].t++;
        maxxt=max(maxxt,obj[i].t);
    }
    maxxt+=n;
    for(int i=0;i<5000;i++)
        dp[i]=1e16;
    dp[0]=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=maxxt;j>=obj[i].t;j--)
        {
            dp[j]=min(dp[j],dp[j-obj[i].t]+obj[i].v);
        }
    }
    int ans=1e9;
    for(int i=n;i<=maxxt;i++)
    {
        ans=min(ans,dp[i]);
    }
    cout<<ans<<endl;
	return 0;
}

进阶问题
0-1背包第k优解问题
P1858 多人背包

#include<bits/stdc++.h>
using namespace std;
int k,v,n,ans,cnt,now[55];
int V[288],W[288],dp[5008][55];
int main()
{
    cin>>k>>v>>n;
    for(int i=0;i<=5000;i++)
        for(int j=0;j<=50;j++)
            dp[i][j]=-1e9;
    dp[0][1]=0;
    for(int i=1;i<=n;i++)
        cin>>V[i]>>W[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=v;j>=V[i];j--)
        {
            int c1=1,c2=1,cnt=0;
            while(cnt<=k)
            {
                if(dp[j][c1]>dp[j-V[i]][c2]+W[i])
                {
                    now[++cnt]=dp[j][c1++];
                }
                else
                {
                    now[++cnt]=dp[j-V[i]][c2++]+W[i];
                }
            }
            for(int c=1;c<=k;c++)
            {
                dp[j][c]=now[c];
            }
        }
    }
    for(int i=1;i<=k;i++)
    {
        ans+=dp[v][i];
    }
   cout<<ans<<endl;
   return 0;
}

3.多维0-1背包问题

完全背包

  1. 恰好装满完全背包问题
    类似于0-1背包的恰好装满问题

A. Cut Ribbon

#include <bits/stdc++.h>
using namespace std;
int dp[4000+1];
int main()
{
	int n,a[4];
	cin>>n;
	for(int i=1;i<=3;i++)
        cin>>a[i];
    for(int i=1;i<=n;i++)//初始化
        dp[i]=-1;
     //注意dp[0]=0;
    for(int i=1;i<=3;i++)
    {
        for(int j=a[i];j<=n;j++)
        {
            if(dp[j-a[i]]<0)//初始化在这里起的作用
                continue;
            dp[j]=max(dp[j],dp[j-a[i]]+1);
        }
    }
    cout<<dp[n]<<endl;
}
  1. 完全背包变型问题

C. Palindrome Basis
题意:给定一个数,问这个数被回文数相加得来的不同方案数是多少
定义回文数:数字翻转后不变 比如 1,11,121
思路:由于n很小只有40000想到是否可以考虑n*n级别算法的优化,发现实际上40000个数中只有不到499个数是回文数,回文数题中说可以任意取
再看题意就是:有499种物品给定体积,每种物品无限取,求物品体积恰好是n的方案数
这样就很明确了:完全背包恰好装满情况变型

#include <bits/stdc++.h>
using namespace std;
const int N = 4e4+100;
const int mod =1e9+7;
int cnt;
int obj[N];
int dp[N][510];
bool check(int n)
{
    int a[10];
    int cnt1=0;
    while(n)
    {
        a[++cnt1]=n%10;
        n/=10;
    }
    for(int i=1,j=cnt1;i<=j;i++,j--)
    {
        if(a[i]!=a[j])
        {
            return false;
        }
    }
    return true;
}
void get()
{
    for(int i=1;i<=N;i++)
    {
        if(check(i))
        {
            obj[++cnt]=i;
        }
    }
}
int main()
{
    get();
    for(int i=1;i<=cnt;i++)
    {
        dp[0][i]=1;
    }
    for(int i=1;i<=40000+10;i++)
    {
        dp[i][0]=0;
        for(int j=1;j<=cnt;j++)
        {
            if(i>=obj[j])
            {
                dp[i][j]=(dp[i][j-1]+dp[i-obj[j]][j])%mod;
            }
            else
            {
                dp[i][j]=dp[i][j-1]%mod;
            }
        }
    }
    int t;
    for(cin>>t;t;t--)
    {
        int n;
        cin>>n;
        cout<<dp[n][cnt]%mod<<endl;
    }
    return 0;
}
  1. 多维完全背包问题(多费用)

多重背包

  1. 多重背包问题与优化
    由于目前少见n3复杂度能够通过的题目,不加优化无疑是瞧不起出题人 ,所以直接讲优化。
    常用优化:二进制拆分
#include<cstdio>
#include<cstring>
#include<algorithm>
#include <iostream>
using namespace std;
struct obj
{
    int v,m,num;//价值。重量,件数
};
int dp[100000000+10];
int n,w;
int main()
{
    freopen("in.txt","r",stdin);
    obj a[110];
    cin>>n>>w;
    for(int i=1;i<=n;i++)
        cin>>a[i].v>>a[i].m>>a[i].num;
    for(int i=1;i<=n;i++)//种类外层循环
    {
        int num=min(a[i].num,w/a[i].m);//最多能装几个
        for(int k=1;num>0;k<<=1)//二进制拆分
        {
            if(k>num)
                k=num;
            num-=k;
            for(int j=w;j>=a[i].m*k;j--)//0-1背包
            {
                dp[j]=max(dp[j],dp[j-a[i].m*k]+a[i].v*k);
            }
        }
    }
    cout<<dp[w]<<endl;
    return 0;
}

通常,二进制拆分是所有背包问题中最为好用的一种优化,极其个别的牛马出题人会严格要求时间复杂度,以至于必须用单调队列优化我看你就是在为难我胖虎
最佳优化:单调队列
2. 恰好装满
3. 变型
4. 多维

提问:所谓混合背包是否可以用多重背包来做,是否0-1背包和完全背包也能变成多重背包来做

分组背包

1.普通分组背包
2.分组背包变型

依赖关系

指物品之间的依赖关系然物品的拿取顺序上产生了某种结构,比如拓扑结构,树形结构这种特殊的结构就可以变为图上背包,树上背包

作为最后一个出场的背包,树上背包其实较为难理解

我们还是以一道题为例

P2014 [CTSC1997] 选课

题意:给定拓扑结构的选课关系,基于选课关系来确定如何选课获得最大学分

首先,这为什么可以作为一个树形背包?,题目中的拓扑结构

;