背包九讲

本篇文章设计的题目均在AcWing的2-12题。

一、01背包

要求:每种物品只能选择0个或1个,即对于每种物品只有选或者不选两种情况。
题目描述:(题目链接)
\quad N N N 件物品和一个容量是 V V V 的背包。每件物品只能使用一次。第 i i i 件物品的体积是 v i v_i vi,价值是 w i w_i wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

输入格式
\quad 第一行两个整数, N , V N,V NV,用空格隔开,分别表示物品数量和背包容积。接下来有 N N N 行,每行两个整数 v i , w i v_i,w_i vi,wi,用空格隔开,分别表示第 i i i 件物品的体积和价值。

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

数据范围
0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例

8

思路1:二维数组记录
\quad f[i][j]表示只看前i物品,总体积是j的情况下总价值最大是多少。最大值答案就在f[n][0-V]中枚举最大值即可,res=max(f[n][0-V])
\quad 假设我们将前i-1个物品已计算完毕,考虑第i个物品,体积为j时,第i个物品只有两种选择,即选或者不选。

  • 若不选,则相当于只考虑前i-1个物品且体积为j的情况,这种情况下f[i][j]=f[i-1][j]
  • 若选,则相当于背包容量只剩下j-v[i],这种情况下f[i][j]=f[i-1][j-v[i]]+w[i]

\quad 最终结果就相当于在这两种情况下取个最大值即可。还有个问题就是其初始化,很简单,就是没有物品给你选择,背包容量为0时f[0][0]=0
\quad 时间复杂度和空间复杂度都为 O ( n V ) O(nV) O(nV)
程序(CPP)

#include <iostream>
#include <algorithm>
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];
    
    f[0][0] = 0;
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= m; j++)
        {
            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]);
        }
    
    int res = 0;
    for(int i = 0; i <= m; i++)
        res = max(res, f[n][i]);
    cout << res << endl;
    return 0;
}

思路2:滚动数组优化为一维
程序(CPP)

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

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

二、完全背包

要求:每种物品可以选0个或者无限个.
题目描述:(题目链接)
\quad N N N 件物品和一个容量是 V V V 的背包,每种物品都有无限件可用。第 i i i 件物品的体积是 v i v_i vi,价值是 w i w_i wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

输入格式
\quad 第一行两个整数, N , V N,V NV,用空格隔开,分别表示物品数量和背包容积。接下来有 N N N 行,每行两个整数 v i , w i v_i,w_i vi,wi,用空格隔开,分别表示第 i i i 件物品的体积和价值。

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

数据范围
0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例

10

思路1
\quad f[i][j]表示只看前i物品,总体积是j的情况下总价值最大是多少。最大值答案就在f[n][0-V]中枚举最大值即可,res=max(f[n][0-V])
\quad 假设我们将前i-1个物品已计算完毕,考虑第i个物品,体积为j时,第i个物品只有多种选择,即选0个,1个,k个。我们可以再加上一重循环,枚举每个物品能选的个数k,注意k的范围为(int k=0; k*v[i]<=j; k++)
程序(CPP):

#include <iostream>
using namespace std;

const int N = 1010;
int f[N][N], 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]);
    
    int res = 0;
    for(int i = 0; i <= m; i++) res = max(res, f[n][i]);
    cout << res << endl;
    return 0;
}

\quad
思路2:利用滚动数组优化到一维,同01背包优化思想

#include <iostream>
using namespace std;

const int N = 1010;
int f[N], 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 = m; j >= v[i]; j--)
            for(int k = 0; k * v[i] <= j; k++)
                f[j] = max(f[j], f[j - k*v[i]] + k*w[i]);
    
    cout << f[m] << endl;
    return 0;
}

思路3
\quad f[i]表示总体积是i的情况下最大价值是多少,最终答案就是max(f[i]),其实也就是f[最大体积],因为体积越大肯定能装下物品的最大价值越高,至少不会降低。这里面体积j从小到大枚举表示f[i]可能从第i个物品转移过来;从大到小枚举的话表示只能从i-1个物品转移过来。因此完全背包相对于01背包只需要体积从小到大枚举。

程序(CPP)

#include <iostream>
using namespace std;

const int N = 1010;
int f[N], 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;
}

Note:如果题目问的是恰好用了m的体积,问最大价值?这时候只需要初始化的时候除了f[0]=0外其他赋值为-inf即可。

三、多重背包

要求:每种物品能选择的个数给个限制。
思路1:直接枚举每种物品能选择的个数k即可。

