背包九讲

穿精又带淫゛_ 提交于 2020-01-26 21:08:30

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

一、01背包

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

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

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

数据范围
0<N,V10000<N,V≤1000
0<vi,wi10000<v_i,w_i≤1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例

8

思路1:二维数组记录
\quadf[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(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个或者无限个.
题目描述:(题目链接)
\quadNN 件物品和一个容量是 VV 的背包,每种物品都有无限件可用。第 ii 件物品的体积是 viv_i,价值是 wiw_i。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

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

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

数据范围
0<N,V10000<N,V≤1000
0<vi,wi10000<v_i,w_i≤1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例

10

思路1
\quadf[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
\quadf[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(nVnum)O(nV*num)变为O(nVlog(num))O(nV*log(num))
\quad这里还有个问题,如何找出log(n)log(n)向上取整个数使得这些数的组合能表示[0,n]中任意一个数。假设n=10n=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

思路
\quadf[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

思路
\quadf[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;
}

七、背包问题求方案数

八、最优物品选择方案

九、有依赖的背包问题

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!