快晴

导航

背包问题

我们考虑在一般试题当中出现的背包问题:

01背包问题

\(N\) 件物品和一个容量是 \(W\) 的背包。每件物品只能使用一次。

\(i\) 件物品的体积是 \(w_{i}\)价值是 \(v_{i}\)

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

0<\(N,W\)≤1000
0<\(v_{i},w_{i}\)≤1000

时间复杂度:\(O(N*W)\)

算法思想:我们对\(i\)个物品价值最大的选法进行分析,明显的,它必然从\(i-1\)个物品的组合中再任选不属于该\(i-1\)个物品的集合的一个物品叠加的价值的最大值。

我们设立一个二维数组\(f[i][j]\),其代表在\(i\)\(j\)的条件下的最大价值,\(i\)代表选择了多少个物品,\(j\)代表已经占用背包的多少空间。

可以得到一个递推式:

\(if(j>=w[i])f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);\)

\(else\) \(f[i][j]=f[i-1][j];\)

也就是我们不重不漏的按顺序递推完成之后,最终\(f[n][W]\)得到的即为解。

int main()
{
	int n,V;
	scanf("%d%d", &n, &W);
	for (int i = 1;i <= n;++i)
		scanf("%d%d", &v[i], &w[i]);
	for(int i=1;i<=n;++i)
	{
	    for(int j=1;j<=W;++j)
	    {
	        if(j>=w[i])f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);
	        else f[i][j]=f[i-1][j];
	    }
	}
	printf("%d",f[n][W]);
	return 0;
}

可以发现,每一轮的递推均用上一轮的递推结果,即由\(f[i-1][?]\)\(f[i][?]\)的递推。

因此我们可以做出空间上的优化。

此处我们按照\(W\)的逆序枚举就是为了保证\(f[i-1][?]\)\(f[i][?]\)的递推。

int main()
{
	int n,V;
	scanf("%d%d", &n, &W);
	for (int i = 1;i <= n;++i)
		scanf("%d%d", &v[i], &w[i]);
	for(int i=1;i<=n;++i)
	    for(int j=W;j>=w[i];++j)
	    	f[j]=max(f[j],f[j-w[i]]+v[i]);
	printf("%d",f[W]);
	return 0;
}

完全背包问题

\(N\) 件物品和一个容量是 \(W\) 的背包。每件物品能使用无限次。

\(i\) 件物品的体积是 \(w_{i}\)价值是 \(v_{i}\)

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

0<\(N,W\)≤1000
0<\(v_{i},w_{i}\)≤1000

我们不考虑贪心做法,我们直接考虑动态规划做法。

时间复杂度:\(O(N*W)\)

算法思想同01背包问题,但是注意每个物品能选无限件,也就是我们不能简单的从\(f[i-1][?]\)\(f[i][?]\)的递推,我们还要考虑\(f[i][?]\)\(f[i][?]\)的递推。但是代码写起来明快了很多。

此处我们按照\(W\)的顺序枚举就是即为了保证\(f[i-1][?]\)\(f[i][?]\)的递推,又为了保证\(f[i][?]\)\(f[i][?]\)的递推。

int main()
{
	int n,W;
	scanf("%d%d", &n, &W);
	for (int i = 1;i <= n;++i)
		scanf("%d%d", &w[i], &v[i]);
	for(int i=1;i<=n;++i)
	    for(int j=w[i];j<=W;++j)
	        f[j]=max(f[j],f[j-w[i]]+v[i]);
	printf("%d",f[W]);
	return 0;
}

多重背包问题

\(N\) 件物品和一个容量是 \(W\) 的背包。每件物品最多能使用\(s_{i}\)次。

\(i\) 件物品的体积是 \(w_{i}\)价值是 \(v_{i}\)

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

0<\(N,W\)≤100
0<\(v_{i},w_{i},s_{i}\)≤100

时间复杂度:\(O(N*W*\sum_{i=1}^{N}s[i])\)

这里我们只是相较于01背包问题多了一层\(s[i]\)即物品件数的考虑,没什么特别的。

int main()
{
	int n,W;
	scanf("%d%d", &n, &W);
	for (int i = 1;i <= n;++i)
		scanf("%d%d%d", &w[i], &v[i],&s[i]);
	for(int i=1;i<=n;++i)
	    for(int j=W;j>=1;--j)
	        for(int k=0;k<=s[i];++k)
	        if(k*w[i]<=j)f[j]=max(f[j],f[j-k*w[i]]+k*v[i]);
	printf("%d",f[W]);
	return 0;
}

多重背包问题____二进制优化

\(N\) 件物品和一个容量是 \(W\) 的背包。每件物品最多能使用\(s_{i}\)次。

