【背包问题详解】【01背包问题】【完全背包问题】【多重背包问题—暴力解法/二进制优化】

【01背包问题】

【题目描述】

        给定n个物品,每个物体有个体体积gif.latex?Vi和一个价值gif.latex?Pi。现有一个容量为V的背包,请问如何选择物品装入背包,使得获得的总价值最大?

 【思路】

        通过讨论每个物品放与不放,连接前 i -1 个物品的状态和前 i 个物品状态之间的关系,最终结果就是两种选择下,收益的更大值

我们维护一个二维状态 f [ i , j ], 来表示前 i 个物品,放到体积为 j 的背包里。

9276b046b4d2475881b3af3ca92ae0dc.png

        可以得到:f [ i , j ] = max( f [ i − 1, j ] , f [ i −1, j − v [ i ] ] + p [ i ] )

对于01背包问题的更详细解释,可以参考以下blog:

《算法图解》动态规划 背包问题


【0-1背包问题动态规划的四要素】

(1)状态:一个二维状态 f [ i , j ], 来表示前 i 个物品,放到体积为 j 的背包里

(2)转移方程:

f[i][j]=f[i-1][j];//表示装不下第i个物品
f[i][j]=max(f[i-1][j],f[i-1,j-v[i]]+p[i]);

(3)初始状态:

f[0][j]=0;//表示一个物品都没放,价值为0

(4)转移方向:保证 i 从小到大增大,等式右边的状态比等式左边先算出来。

完整代码如下:

#include<iostream>
#include<algorithm>
#define N 1002
using namespace std;
int n, V, v[N], p[N],f[N][N];
int main()
{
	cin >> n >> V;
	for (int i = 1; i <= n; i++)
		cin >> v[i] >> p[i];
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= V; j++)
		{
			if (j < v[i])
				f[i][j] = f[i - 1][j];
			else
				f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + p[i]);
		}
	}
	cout << f[n][V]<<endl;
	return 0;
}

【复杂度分析】

空间复杂度:O(nV)使用了二维数组 f [ n ][ V ]

时间复杂度:O(nV)双层for循环

【缺点分析】

        因为这个算法与物品个数,背包容量有关,假如物品个数很多,物品体积也非常大的时候,空间复杂度会急剧增加


 【算法优化1——滚动数组优化】

【基本思想】

        在动态规划中,有时候内存空间会比较紧张,所以我们需要一些技巧来优化内存开销,下面提出一种优化方式为“滚动数组优化”,其基本思想类似于“踩石头过河”

        而在此题中,当我们在计算第 i 行时,只需保留第 i -1 行,可以把前 i - 2 行的内存空间释放掉,那么也就是说每一次计算只需要两行的数据。那么我们可以只利用 f [ 2 ][ V ]来记录数组的状态。奇数行填入状态f [ 1 ][ j ]中,偶数行填入状态f [ 0 ][ j ]中。

f87e04b2b4714e468ed9e8828abd38ef.png

 代码修改如下:

int f[2][V];
for(int i=1;i<=n;i++)
    for(int j=1;j<=V;j++)
        if(j<v[i])
            f[i&1][j]=f[(i-1)&1][j];
        else
            f[i&1][j]=max(f[(i-1)&1][j],f[(i-1)&1][j-v[i]]+p[i]);
cout<<f[n&1][V]<<endl;

【注意】

(1)奇数的二进制表示的最低位为“1”,偶数的最低位为“0”,可以利用 i & 1 来取 i 的奇偶性

i & 1 = 1 ( i 为奇数)

i & 1 = 0( i 为偶数)

(2)利用 i & 1 来取 i 的奇偶性,为什么不用 i % 2 呢?因为位运算的优先级最低,但是运算速度却最高,用 i & 1来判断奇偶性比用 i % 2 要高4倍,当循环的次数非常大时,位运算是非常有效率的

【复杂度分析】

        空间复杂度降为O(2V)


 【算法优化2——优化到一维数组】

【基本思路】

//j<V时
if (j < v[i])
	f[i][j] = f[i - 1][j];
else
	f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + p[i]);

        根据上面代码可以看出来,当 j < v [ i ]时,f [ i ][ j ] = f [ i - 1 ][ j ],那么如果我们将 j 从大到小进行枚举当 j 从 V 变化到 v[ i ]的过程中,一直记录的是:

                f [ i ][ j ] = max(f [ i - 1 ][ j ], f [ i - 1 ][ j - v [ i ] ] + p [ i ] )

        可以用这个图来进行理解(摘取自上文提到的blog):

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2JvaHU4Mw==,size_16,color_FFFFFF,t_70

        当 j < v [ i ] 时一直都有f [ i ][ j ] = f [ i - 1 ][ j ],那么如果映射到一维数组的话,相当于没有变化。

        所以我们维护一个一维数组f [ j ]当 j < v [ i ]时f [ j ]记录的就是f [ i - 1, j ],当 j > v [ i ]时,f [ j ]记录的就是f [ i ,j ]

