背包问题——动态规划(c++)

定义

背包问题指这样一类问题,题意往往可以抽象成:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。

经典例题

1.AcWing 2. 01背包问题

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

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

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

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8

在这里插入图片描述

解释

只考虑子问题“将前 i 个物品放入容量为 v 的背包中的最大价值”那么考虑如果不放入 i ,最大价值就和 i 无关,就是 f[ i - 1 ][ v ] , 如果放入第 i 个物品,价值就是 f[ i - 1][ v - w[i] ] + val[ i ],我们只需取最大值即可。
在这里插入图片描述

算法1 二维数组+动规

#include<bits/stdc++.h>

using namespace std;

const int N = 1005;
int w[N];    // 重量 
int v[N];    // 价值 
int f[N][N];  // f[i][j], j重量下前i个物品的最大价值 

int main() 
{
    
    
    int n, m;   
    cin >> n >> m;
    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)
        {
    
    
            //  当前重量装不进,价值等于前i-1个物品
            if(j < w[i]) 
                f[i][j] = f[i-1][j];
            // 能装,需判断 
            else    
                f[i][j] = max(f[i-1][j], f[i-1][j-w[i]] + v[i]);
        }           

    cout << f[n][m];
    return 0;
}

算法2 优化 (动规+一维数组)

空间优化:上述状态表示,我们需要用二维数组,但事实上我们只需要一维的滚动数组就可以递推出最终答案。考虑到用f[ v ]来保存每层递归的值,由于我们求f[ i ][ v ] 的时候需要用到的是f[ i-1 ][ v] 和 f[ i-1 ][v - w[i] ] 于是可以知道,只要我们在求f[ v ]时不覆盖f[ v - w[i] ],那么就可以不断递推至所求答案。所以我们采取倒序循环,即v = m(m为背包总容积)

状态转移方程为:f[j] = max(f[j], f[j-w[i]] + v[i]
#include <iostream>
#include <algorithm>

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 -- )//条件j >= v[i]
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m] << endl;

    return 0;
}

2.AcWing 3. 完全背包问题

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

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

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

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

10

思考框架
在这里插入图片描述
在这里插入图片描述

算法1 穷举

时间复杂度O(nm2)

#include<iostream>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N],w[N];
int main()
{
    
    
    int n,m;
    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;
}

算法2 二维

列举一下更新次序的内部关系:

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w ,  f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v]= max(            f[i-1,j-v]   ,  f[i-1,j-2*v] + w , f[i-1,j-2*v]+2*w , .....)
由上两式,可得出如下递推关系: 
                        f[i][j]=max(f[i,j-v]+w , f[i-1][j]) 
#include <iostream>
#include <algorithm>

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 ++ )//注意了,这里的j是从小到大枚举,和01背包不一样
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m] << endl;

    return 0;
}

算法3 滚动数组优化 一维

优化到一维

这里的j层循环,不用从大到小逆序推,因为算法2里,求的就是f[i][j - v[i]]
这样正序推,f[j - v[i]]算的刚好是第i层的f[j - v[i]]
01背包是不同的,01背包要求得是i-1层的f[j - v[i]]
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

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

int main()
{
    
    
    int n, m;
    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;
}

3.AcWing 4. 多重背包问题1

N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

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

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例

4 5
1 2 3
2 4 1
3 4 3
4 5 2

输出样例:

10

基本思路:
多重背包问题与完全背包问题类似,对于第i种物品有n[i] + 1种策略,即取0件、取1件、…、取n[i]件,用F[i, v]表示前i件物品恰好装满背包V的最大价值,由此我们得出状态转移方程:F[ i , v] = max( F[i - 1][v - kCi] + kwi)(0<=k<=n[i])

解题思路:
一、状态表示:f[i][j]

  1. 集合:从前i个物品中选,且总体积不超过j的所有方案的集合.
  2. 属性:最大值

二、状态计算:

  1. 思想-----集合的划分
  2. 集合划分依据:根据第i个物品有多少个来划分.含0个、含1个···含k个.
    状态表示与完全背包朴素代码一样均为:

f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);

时间复杂度O(n∗v∗s)

算法1 朴素版

#include <iostream>
#include <algorithm>

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 ++ )//枚举体积,体积不超过j
            for (int k = 0; k <= s[i] && k * v[i] <= j; k ++ )// 选多少个,限制1:数量s[i];限制2:体积k * v[i] <= j
                f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);

    cout << f[n][m] << endl;
    return 0;
}

算法2 优化版

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 25000;

int f[N], v[N], w[N];
int n, 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] = k * a;
            w[cnt] = k * b;
            s -= k;
            k *= 2;
        }
        if(s > 0){
    
    
            cnt ++;
            v[cnt] = s * a;
            w[cnt] = s * b;
        }

    }

    //多重背包转化为01背包问题
    for(int i = 1; i <= cnt; 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;
}

算法3 优化空间

#include <iostream>
#include <cstring>

using namespace std;