\(i\) 件物品的体积是 \(w_{i}\)价值是 \(v_{i}\)

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

0<\(N\)≤1000
0<\(V\)≤2000
0<\(v_{i},w_{i},s_{i}\)≤2000

我们可以发现,多重背包问题下,时间复杂度过高,因此我们考虑优化。

显然的,对于任意一个数字,我们都可以将它拆分为\(2^0+2^1+...+2^n\)的形式。

(注意,与二进制形式的幂次不同,这里只是按严格递增幂次的顺序拆分)

同理的,对于一种价值为\(v\)的物品,总共存在\(s\)件。我们可以将其\(s\)拆分为其上的组合。

因此我们将\(s\)件的物品最多拆分为\(\log s\)件物品。

在递推的枚举当中,必然会不重不漏的经历每一种状态。

但是我们去最大值的原因,不少价值小的状态未加入计算,减少了时间复杂度。

同理的,我们可以举一反三,进行三进制拆分等,理论时间复杂度均等,全看数据。例如这题八进制拆分能够得到最快的运行结果。

#include<cstdio>
#include<iostream>
#include<algorithm>
#define pb push_back
using namespace std;
int s,f[1000007],n,W;
vector<int>w,v;
int main()
{
  int num[15]={1,2,4,8,16,32,64,128,256,512,1024,2048,5096,10192};
  cin>>n>>W;
  for(int i=1;i<=n;++i)
  {
      int vv,ww;
      cin>>ww>>vv>>s;
      for(int k=0;k<14&&num[k]<=s;++k)
      w.pb(num[k]*ww),v.pb(num[k]*vv),s-=num[k];
      if(s)w.pb(s*ww),v.pb(s*vv);
  }
  for(int i=0;i<w.size();++i)
      for(int j=W;j>=w[i];--j)
       f[j]=max(f[j],f[j-w[i]]+v[i]);
  cout<<f[W];
  return 0;
}

多重背包问题____单调队列优化

\(N\) 件物品和一个容量是 \(W\) 的背包。每件物品最多能使用\(s_{i}\)次。

\(i\) 件物品的体积是 \(w_{i}\)价值是 \(v_{i}\)

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

0<\(N\)≤1000
0<\(V\)≤20000
0<\(v_{i},w_{i},s_{i}\)≤20000

我们对\(f[i][j]\)进行空间优化,现在,对于任意一个\(f[j]\)怎样由其余数据递推?

我们令 \(f[j]\) 表示容量为\(j\)的情况下,获得的最大价值
那么,针对每一类物品\(i\) ,我们都更新一下 \(f[m] --> f[0]\) 的值,最后 \(f[m]\) 就是一个全局最优值\(f[m] = max(f[m], f[m-v] + w, f[m-2*v] + 2*w, f[m-3*v] + 3*w, ...)\)
接下来,我们把 \(f[0] --> f[m]\) 写成下面这种形式
\(f[0], f[v], f[2*v], f[3*v], ... , f[k*v]\)
\(f[1], f[v+1], f[2*v+1], f[3*v+1], ... , f[k*v+1]\)
\(f[2], f[v+2], f[2*v+2], f[3*v+2], ... , f[k*v+2]\)
...
\(f[j], f[v+j], f[2*v+j], f[3*v+j], ... , f[k*v+j]\)
显而易见,\(m\) 一定等于 \(k*v + j\),其中 \(0 <= j < v\)
所以,我们可以把 \(f\) 数组分成 \(j\) 个类,每一类中的值,都是在同类之间转换得到的
也就是说,\(f[k*v+j]\) 只依赖于{\(f[j], f[v+j], f[2*v+j], f[3*v+j], ... , f[k*v+j]\)}
因为我们需要的是{\(f[j], f[v+j], f[2*v+j], f[3*v+j], ... , f[k*v+j]\)}中的最大值。
可以通过维护一个单调队列来得到结果。这样的话,问题就变成了 \(j\) 个单调队列的问题
很显然,找最大的值,因此我们可以考虑使用单调队列优化。

单调队列此处不做介绍

时间复杂度:\(O(N*W)\)

int f[rg],q[rg],g[rg];
int main()
{
   int n,m,v,w,s;
   scanf("%d%d",&n,&m);
   for(int i=1;i<=n;++i)
   scanf("%d%d%d",&v,&w,&s),
   obj[i]={w,v,s};
   for(int i=1;i<=n;++i)
   {
      memcpy(g,f,sizeof f);
      for(int j=0;j<obj[i].v;++j)
      {
        int l=0,r=-1;
        for(int k=j;k<=m;k+=obj[i].v)
        {
            while(r>=l&&k-q[l]>obj[i].s*obj[i].v)++l;
            while(r>=l&&g[k]-(k-j)/obj[i].v*obj[i].w>=g[q[r]]-(q[r]-j)/obj[i].v*obj[i].w)--r;
            q[++r]=k;
            f[k]=max(f[k],g[q[l]]+(k-q[l])/obj[i].v*obj[i].w);
        }
      }
   }
   printf("%d",f[m]);
   return 0;
}