采用代码如下:

int f[N];//维护一个一维数组
for(int i=1;i<=n;i++)
    for(int j=V;j>=v[i];j--)//j从V开始枚举到v[i],1~v[i]的状态都是一样的,f[i,j]=f[i-1,j]
                            //可以用v[i]的状态来代表v[i]之前的状态
        f[j]=max(f[j],f[j-v[i]]+p[i]);
cout<<f[V]<<endl;

【复杂度分析】

        空间复杂度降为O(V)

【完全背包问题】

【题目描述】

        有 N种物品和一个容量是 V 的背包,每种物品都有无限件可用。

        第 i 种物品的体积是 vi,价值是 wi。

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

【思路】(以下思路摘录于下述blog)(1条消息) 01背包问题,完全背包,多重背包详解(C++代码实现)_完全背包问题代码_BabyCrys的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/BabyCrys/article/details/104747308

        完全背包和01背包的区别在于:01背包每种物品只有一件,而完全背包每种物品有无限件,即为每件物品可以选择无数次

        ▲01背包问题中要按照v=V…0的逆序来循环。
        这是因为要保证第 i 次循环中的状态 f [i][v] 是由状态 f [i-1][v-c[i]] 递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第 i 件物品的子结果 f [i-1][v-c[i]]

        ▲完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第 i 种物品的子结果 f [i][v-c[i]],所以就可以并且必须采用

v=0…V的顺序循环。

【代码实现如下】

#include<iostream>
#include<algorithm>
using namespace std;
int f[1001];
int main(){
    int N,V;
    cin>>N>>V;
    for(int i=1;i<=N;i++){
        int v,m;
        cin>>v>>m; //这里没有像01背包那样,利用数组去存储每件物品的体积和价值
                    //因为这里采用的是顺序遍历,可以在得到第i种物品的体积的时候,直接进行顺序循环
        for(int j=v;j<=V;j++){
            f[j]=max(f[j],f[j-v]+m);}
    cout<<f[V]<<endl;
    return 0;
}

【多重背包问题】

【题目描述】

        有 gif.latex?N种物品和一个容量是gif.latex?V的背包。 

        第 gif.latex?i 种物品最多有 gif.latex?Si 件,每件体积是 gif.latex?Vi,价值是 gif.latex?Wi

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

【思路】

        【基本思路】

        最优解中可以包含 0,1,2,…,s 个 第 gif.latex?i 个物品
        状态转移矩阵为f [ i ][ v ] = max { f [ i-1 ][ v ] , f [ i-1 ][ v- k * c[ i ] ]+k*w[ i ] }
                                                                                                                           (k=0,1,2,…,s)
        空间复杂度为:O( V*∑s[i] )

        上述思路的方法是将第 i 个物品拆分为1,2, …,s,对应的质量与价值也乘以相应的倍数每个不同倍数的物品 i 就是一个全新的物品,然后就转换为了01背包问题

        【二进制思想】

        参考二进制数的表示方法,将每件物品的数量都可以用1,2,4,8,… 的 gif.latex?2%5E%7Bk%7D 组合来表示。一个正整数n,可以被分解成1,2,4,…,gif.latex?2%5E%7B%28k-1%29%7D,n-gif.latex?2%5E%7Bk%7D+1的形式。其中,k是满足n-gif.latex?2%5E%7Bk%7D+1>0的最大整数。 

        例如13,则0~13范围内的所有数都可以用1,2,4,6,四个数来表示,其中6=13-(1+2+4)。这样13就由之前拆分为13个物品,简化为拆分4个物品

        空间复杂度:O( V*∑log n[i] )

【基本思路——暴力代码实现】

#include<iostream>
#include<algorithm>
using namespace std;
int f[101];
int main(){
    int N,V;
    cin>>N>>V;
    for(int i=1;i<=N;i++){
        int v,w,s;
        cin>>v>>w>>s;
        for(int j=V;j>=v;j--){
            for(int k=1;k<=s&&k*v<=j;k++){
                f[j]=max(f[j],f[j-k*v]+k*w);
            }
        }
    }
    cout<<f[V]<<endl;
}

【二进制算法代码实现】

#include<iostream>
#include<algorithm>
using namespace std;
int f[101];
int v[10005],w[10005];
int cnt=0;
int main(){
    int N,V;
    cin>>N>>V;
    for(int i=1;i<=N;i++){
        int vi,wi,si;
        cin>>vi>>wi>>si;
        for(int k=1;k<=si;k*=2){
            cnt++;
            v[cnt]=vi*k;
            w[cnt]=wi*k;
            si-=k;
        }
        if(si>0){
            cnt++;
            v[cnt]=vi*si;
            w[cnt]=wi*si;
        }
    }
    for(int i=1;i<=cnt;i++){
        for(int j=V;j>=v[i];j--){
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }
    cout<<f[V]<<endl;
}

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值