const int N = 1010, M = 20010;

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

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

     // 多重背包朴素版,时间复杂度O(NVS)
    for(int i=1;i<=n;i++) // 从前i个物品中选
    {
    
    
        for(int j =m;j>=0;j--) // 体积不超过j,体积从大到小循环
        {
    
    
            for(int k =0; k<=s[i] && k * v[i] <= j;k ++) // 选多少个,限制1:数量s[i];限制2:体积k * v[i] <= j
            {
    
    
                f[j] = max(f[j],f[j-k * v[i]] + k * w[i]);
            }
        }
    }

    cout<<f[m]<<endl;

    return 0;
}

4.AcWing 5. 多重背包问题 II

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

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

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

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N≤1000
0<V≤2000
0<vi,wi,si≤2000
提示:
本题考查多重背包的二进制优化方法。

输入样例

4 5
1 2 3
2 4 1
3 4 3
4 5 2

输出样例:

10

代码:01优化+二进制优化

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....) 
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-2*v]+2*w , .....)
 由上两式,可得出如下递推关系: f[i][j]=max(f[i,j-v]+w , f[i-1][j])

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
为什么用二进制优化
(1)我们知道转化成01背包的基本思路就是:判断每件物品我是取了你好呢还是不取你好。

(2)我们知道任意一个实数可以由二进制数来表示,也就是20~2k其中一项或几项的和。

(3)这里多重背包问的就是每件物品取多少件可以获得最大价值。

分析:

如果直接遍历转化为01背包问题,是每次都拿一个来问,取了好还是不取好。那么根据数据范围,这样的时间复杂度是O(n^3),也就是1e+9,这样是毫无疑问是会TLE的。

假如10个取7个好,那么在实际的遍历过程中在第7个以后经过状态转移方程其实已经是选择“不取”好了。现在,用二进制思想将其分堆,分成k+1个分别有2^k个的堆,然后拿这一堆一堆去问,我是取了好呢,还是不取好呢,经过dp选择之后,结果和拿一个一个来问的结果是完全一样的,因为dp选择的是最优结果,而根据第二点任意一个实数都可以用二进制来表示,如果最终选出来10个取7个是最优的在分堆的选择过程中分成了2^0=1,2^1=2,2^2=4,10 - 7 = 3 这四堆,然后去问四次,也就是拿去走dp状态转移方程,走的结果是第一堆1个,取了比不取好,第二堆2个,取了比不取好,第三堆四个,取了比不取好,第四堆8个,取了还不如不取,最后依旧是取了1+2+4=7个。

方法是:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 1,2,4,…,2(k-1),n[i]-2k+1,且k是满足n[i]-2^k+1>0的最大整数(注意:这些系数已经可以组合出1~n[i]内的所有数字)。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。

#include<iostream>
using namespace std;

const int N = 12010, M = 2010;

int n, m;
int v[N], w[N]; //逐一枚举最大是N*logS
int f[M]; // 体积<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; // s要减小
            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;
}

5.AcWing 9. 分组背包问题

有 N 组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。

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

输出最大价值。

输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。

接下来有 N 组数据:

每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤100
0<Si≤100
0<vij,wij≤100
输入样例

3 5
2
1 2
2 4
1
3 4
1
4 5

输出样例:

8

在这里插入图片描述
解析

这道题和01背包差不多

对于每组s个物品,有s+1种选法:f[j]=max(f[j],f[j-v[0]]+w[0],f[j-v[1]]+w[1],,f[j-v[s]]+w[s])就是说可以不选(选0个),选1个,选2个…选s个

所以,我们先循环枚举所有体积,再循环枚举所有选择,最后得出状态转移方程:f[j]=max(f[j],f[j-v[k]]+w[k]),其中k是枚举所有选择中的循环变量

算法1 朴素版

#include <iostream>
#include <algorithm>

using namespace std;
const int N = 110;

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

int main(){
    
    
    cin >> n >> m;
    for(int i = 1; i <= n; i ++){
    
    
        cin >> s[i];//第i组物品的数量
        for(int j = 1; j <= s[i]; j ++){
    
    //依次读入第i组第j个物品的体积和价值
            cin >> v[i][j] >> w[i][j];
        }
    }

    for(int i = 1; i <= n; i ++){
    
    
        for(int j = 1; j <= m; j ++){
    
    
            f[i][j] = f[i - 1][j];//第i组物品一个都不选
            for(int k = 1; k <= s[i]; k ++){
    
    
                if(j >= v[i][k]){
    
    
                   f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
                }
            } 
        }
    }

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

    return 0;
}

算法2 优化版 一维

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;

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

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

    for (int i = 1; i <= n; i ++ )
    {
    
    
        cin >> s[i];//第i组物品的数量
        for (int j = 0; j < s[i]; j ++ )//依次读入第i组第j个物品的体积和价值
            cin >> v[i][j] >> w[i][j];
    }

    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)
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);

    cout << f[m] << endl;

    return 0;
}

猜你喜欢

转载自blog.csdn.net/Annabel_CM/article/details/110671979
今日推荐