#include <iostream>
using namespace std;

const int N = 110;
int f[N][N];

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

思路2:滚动数组优化空间

#include <iostream>
using namespace std;

const int N = 110;
int f[N];

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

思路3:二进制优化
\quad 假设有a个物品b,那么我们将物品b拆分为a份,每一份看作一个单独的物品,这样就将多重背包转化为01背包啦!但我们需要按照一份一份拆分吗?其实是不必的,比如有7个物品b,那么我们只需要拆分为1个b,2个b,4个b,这样我们能表示出任意一个物品b,假设我们选择6个b,那么就是4+2。即给定n个物品b,我们根据二进制拆分法能拆出来log(n)向上取整个数,这些数能表示出[0-n]中任意一个数。借助于这个思想,我们就可以进行优化,假设有n个物品,背包容量为V,每种物品个数不超过num,则时间复杂度由暴力的 O ( n V ∗ n u m ) O(nV*num) O(nVnum)变为 O ( n V ∗ l o g ( n u m ) ) O(nV*log(num)) O(nVlog(num))
\quad 这里还有个问题,如何找出 l o g ( n ) 向 上 取 整 log(n)向上取整 log(n)个数使得这些数的组合能表示[0,n]中任意一个数。假设 n = 10 n=10 n=10,我们取前三个数为1,2,4,最后一个数不能是8,因为是8的话就能表示出[0-15]间任意一个数,而每种物品个数不能超过10。那最后一个数怎么选呢?其实这个数就是10-1-2-4=3,因为1,2,4能表示出[0-7]间任意一个数,再拿出一个数3,与[0-7]间数相加,就可以得到[8-10]间任意一个数且不会超过10,因此1,2,4,3这4个数,就可以拼出任意[0-10]区间的数。
程序(CPP)

#include <iostream>
#include <vector>
using namespace std;

const int N = 2010;
int f[N];
struct Good
{
    int v, w;
};
int main()
{
    int n, m; cin >> n >> m;
    vector<Good> goods;
    for(int i = 1; i <= n; i++)
    {
        int v, w, s; cin >> v >> w >> s;
        // 二进制拆分物品
        for(int k = 1; k <= s; k *= 2)
        {
            s -= k;
            goods.push_back({v*k, w*k});
        }
        if(s>0) goods.push_back({v*s, w*s});
    }
    
    // 01背包
    for(auto good: goods)
        for(int j = m; j >= good.v; j--)
            f[j] = max(f[j], f[j - good.v] + good.w);
    cout << f[m] << endl;
    return 0;
}

四、混合背包问题

要求:每种物品的个数分为多种情况。如下图所示:
在这里插入图片描述在这里插入图片描述
\quad 其实这个题跟多重背包完全一样,无非就是要设置下每种物品个数上限,最朴素的,直接按照多重背包的不优化版写出程序如下:

#include <iostream>
using namespace std;

const int N = 1010;
int f[N];

int main()
{
    int n, m; cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int v, w, s; cin >> v >> w >> s;
        if(s==-1) s = 1;  // 设置物品数上限为1
        else if(s==0) s = 1010;  // 设置物品无限个,不超过背包体积
        for(int j = m; j >= v; j--)  //
            for(int k = 0; k <= s && k * v <= j; k++)
                f[j] = max(f[j], f[j - k * v] + k * w);
    } 
    
    cout << f[m] << endl;
    return 0;
}

\quad 利用二进制优化才能过这个题,如下:

#include <iostream>
#include <vector>
using namespace std;

const int N = 1010;
int f[N];
struct Good
{
    int v, w;
};
int main()
{
    vector<Good> goods;
    int n, m; cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int v, w, s; cin >> v >> w >> s;
        if(s==-1) s = 1;
        else if(s==0) s = 1010;
        for(int k = 1; k <= s; k *= 2)
        {
            s -= k;
            goods.push_back({k * v, k * w});
        }
        if(s>0) goods.push_back({s * v, s * w});
    } 
    
    
    // 01背包
    for(auto good: goods)
        for(int j = m; j >= good.v; j--)
            f[j] = max(f[j], f[j - good.v] + good.w);
    cout << f[m] << endl;
    return 0;
}

五、二维费用的背包问题

要求:一维背包问题可能只有一个限制,比如只有背包容量的限制;二维的话有两个限制,比如背包容量和物品总重量限制。来个题就明白啦:
在这里插入图片描述
样例

输入:
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
输出:
8

