Acwing算法基础课学习笔记(十四)--动态规划之背包问题

终于来到了动态规划问题。
01背包问题
背包问题属于动规问题中一个非常大的门类,首先要推荐的就是背包九讲,可以说是必读的一个系列博客。
在这里插入图片描述
01背包
特点:每个物品仅能使用一次

重要变量&公式解释
f[i][j]:表示所有选法集合中,只从前i个物品中选,并且总体积≤≤j的选法的集合,它的值是这个集合中每一个选法的最大值.
状态转移方程:f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i])
f[i-1][j]:不选第i个物品的集合中的最大值
f[i-1][j-v[i]]+w[i]:选第i个物品的集合,但是直接求不容易求所在集合的属性,这里迂回打击一下,先将第i个物品的体积减去,求剩下集合中选法的最大值.

问题:集合如何划分

一般原则:

  • 不重不漏,不重不一定都要满足(一般求个数时要满足)
  • 如何将现有的集合划分为更小的子集,使得所有子集都可以计算出来.

在这里插入图片描述

无优化版代码:

#include <iostream>

using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];//体积、价值 
int f[N][N];// f[i][j]表示的状态是:j重量下前i个物品的最大价值

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];

    for (int i  = 1; i <= n; i++)
        for (int j = 0; j <= m; j++)//关于下标的起始,要思考是否合法即可,比如j = 0,就是总体积是0,显然是合法的
        {
            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]);
        }
    cout << f[n][m] << endl;
    return 0;
}

将二维优化成一维如下:

/*
1. f[i] 仅用到了f[i-1]层, 
2. j与j-v[i] 均小于j
3.若用到上一层的状态时,从大到小枚举, 反之从小到大
*/
#include <iostream>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    
   	for(int i = 1; i <= n; i++) 
       	for(int j = m; j >= v[i]; j--) 
           	f[j] = max(f[j], f[j-v[i]]+w[i]);
    cout << f[m] << endl;
 return 0;    
}

完全背包问题
在这里插入图片描述

f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包

f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题
最朴素的写法(数据加强后已经不能ac了)

#include <iostream>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][N];

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
    
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= m; j++)
            for (int k = 0; k * v[i] <= j; k++)
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
    cout << f[n][m] <<endl;
    return 0;
    
}

在这里插入图片描述

分析过状态转移后优化的朴素写法:

#include <iostream>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][N];

int main()
{
    cin >> n >> m;

    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            f[i][j] = f[i - 1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
        }

    cout << f[n][m] << endl;

    return 0;
}

最终优化后的代码:

#include <iostream>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    
   	for(int i = 1; i <= n; i++) 
       	for(int j = v[i]; j <= m; j++) 
           	f[j] = max(f[j], f[j-v[i]]+w[i]);
    cout << f[m] << endl;
 return 0;    
}

多重背包问题 I
只需要在完全背包的基础上加上 k <= s[i]的判断即可。

#include <iostream>

using namespace std;

const int N = 110;
int n, m;
int v[N], w[N], s[N];
int f[N][N];

int main ()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
    
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= m; j++)
            for (int k = 0; k <= s[i] && k * v[i] <= j; k++)
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
    
    cout <<f[n][m] << endl;
    return 0;
}

多重背包问题 II
思路和多重背包问题I一样,但这题的数据范围变成1000了,非优化写法时间复杂度O ( n 3 ) O(n^3)O(n3) 接近 1e9必超时。所以需要用二进制优法。
在这里插入图片描述
假设有一组商品,一共有11个。我们知道,十进制数字 11 可以这样表示
11 = 1011 ( B ) = 0111 ( B ) + ( 11 − 0111 ( B ) ) = 0111 ( B ) + 0100 ( B ) 11=1011(B)=0111(B)+(11−0111(B))=0111(B)+0100(B)11=1011(B)=0111(B)+(110111(B))=0111(B)+0100(B)。正常背包的思路下,我们要求出含这组商品的最优解,我们要枚举12次(枚举装0,1,2…12个)。

现在,如果我们把这11个商品分别打包成含商品个数为1个,2个,4个,4个(分别对应0001,0010,0100,0100)的四个”新的商品 “, 将问题转化为01背包问题,对于每个商品,我们都只枚举一次,那么我们只需要枚举四次 ,就可以找出这含组商品的最优解。 这样就大大减少了枚举次数。

这种优化对于大数尤其明显,例如有1024个商品,在正常情况下要枚举1025次 , 二进制思想下转化成01背包只需要枚举10次。

#include <iostream>
#include <algorithm>

using namespace std;
//2的12次方大于2000,也就是说一个数最多可以拆成12个,故数组容量乘12 
const int N = 12010, M = 2010;

int n, m;
int v[N], w[N];
int f[M];

int main()
{
    cin >> n >> m;

    int cnt = 0;
    for (int i = 1; i <= n; i ++ )//二进制拆分 
    {
        int a, b, s;
        cin >> a >> b >> s;
        int k = 1;
        while (k <= s)
        {
            cnt ++ ;
            v[cnt] = a * k;//存容量 
            w[cnt] = b * k;//存价值 
            s -= k;
            k *= 2;
        }
        if (s > 0)
        {
            cnt ++ ;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }

    n = cnt;
    //01背包模板
    for (int i = 1; i <= n; i ++ )
        for (int j = m; j >= v[i]; j -- )
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m] << endl;

    return 0;
}

分组背包问题
在这里插入图片描述

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;

int n, m;
int v[N][N], w[N][N], s[N];//v为体积,w为价值,s代表第i组物品的个数
int f[N];//只从前i组物品中选,当前体积小于等于j的最大值

int main()
{
    cin >> n >> m;
    //读入
    for (int i = 1; i <= n; i ++ )
    {
        cin >> s[i];
        for (int j = 0; j < s[i]; j ++ )
            cin >> v[i][j] >> w[i][j];
    }
    //若用到上一层(i - 1)的状态时,从大到小枚举;用到当前状态则从小到大枚举
    for (int i = 1; i <= n; i ++ )//从前往后枚举每一组
        for (int j = m; j >= 0; j -- )//从大到小枚举所有体积
            for (int k = 0; k < s[i]; k ++ )//枚举所有选择
                if (v[i][k] <= j)//第i组的第k件物品的要能放得下背包,才去更新状态
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);

    cout << f[m] << endl;

    return 0;
}

版权声明:本文为weixin_43619346原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。