背包问题

文章目录

    • 1.01背包
    • 2.完全背包问题
    • 3.多重背包问题
    • 4.混合背包问题
    • 5.二维费用的背包问题
    • 6.分组背包问题
    • 7.背包问题求方案数
    • 8.背包问题求具体方案

1.01背包

题目链接

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。

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

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

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

数据范围 0 输入样例 :
4 5
1 2
2 4
3 4
4 5
输出样例: 8

f[i][j]:只看前i个物品,总体积是j,总价值最大是多少
res = max{f[n][0…v]},f[i][j]这里是前i件物品的一个子集,最终结果要在所有可能的体积中选取最大的,(如果开始时数组初始化为0,那f[n][v]就是最终结果)

转移方程:
f[i][j] = max{1,2},
1.不选第i件物品,f[i][j] = f[i-1][j]
2.选第i个物品,f[i][j] = f[i - 1][j - v[i]] + w[i];
f[0][0] = 0;

时间复杂度:O(N^2)
空间复杂度:O(N^2)

#include 

using namespace std;

const int N = 1010;

int n, m;
int f[N][N];
int v[N], w[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){
            f[i][j] = f[i - 1][j];
            if(j >= v[i]){
                f[i][j] = max(f[i - 1][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;
}

空间优化
定义的动态规划数组,每一行的状态都只和上一行的状态有关,所以这里采用一维数组来保存上一行的状态,为了不覆盖上一行的状态这里采取从m到0逆序来计算。

说明:这里求解的f[m]就是最终的结果,即容量为m时,背包的最大价值。而不用在f[0…m]中取最大的。

特别说明 对于降维成一维的情况:
1.f[i] = 0(所有的都初始化为0), 最终结果是f[m]
2.f[0] = 0, f[i] = -INF i != 0(只有f[0]初始化为0),(其实这里求得是容量恰好为i时的最大价值,所以最终结果要取其中最大的)最终结果是max{f[0…m]}

初始化细节问题:
1.如果题目要求是恰好装满背包时的最大价值,初始化的时候需要将f[0]初始化为0,其余初始化为-INF,可以这样理解:当背包容量为0时,恰好装满背包只能用体积为0的物品来装,那其余的容量的背包均没有合法的解,属于未定义的状态将其初始化为-INF。最终结果是f[m]
2.如果题目要求是不超过背包的容量时的最大价值,初始化时将f[0…m]都初始化为0,可以这样理解:只要不超过背包的容量就可以,那每一个背包都有一个合法的解“什么都不装”,价值为0,也就是初始状态将背包的价值置为0。最终结果是f[m]
本题属于第2种情况

#include 

using namespace std;

const int N = 1010;

int n, m;
int f[N];
int v[N], w[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;
}

2.完全背包问题

题目链接

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。

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

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

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

数据范围 0 输入样例
4 5
1 2
2 4
3 4
4 5
输出样例: 10

对比01背包问题,状态依然是物品和重量,只不过选择的情况多了,01背包只考虑k=0,1的情况,完全背包要考虑所有的k

#include 

using namespace std;

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

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];
    return 0;
}

优化空间复杂度:

#include 

using namespace std;

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

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

基于01背包优化空间复杂度的写法进行变形
01背包优化空间复杂度是将数组降维成一维,为了使当前的结果不覆盖之前的结果,采取逆序遍历的方式。
完全背包问题是每种物品的个数不加以限制,因此,这里可以不用考虑覆盖的情况。也就是说:01背包是基于前i-1个物品进行计算,而完全背包是考虑的前i个物品。

  • 01背包转移方程:f[j] = max(f[j], f[j - v[i]] + w[i]); f[j - v[i]]不包含第i件物品
  • 完全背包转移方程:f[j] = max(f[j], f[j - v[i]] + w[i]); f[j - v[i]]包含第i件物品
#include 

using namespace std;

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

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];
    return 0;
}

上述思想的二维数组形式,针对01背包原始写法的改进

  • 01背包的状态转移方程:f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);f[i - 1][j - v[i]] + w[i]考虑的是不选择第i件物品的最大价值加上当前物品的价值,保证该物品只选择1件
  • 01背包的状态转移方程:f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);f[i][j - v[i]] + w[i]考虑的是选择第i件物品的最大价值加上当前物品的价值,该物品可多次选择

注:将该写法降维可得上述写法

#include 

using namespace std;

const int N = 1010;