思路
\quad f[i][j]表示体积是i,重量是j时最大价值。第一重循环枚举物品,第二重循环枚举体积,第三重循环枚举重量。因为是01背包,故而都是从大到小枚举。程序如下:

#include <iostream>
using namespace std;

const int N = 1010;
int f[N][N];

int main()
{
    int n, V, M; cin >> n >> V >> M;
    for(int i = 1; i <= n; i++)
    {
        int v, m, w; // 体积、重量、价值
        cin >> v >> m >> w;
        for(int j = V; j >= v; j--)
            for(int k = M; k >= m; k--)
                f[j][k] = max(f[j][k], f[j - v][k - m] + w);
    }
    cout << f[V][M] << endl;
    return 0;
}

六、分组背包问题

要求:把物品分成若干组,每组里面最多选一件,来个题目:
在这里插入图片描述在这里插入图片描述
样例

输入:
3 5
2
1 2
2 4
1
3 4
1
4 5
输出:
8

思路
\quad f[j]表示在j体积下的最大价值。与01背包类似,第一重循环物品,第二重循环从大到小循环体积,然后依次把该组的每一个物品要么放进去要么不放进去。假设每一组有s个物品,则

for(int i = 1; i <= n; i++)
	for(int j = m; j >= v; j--)
		f[j] = max(f[j], f[j - v[0]]+w[0],..., f[j - v[s-1]]+w[s-1])

程序

#include <iostream>
using namespace std;

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

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

更好理解的版本

#include <iostream>
using namespace std;

const int N = 110;
int f[N][N], v[N], w[N];
// 参考多重背包,每个背包最多可以被用si次,这里相当于每组背包最多用1次
// f[i][j] 表示只用前i组物品和体积为j的情况下最多能装下多少价值的物品
// f[i][j] = max(f[i - 1][j], f[i - 1][j - 第i组的某个物品] + 该物品的价值)

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

七、背包问题求方案数

在这里插入图片描述

输入样例

4 5
1 2
2 4
3 4
4 6

输出样例

2

思路
\quad 求方案数的时候就不能再像之前定义f[j]表示体积不超过j的情况下最大价值,而应该表示为体积恰好为j的情况下的最大价值,我们可以在初始化的时候将f[j]除了f[0]=0外其他全部初始化为负无穷。同时还需要新增加一个数组g[j],表示体积恰好为j的情况下得到最大价值的方案数。g[j]更新方案如下所述:

  • g[0]=1
  • f[j]=max(f[j], f[j-v[i]]+w[i]),当f[j]f[j]转移而来时更新g[j]+=g[j],当其由f[j-v[i]]+w[i]转移而来时更新g[j]+=g[j-v[i]],当二者相同时g[j]+=g[j]+g[j-v[i]]
  • 求背包实现最大价值时的方案数,自然先求出背包所装物品的最大价值maxW,再求出用了多大的体积j实现了最大价值,最后答案就是这些实现最大价值的体积下对应的方案数之和
#include <iostream>
using namespace std;

const int N = 1010;
const int mod = 1e9+7, INF = 1e9;
int f[N]; // f[N]表示恰好体积是j的情况下最大价值
int g[N]; // g[N]表示体积是j的情况下最大方案数

int main()
{
    int n, m; cin >> n >> m;
    g[0] = 1;  // 初始方案数赋值
    for(int i = 1; i <= m; i++) f[i] = -INF;  // 赋值为负无穷才能得到体积恰好为j时的最大价值
    for(int i = 1; i <= n; i++)
    {
        int v, w; cin >> v >> w;
        for(int j = m; j >= v; j--)
        {
            int t = max(f[j], f[j-v]+w);
            int s = 0;  // 记录当前体积下方案数
            if(t==f[j]) s += g[j];  // 是从体积j转移而来
            if(t==f[j-v]+w)  s += g[j-v];  // 是从体积j-v转移而来
            s = s % mod;
            f[j] = t, g[j] = s;
        }
    }
    int maxW = 0;  // 搜素最大价值
    for(int i = 0; i <= m; i++) maxW = max(maxW, f[i]);
    int res = 0;  // 记录总的方案数
    for(int i = 0; i <= m; i++)
    {
        if(maxW == f[i])  // 当前体积下得到最大价值
        {
            res += g[i];
            res %= mod;
        }
    }
    cout << res << endl;
    return 0;
}

八、最优物品选择方案

在这里插入图片描述
输入样例

4 5
1 2
2 4
3 4
4 6

输出样例

2

思路:见博客

九、有依赖的背包问题

要求:选一件物品前必须选其依赖的物品

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值