背包DP的一些idea


简单的背包问题往往是学好\(DP\)的基础。对于许多动态规划问题,我们都要通过局部的最优值推出当前结果的最优值。是无后效性的。而对于这些最优值的状态,我们往往使用\(dp[]\)\(dp[][]\)来存储。那么,背包问题的状态又该如何表示呢?

\(01\)背包

\(01\)背包是最基础的背包问题,如果是初学者,我们可以使用二维数组先来理解。首先,对于它们的状态我们可以使用\(dp[i][j]\)来存储。表示面对前\(i\)个物品\(j\)个空间所能取到的最大价值。下面我们列举一个栗子:

通过这张表不难看出,\(dp[n][m]\)就是最终答案。那么,这个值是如何推导出来的呢?

\(01\)背包本身就是取和不取两种情况。如果不取,那么当前的\(dp[i][j]\)就是\(dp[i-1][j]\),表示上个物品面对\(j\)个空间的最优值。如果取了,那么当前的\(dp[i][j]\)就是取\(dp[i-1][j-w[i]]+v[i]\)的值,表示上个物品面对\(j-w[i]\)的最优价值加上自己本身的价值。两者取最大,得到公式:

\[dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]) \]

\[j-w[i]\geq0 \]

\(Code\)

#include<bits/stdc++.h>
using namespace std;
int n,m,w[1010],v[1010],dp[1010][1010];
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>m>>n;
    for(int i=1;i<=n;i++){
        cin>>w[i]>>v[i];
    }
    for(int i=1;i<=n;i++){//枚举每件物品 
        for(int j=1;j<=m;j++){//枚举背包容量 
            if(j<w[i])dp[i][j]=dp[i-1][j];
			//如果这个值取不了,就只有不取的情况了 
            else dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
            //如果这个值取得了,那么可取可不取,两者挑出最大值 
        }
    }
    cout<<dp[n][m]<<endl;//答案就是N件物品面对M个容量 
    return 0;
}

\(01\)背包的一维做法

首先观察代码中的\(dp[i-1][j]\)

#include<bits/stdc++.h>
using namespace std;
int w[1010],v[1010],dp[1010];
int n,m;
int main(){
    cin>>m>>n;
    for(int i=1;i<=n;i++){
        cin>>w[i]>>v[i];
    }
    for(int i=1;i<=n;i++){//枚举每件物品 
        for(int j=m;j>=w[i];j--){//枚举当前物品取到的容量 
            dp[j]=max(dp[j-w[i]]+v[i],dp[j]);
			//可取可不取,两者挑出最大值
        }
    }
    cout<<dp[m]<<endl;//答案就是背包容量为m时的状态 
    return 0;
}
  • 好了,你会发现一维的解法不过把二维数组中第一个维度完全舍去了,而第二个维度则保持不变。仔细理清一下逻辑,面对当前的\(ij\)\(dp[j]\)就是上个\(i\)面对\(j\)个空间的最优值,而\(dp[j-w[i]]+v[i]\)就是上个\(i\)面对\(j-w[i]\)的最优值加上自己的值。所以很明显,\(dp[j]\)就是不取的情况,\(dp[j-w[i]]+v[i]\)就是取的情况,\(dp[i][j]\)显然是两者之间挑最大值。因此我们就可以将空间压到一维:

\[dp[j]=max(dp[j-w[i]]+v[i],dp[j]) \]

\[j-w[i]\geq0 \]

  • \(OK\),此时再看代码,发现公式问题好像解决了,但你可能会产生新疑问:为啥\(j\)要倒着循环呢 ? 假若正着循环,那么\(dp[j]\)还是可以理解成\(dp[i-1][j]\),但是\(dp[j-w[i]]\)的值却可能会在\(i\)相同的情况下更新,所以\(dp[j-w[i]]\)实际是取的\(dp[i][j-w[i]]\)的值,所以是不对的。

二 完全背包

完全背包和\(01\)背包大体都是一样的。唯一的区别就是\(01\)背包在面对某种物品时只有取与不取两种情况。而完全背包可以随你取多少个\(0\)个,\(1\)个,\(2\)个一直到无限个都有可能,只是这取决于你的背包容量。所以,完全背包也可以叫做无限背包

接下来,我们可以这样思考。对于某件物品,我们可以尽最大化取,比如背包容量是\(601\),物品重\(150\),这时我们只需再填一个循环来模拟取几个,那么对于这个例子,循环明显只要跑\(4\)次,所以在程序实现中,我们只要判断背包容量是否还够取,如果够,那么就按\(01\)背包的方法得到\(dp[j]\),如果不够,那么\(break\)即可。

最后提一下公式,想必各位已经不用我说了吧:

\[dp[j]=max(dp[j-w[i] \times p]+v[i] \times p,dp[j]) \]

\[j-w[i] \times p\geq0 \]

\(p\)指的是当前\(i\)物品取的个数

\(Code\)