int n, m;
int f[N][N];
int v[N], w[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){
            f[i][j] = f[i - 1][j];
            if(j >= v[i]){
                f[i][j] = max(f[i - 1][j], f[i][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;
}

3.多重背包问题

题目链接

有 N 种物品和一个容量是 V 的背包。 第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。

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

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

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

数据范围 0 输入样例
4 5
1 2
3 2
4 1
3 4
3 4
5 2
输出样例:
10

多重背包问题是01背包问题的扩展,01背包对于当前物品只有两种选择,多重背包给定了当前物品的最多选择的件数

#include 

using namespace std;

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

int main(){
    cin >> n >> m;
    for(int i = 0; i < n; ++i){
        int v, w, s;
        cin >> v >> w >> s;
        for(int j = m; j >= 0; --j){//依然要逆序遍历
            for(int k = 1; k <= s && k * v <= j; ++k){
                f[j] = max(f[j], f[j - k * v] + k * w);
            }
        }
    }
    cout << f[m];
    return 0;
}

多重背包问题的优化:
基本思想:将多重背包问题转化成01背包问题

每个物品都有s份,可以将同一件物品的s份分成不同的s件物品,意思就是说:01背包问题是每个物品都有一份,那我们将这s件物品都放到数组内,就变成了01背包问题。
如果我们按照将这s份物品,都拆成一件一件的,转化成01背包问题,就类似于:我们把7拆成7个1,然后按照01背包的问题来求解。这样转化正确的原因在于:由于将当前物品放入背包中可能放入的是k件,k<=7,任何一个小于k的数都能够用所拆分成的7个1来表示。那有没有更高效的办法呢?
现在的问题是:将7至少拆分成几个数,使得这几个数都能组合成任意小于等于7的数?
乘法原理拆分: 每个数可以选或者不选,至少用几个数可以将这个数s表示出来, log(s)
7 = 1 + 2 + 4,任意小于等于7的数都能够用1,2,4这三个数来组成。10 = 1 + 2 + 4 + 3,任意小于等于10的数都能够用1,2,3,4这4个数来组成,这四个数取或者不取,
0 = 全都不取
1 = 1
2 = 2
3 = 3
4 = 4
5 = 1 + 4
6 = 2 + 4
……
总结:用乘法原理进行拆分,将拆分好的数目组成新的物品,物品的价值为w[i] * k,体积为v[i] * k,将拆分好的物品放入物品列表中,按照01背包的思想进行计算

#include 

using namespace std;

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

4.混合背包问题

题目链接

有 N 种物品和一个容量是 V 的背包。
物品一共有三类:
第一类物品只能用1次(01背包);
第二类物品可以用无限次(完全背包);
第三类物品最多只能用 si 次(多重背包);

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

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

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

si=−1 表示第 i 种物品只能用1次; si=0 表示第 i 种物品可以用无限次; si>0 表示第 i 种物品可以使用 si 次;
输出格式 输出一个整数,表示最大价值。

数据范围 0 输入样例
4 5
1 2 -1
2 4 1
3 4 0
4 5 2
输出样例: 8

混合背包问题是01背包、完全背包、多重背包问题的综合,可以将这三类看成两类,也就是将多重背包转化成01背包,这样可分别对01背包和完全背包这两类问题进行求解

#include

using namespace std;

struct Good{
    int kind;
    int v, w;
};
const int N = 1010;
int f[N];
int n ,m;

int main(){
    cin >> n >> m;
    vector<Good> goods;
    for(int i = 0; i < n; ++i){
        int v, w, s;
        cin >> v >> w >> s;
        if(s == -1)
            goods.push_back({-1, v, w});
        else if(s == 0)
            goods.push_back({0, v, w});
        else{
            for(int k = 1; k <= s; ++k){
                s -= k;
                goods.push_back({-1, v * k, w * k});
            }
            if(s)
                goods.push_back({-1, v * s, w * s});
        }
    }
    for(auto g : goods){
        if(g.kind == -1){
            for(int i = m; i >= g.v; --i)
                f[i] = max(f[i], f[i - g.v] + g.w);
        }
        else{
            for(int i = g.v; i <= m; ++i){
                f[i] = max(f[i], f[i - g.v] + g.w);
            }
        }
    }
    cout << f[m];
    return 0;
}

5.二维费用的背包问题

题目链接

有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。 每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。 求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。 输出最大价值。

输入格式 第一行两个整数,N,V,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。

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

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

数据范围 0 输入样例
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
输出样例: 8

二维费用的背包问题是01背包问题的扩展,01背包只要求不超过背包的最大容量,二维费用的背包要求不超过背包的最大容量以及背包的重量。
具体实现方法:

  • 在01背包的基础上增加一维来表示重量的状态,求解过程和容量类似
#include

using namespace std;

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

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

6.分组背包问题

题目链接

有 N 组物品和一个容量是 V 的背包。 每组物品有若干个,同一组内的物品最多只能选一个。 每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。 求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。 输出最大价值。

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

接下来有 N 组数据:

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

数据范围 0 输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8

分组背包问题其实和多重背包问题类似,多重背包是分组背包的一个特殊情况,特殊在将一个物品的s件看成一个组,组内的物品有每个有1…s件,在这其中选一种。分组背包中组内的物品是不同的,依然是组内物品选择一个。
对比01背包问题:

  • 分组背包在状态上和01背包一样,物品和容量
  • 在选择上,01背包是指当前物品选或者不选,分组背包是指这组内的物品选哪一个
#include 

using namespace std;

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

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

7.背包问题求方案数

题目链接

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出 最优选法的方案数。注意答案可能很大,请输出答案模> 10^9+7 的结果。

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

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

输出格式 输出一个整数,表示 方案数 模 109+7 的结果。

数据范围 0 输入样例
4 5
1 2
2 4
3 4
4 6
输出样例: 2

该问题是求01背包问题的最优方案数
具体做法:

  • 本文中的01背包问题求得是不超过最大容量时的最优解,初始时将数组初始化为0,数组的最后一个值就是最优解
  • 那本题是求最优方案数,最优方案可能会在求解的过程中出现,那为了便于统计,这里数组求恰为该容量时的最优解,那么最终的结果是数组中的最大值,求解的过程中同时统计该容量时该最优解的方案数
#include 

using namespace std;

const int N = 1010, INF = 1000000;
int n, m;
int f[N],g[N];//f统计最大价值,g统计最大价值的方案数

int main(){
    cin >> n >> m;
    int res = 0;
    for(int i = 1; i <= m; ++i)//统计的是体积恰为j时的最大价值
        f[i] = -INF;
    g[0] = 1;
    for(int i = 0; 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];
            if(t == f[j - v] + w)
                s += g[j - v];
            s %= 1000000007;  
            f[j] = t;
            g[j] = s;
        }
    }
    int max_w = 0;
    for(int i = 0; i <= m; ++i){
        max_w = max(max_w, f[i]);
    }
    for(int i = 0; i <= m; ++i){
        if(f[i] == max_w)
            res += g[i];
        res %= 1000000007; 
    }
    cout << res;
    return 0;
}

8.背包问题求具体方案

题目链接

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 字典序最小的方案。 这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。

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

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

输出格式 输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。

物品编号范围是 1…N。

数据范围 0 输入样例
4 5
1 2
2 4
3 4
4 6
输出样例:
1 4

该题目是求01背包问题的具体的方案,可能会有多种方案,这里要求输出字典序最小的。
具体做法:

  • 在求解的过程中保存当前物品的状态,定义长度为m+1的二维数组来保存当前的方案
#include 

using namespace std;

const int N = 1010;
int n, m;
int f[N];

bool cmp(vector<int> &num1, vector<int> &num2){
    int i;
    for(i = 0; i < num1.size() && i < num2.size(); ++i){
        if(num1[i] < num2[i])
            return true;
        else if(num1[i] > num2[i])
            return false;
    }
    if(i < num1.size())
        return false;
    else
        return true;
}

int main(){
    cin >> n >> m;
    vector<vector<int>> res(m + 1);
    for(int i = 0; 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);
            if(f[j] == f[j - v] + w){//当前物品不装入和装入结果一样,保存最小的
                vector<int> temp = res[j - v];
                temp.push_back(i);
                if(!cmp(res[j], temp))
                    res[j] = temp;
            }
            else if(t == f[j - v] + w){
                res[j] = res[j - v];
                res[j].push_back(i);
                f[j] = t;
            }
            //cout << t << endl;
        }
    }
    for(auto a : res[m]){
        cout << a + 1 << " ";
    }
    return 0;
}

说明:还有一种做法是,先求最优解再反推方案,那这就必须用二维数组,以便获得具体的方案。
由于这里要求按字典序输出,那么对于物品可以采取从后往前的方式来遍历,如果当前物品放入最终结果会使得方案最优,那就一定放入。
反推方案数的时候从第一个物品开始判断

#include 

using namespace std;

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

int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; ++i) cin >> v[i] >> w[i];
    for(int i = n; i > 0; --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 + 1][j], f[i + 1][j - v[i]] + w[i]);
        }
    }
    //int val = m;
    for(int i = 1; i <= n; ++i){//从前往后推方案
        if(m >= v[i] && f[i][m] == f[i + 1][m - v[i]] + w[i]){
            cout << i << " ";
            m -= v[i];
        }
    }
    return 0;
}

你可能感兴趣的:(#,算法)