有依赖的背包问题(树上背包问题)

\(N\) 个物品和一个容量是 \(V\) 的背包。

物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。

如下图所示:
QQ图片20181018170337.png

如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。

每件物品的编号是 \(i\),体积是 \(v_{i}\),价值是 \(w_{i}\),依赖的父节点编号是 \(p_{i}\)。物品的下标范围是 \(1…N\)

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

0<\(N,W\)≤100
0<\(v_{i},w_{i}\)≤100

根节点\(p=-1\)

依照01背包的思路,我们可以写出,由于其依赖于根是否被选取,因此会有些差异。

struct node
{int c,w;}box[maxn];
struct edge
{int u,v,ne;}e[maxn];
int head[maxn],cnt,f[107][10007];
void add(int u,int v){e[++cnt]={u,v,head[u]},head[u]=cnt;}
void dfs(int root,int tot)
{
   if(root==-1)return;
   for(int i=box[root].c;i<=tot;++i)f[root][i]=box[root].w;//父节点必选
   for(int i=head[root];i!=-1;i=e[i].ne)
   {
       int v=e[i].v;
       dfs(v,tot);
       for(int j=tot;j>=box[root].c;--j)
       {
           for(int k=j-box[root].c;k>=0;--k)
               f[root][j]=max(f[root][j],f[root][j-k]+f[v][k]);
       }
   }
}
int main()
{
   int  n,V,fa,root;
   memset(head,-1,sizeof head);
   scanf("%d%d",&n,&V);
   for(int i=1;i<=n;++i)
   {
       scanf("%d%d%d",&box[i].c,&box[i].w,&fa);
       if(fa==-1)root=i;
       else add(fa,i);
   }
   dfs(root,V);
   printf("%d",f[root][V]);
   return 0;
}

⑦:巨大背包问题(依赖于数量的背包问题)

1<=\(N\)<=40

\(w_{i},v_{i},W\)均可视为无限大。

为了方便表示我们约定

\(w_{i},v_{i},W\)<\(10^{15}\)

二分搜索+状态压缩

这里如果使用动态规划的方法,无论时间还是空间均会超出限制。

因此我们只能考虑最平凡的解法——搜索。

但是一般的搜索又会超出时间限制,因此我们考虑优化。

我们将左半边整理,对于左半边的任意组合价值与重量我们存储到表中。

我们再定义一个合适的偏序,将表中的数据排好序。

对于右半边的所有组合,我们均在左半边组成的顺序表中寻找最优解,即最大合法解。

我们考虑二分查找,因此优化至\(\log\)

时间复杂度:\(O(\frac{n}{2}*2^{\frac{n}{2}}*\log2^{\frac{n}{2}})\)

#include<cstdio>
#include<cstring>
#include<vector>
#include<cmath>
#include<bitset>
#include<algorithm>
#include<iostream>
#define fi first
#define se second
using namespace std;
typedef long long ull;
const ull inf=1e18;
pair<ull,ull>lft[1<<23],e[155];
int main()
{
int n;
ull m,maxx=0;
cin>>n>>m;
for(int i=0;i<n;++i)
cin>>e[i].se>>e[i].fi;
int llim=n/2,rlim=n-llim;
for(int i=0;i<1<<llim;++i)
{
 ull tot=0,vov=0;
 for(int j=0;j<llim;++j)
 if((i>>j)&1)tot+=e[j].fi,vov+=e[j].se;
 lft[i]={tot,vov};
}
sort(lft,lft+(1<<llim));
int r=0;
for(int i=0;i<(1<<llim);++i)
if(lft[r].se<lft[i].se)lft[++r]=lft[i];
for(int i=0;i<1<<rlim;++i)
{
 ull tot=0,vov=0;
 for(int j=0;j<rlim;++j)
 if((i>>j)&1)tot+=e[j+llim].fi,vov+=e[j+llim].se;
 if(tot<=m)
 {
     ull k=(upper_bound(lft,lft+r+1,make_pair(m-tot,inf))-1)->se;
     maxx=max(maxx,vov+k);
 }
}
cout<<maxx;
return 0;
}

posted on 2021-12-13 20:23  快晴  阅读(39)  评论(0编辑  收藏  举报