#include<bits/stdc++.h>
using namespace std;
int w[1010],v[1010],dp[1010];
int n,m;
int main(){
    cin>>m>>n;
    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[i];
    }
    for(int i=1;i<=n;i++){
        for(int j=m;j>=w[i];j--){
            for(int p=1;j-w[i]*p>=0;p++){//空间不足,跳出循环
                dp[j]=max(dp[j-w[i]*p]+v[i]*p,dp[j]);
                //公式没必要解释 
            }
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}

下面我们来试着找找\(O(nm)\)算法

先试着想想\(01\)背包的反例。

假如如果用\(01\)背包正着循环,当前的\(dp[j]\)还是\(dp[i-1][j]\),而\(dp[j-w[i]]\)却变成了\(dp[i][j-w[i]]\),那么此时你观察一下\(dp[i][j-w[i]]\),仔细想想,它指的是什么呢?没错,它就是指当前这个\(i\)面对当前自己已经取过的\(j-w[i]\)的值,也就是说,\(dp[j-w[i]]\)此时已经是经过当前\(i\)的刷新了。而根据完全背包,每个\(i\)可以无限的取,所以不管\(dp[j-w[i]]\)是否取了\(i\),当前的\(dp[j]\)都可以通过\(dp[j-w[i]]\)来得到自己。

  • 那么概括来说,就是要让当前的\(dp[j-w[i]]\)\(i\)刷新过才能达到被重复取的效果!

\(Code\)

#include<bits/stdc++.h>
using namespace std;
int n,m,w[1010],v[1010],dp[1010];
int main(){
    cin>>m>>n;
    for(int i=1;i<=n;i++){
        cin>>w[i]>>v[i];
    }
    for(int i=1;i<=n;i++){
        for(int j=w[i];j<=m;j++){//注意是正向循环 
            dp[j]=max(dp[j-w[i]]+v[i],dp[j]);//01公式 
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}

三 多重背包

多重背包也称为有限背包,顾名思义,在多重背包中,每个值取的最大数量是给定的。

那么不妨这样想,对于一个物品可以选\(k\)次,我们可以转化成\(k\)个物品可以选一次,于是就又变回了\(01\)背包。

\(Code\)

#include<bits/stdc++.h>
using namespace std;
int n,m,len,w[1005],v[1005],k[1005];
int W[100005],V[100005],dp[1005];
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>w[i]>>v[i]>>k[i];
    	for(int j=1;j<=k[i]&&w[i]*j<=m;j++){//转化 
			W[++len]=w[i],V[len]=v[i]; 
		}
    }
    for(int i=1;i<=len;i++){
        for(int j=m;j>=W[i];j--){
            dp[j]=max(dp[j],dp[j-W[i]]+V[i]);//01背包 
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}
  • 多重背包二进制优化

这个优化一般用在\(k\)普遍比较大的时候。

因为我们每次都将一个\(k\)拆成\(k\)\(1\),所以有时如果\(k\)较大空间和时间可能都不会允许。

但其实还能换种方式拆。

不妨让\(k\)二进制拆分

可以举个栗子:\(k=20\)

我们将其拆成\(1,2,4,8,5\),再把花费和价值乘以次数即可。

于是你会发现取\(1\)种,取\(2\)\(……\)\(20\)种的情况都可以被表示。其本质是因为二进制数可以拼凑成范围内的所有数,相信可以理解吧。

故复杂度降至\(O(nmlogk)\)

\(Code:\)

#include<bits/stdc++.h>
using namespace std;
int n,m,len,w[1005],v[1005],k[1005];
int W[100005],V[100005],dp[1005];
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>w[i]>>v[i]>>k[i];
    	k[i]=min(k[i],m/w[i]);//严格保证k[i]*w[i]<=m 
    	int tot=0;
    	for(int j=1;;j=j<<1){
        	if(tot+j>k[i]){//不够拆了
        		tot=k[i]-tot;
        		W[++len]=w[i]*tot,V[len]=v[i]*tot;//把剩下的数记上 
        		break;
			}
			tot+=j;
			W[++len]=w[i]*j,V[len]=v[i]*j;//二进制拆分 
		}
    }
    for(int i=1;i<=len;i++){
        for(int j=m;j>=W[i];j--){
            dp[j]=max(dp[j],dp[j-W[i]]+V[i]);//01背包 
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}

四 背包求种类

在庞大的背包问题中,求种类数也是很常见的。没错,我们在做许多题时,都会遇见像在\(n\)个数中取到值为\(m\)的种类数是多少。其实这类问题很简单易懂,下面我为大家普及一下这种模板类型的题。

首先,状态不用说,\(dp[i][j]\)表示第\(i\)个值面对\(j\)个空间的种类数。在这其中,我们先考虑\(j\)\(0\)的情况,没错,很明显就是\(1\)个。因为一个值都不能取本身就是\(1\)种情况。接下来,对于\(dp[i][j]\),依然考虑取与不取两种情况。若取即是\(dp[i-1][j-w[i]]\),不取即是\(dp[i-1][j]\)。将两者种类数相加即可。

\[dp[i][j]=dp[i-1][j-w[i]]+dp[i-1][j] \]

把它压到一维,也就是:

\[dp[j]=dp[j-w[i]]+dp[j] \]

\(\operatorname{Update}\) \(\operatorname{On}\) \(\operatorname{2018.12.25}\)


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM