2020秋招_动态规划之背包问题学习总结

参考

博客:背包问题九讲
视频:背包九讲专题
练习:AcWing题库

0-1背包问题

O ( V × N ) O(V\times N) O(V×N)
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
状态转移方程: d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i i i 件物品,容量为 j j j 的背包,可获得最大的总价值。
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − c [ i ] ] + w [ i ] ) , if j > = c [ i ] d p [ i ] [ j ] = d p [ i − 1 ] [ j ] , if j < c [ i ] dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]]+w[i]), \quad \text{if}\quad j >= c[i] \\ dp[i][j] = dp[i-1][j], \quad \text{if}\quad j < c[i] dp[i][j]=max(dp[i1][j],dp[i1][jc[i]]+w[i]),ifj>=c[i]dp[i][j]=dp[i1][j],ifj<c[i]

初始化状体:容量为 V 的背包不要求装满,第一行和第一列的状态均为0,即 d p [ 0 ] [ j ] = 0 , d p [ i ] [ 0 ] = 0 dp[0][j]=0,dp[i][0]=0 dp[0][j]=0dp[i][0]=0;容量为V的背包要求装满, d p [ 0 ] [ 0 ] = 0 dp[0][0] = 0 dp[0][0]=0,其余状态设置为负无穷,这样确保每一个有效的状态值(大于等于0)都是从 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0] 转移过来的,即装入的物品体积为背包的大小。
模板题:2. 01背包问题

#include<iostream>
#include<vector>
using namespace std;
int main(){
int N,V;
cin >> N >> V;
vector<int> v(N+1);
vector<int> w(N+1);
for(int i = 1; i <= N; i++){
    cin >> v[i] >> w[i];
}

vector<int> dp(V+1,0);

for(int i = 1; i <= N; i++){
    for(int j = V; j >= v[i]; j--){
        dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
    }
}
cout << dp[V] << endl;
return 0;
}

0-1背包不超过背包体积下的最大重量

1049. 最后一块石头的重量 II
问题转化:

  • 将石头分成两堆,使两堆石头的重量差最小。
  • 进一步转化问题为前 i 块石头均可以选择放入或者不放入背包,背包的重量不超过所有石头重量总和的一半,求可以获得的最大重量。
  • 0-1背包问题,本题目背包的体积和物品的价值均是值石头的重量。

二维状态: d p [ i ] [ j ] dp[i][j] dp[i][j]表示已经对前 i 块石头做出选择,背包可以容纳的重量为 j 时,背包的最大重量。

int lastStoneWeightII(vector<int>& stones) {
    int sum = 0;
    for(int stone: stones){
        sum += stone;
    }
    int v = sum/2; // 必须向下取整
    int n = stones.size();
    vector<vector<int>> dp(n+1,vector<int>(v+1,0)); // 初始化全部为0
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= v; j++){
            // 注意stones的下标是从0开始的,所以使用i-1!
            if(j>=stones[i-1]){
                dp[i][j] = max(dp[i-1][j],dp[i-1][j-stones[i-1]]+stones[i-1]);
            }else{
                dp[i][j] = dp[i-1][j];
            }
        }
    }
    return sum-2*dp[n][v];
}

思考两个问题:

  • i i i j j j 的循环能不能交换? -> 可以
  • 为什么 d p [ n ] [ v ] dp[n][v] dp[n][v]就是不超过重量 v v v的最大重量?-> 与状态初始化有关,因为所有状态都被初始化成了0

状态降成一维:

  int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;
        for(int stone: stones){
            sum += stone;
        }
        int v = sum/2; // 必须向下取整
        int n = stones.size();
        vector<int> dp(v+1,0); // 初始化全部为0

        for(int i = 1; i <= n; i++){
            for(int j = v; j >= stones[i-1] ; j--){
                // 注意stones的下标是从0开始的,所以使用i-1!
                dp[j] = max(dp[j],dp[j-stones[i-1]]+stones[i-1]);
            }else{
                dp[j] = dp[j];
            }
        }

        return sum-2*dp[v];
    }

dp[j] = max(dp[j],dp[j-stones[i-1]]+stones[i-1]);中左边的dp[j]表示对第 i 件物品做出选择后的状态,右边的dp[j]表示对第 i-1 件物品做出选择后的状态。如果 j 是由1开始递增遍历,则右边的dp[j]会被 i 的状态下的dp[j]覆盖;可以让 j 从大到小开始遍历,由于 j - stones[i-1]必然小于 j ,因此不会出现状态的覆盖(覆盖的dp[j]是不需要被用到的)。

思考:

  • 状态降成一维后 i i i j j j 的循环能不能交换?-> 不可以,因为降成一维后,只能记录 i 的 上一个状态,因此 i 必须放在外层循环。

0-1背包恰好装满问题

416. 分割等和子集
选择子集和等于所有元素值和的一半。
d p [ i ] [ j ] dp[i][j] dp[i][j]表示已经对前 i 个元素做出选择,选择的元素的和是否可以为 j,true表示可以,false表示不可以。(下标从1开始)
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] ∣ d p [ i − 1 ] [ j − n u m s [ i ] ] dp[i][j] = dp[i-1][j] \quad | \quad dp[i-1][j-nums[i]] dp[i][j]=dp[i1][j]dp[i1][jnums[i]]
二维状态:

    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for(int num : nums){
            sum += num;
        }
        if(sum%2==1){
            return false;
        }
        sum/=2;
        int n = nums.size();
        vector<vector<bool>> dp(n+1,vector<bool>(sum+1,false));
        dp[0][0] = true;
        // 0-1背包,O(sum*n),sum为数组的元素和的一般,n为数组元素的个数2. 01背包问题2. 01背包问题
        for(int j = 1; j <= n; j++){
            // nums下标从0开始,所以用j-1
            for(int i = 0; i <= sum; i++){
                if(i >= nums[j-1]){
                    dp[j][i] = dp[j-1][i] | dp[j-1][i-nums[j-1]];
                }else{
                    dp[j][i] = dp[j-1][i];
                }
                
            }
        }
        return dp[n][sum];
    }

此处要求背包刚好装满,所以初始设置状态的时候只设置 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0] 为true,其余为false。
状态降成一维:

    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for(int num : nums){
            sum += num;
        }
        if(sum%2==1){
            return false;
        }
        sum/=2;
        vector<bool> dp(sum+1,false);
        dp[0] = true;
        // 0-1背包,O(sum*n),sum为数组的元素和的一般,n为数组元素的个数
        for(int j = 0; j < nums.size(); j++){
            for(int i = sum; i >= nums[j]; i--){
                dp[i] = dp[i] | dp[i-nums[j]];
            }
        }
        return dp[sum];
    }

完全背包问题

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

模板题:3. 完全背包问题

#include<iostream>
#include<vector>
using namespace std;
int main(){
    int N,V;
    cin >> N >> V;
    vector<int> v(N+1);
    vector<int> w(N+1);
    for(int i = 1; i <= N; i++){
        cin >> v[i] >> w[i];
    }
    vector<int> dp(V+1,0);
    for(int i = 1; i <= N; i++){ // 前i件物品
        for(int j = v[i]; j <= V; j++){  // 背包容量j
            dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
        }
    }
    cout << dp[V] << endl;
    return 0;
}

与0-1背包问题最大的区别在于内层循环 j 从小到大遍历。j 从小到大遍历,等号右边的dp[j]为已经更新的状态,即表示不断地在放入第i件物品,直到背包放不下。0-1背包中 j 从大到小遍历,等号右边的 dp[j] 为 i-1 时的状态。

多重背包

O ( V × N × s [ i ] ) O(V\times N \times s[i]) O(V×N×s[i])
有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。
模板题:4. 多重背包问题 I

#include<iostream>
#include<vector>
using namespace std;
int main(){
int N,V;
cin >> N >> V;
vector<int> v(N+1);
vector<int> w(N+1);
vector<int> s(N+1);
for(int i = 1; i <= N; i++){
    cin >> v[i] >> w[i] >> s[i];
}
vector<int> dp(V+1,0);
for(int i = 1; i <= N; i++){
    for(int j = V; j >= v[i]; j--){
        for(int k = 1; k <= s[i]; k++){
            if(j>=k*v[i]){
                dp[j] = max(dp[j],dp[j-k*v[i]]+k*w[i]);
            }
        }
    }
}
cout << dp[V] << endl;
return 0;
}

在0-1背包的基础上多了一层循环,用于第 i 件物品选择不同次数计算背包中的价值。

二进制优化方法

多重背包问题可以转成0-1背包问题。
最简单的做法是把第i种物品的s[i]件转化体积均为v[i]的s[i]种物品,保证了可以取到所有的情况。但是这样并没有减少时间复杂度。
可以用二进制来表示,如7件物品,用3位二进制就可以表示取0,1,2,3,4,5,6,7件的所有情况。

模板题:5. 多重背包问题 II

#include<iostream>
#include<vector>
using namespace std;
struct Good{
    int v, w;
    Good(int _v, int _w): v(_v), w(_w){}
};

int main(){
    int N,V;
    int v,w,s;
    cin >> N >> V;
    vector<Good> goods;
    for(int i = 0; i < N; i++){
        cin >> v >> w >> s;  
        // i的把s件物品转化成体积为v[i],2*v[i],4*v[i],8*v[i],...的log(s)件物品
        for(int k = 1; k <= s; k*=2){
            s -= k;
            goods.push_back(Good(k*v,k*w));
        }
        // 把剩余的s件物品合成一种物品
        if(s>0){
            goods.push_back(Good(s*v,s*w));
        }
    }
    
    vector<int> dp(V+1,0);
    for(Good good: goods){
        for(int i = V; i >= good.v; i--){
            dp[i] = max(dp[i],dp[i-good.v]+good.w);
        }
    }
    cout << dp[V] << endl;
}

单调队列优化

混合背包

模板题:7. 混合背包问题
思路:将背包问题分成两类,0-1背包和完全背包。把多重背包转化为0-1背包。

#include<bits/stdc++.h>

using namespace std;

struct Good{
    int v,w;
    bool flag;
    Good(int _v, int _w, bool _flag):v(_v),w(_w),flag(_flag){}
};


int main(){
    int N,V;
    cin >> N >> V;
    vector<Good> goods;
    for(int i = 0; i < N; i++){
        int v,w,s;
        cin >> v >> w >> s;
        if(s == -1){
            goods.push_back(Good(v,w,true));
        }else if(s == 0){
            // false 表示是完全背包
            goods.push_back(Good(v,w,false));
        }else{
            for(int k = 1; s >= k; k*=2){
                s -= k;
                goods.push_back(Good(k*v,k*w,true));
            }
            if(s > 0){
                goods.push_back(Good(s*v,s*w,true));
            }
        }
    }
    
    vector<int> dp(V+1,0); 
    for(int i = 0; i < goods.size(); i++){
        if(goods[i].flag){
            for(int j = V; j >= goods[i].v; j--){
                dp[j] = max(dp[j],dp[j-goods[i].v]+goods[i].w);
            } 
        }else{
            for(int j = goods[i].v; j <= V; j++){
                dp[j] = max(dp[j],dp[j-goods[i].v]+goods[i].w);
            }
        }

    }
    cout << dp[V] << endl;
    return 0;
}

二维费用的背包问题

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

模板题:8. 二维费用的背包问题

#include <bits/stdc++.h>

using namespace std;

struct Good{
  int v,m,w;
  Good(int _v, int _m, int _w):v(_v),m(_m),w(_w){}
};

int main(){
    int N,V,M;
    cin >> N >> V >> M;
    vector<Good> goods;
    for(int i = 0 ; i < N ; i++){
        int v,m,w;
        cin >> v >> m >> w;
        goods.push_back(Good(v,m,w));
    }
    vector<vector<int>> dp(V+1,vector<int>(M+1));
    for(int i = 0; i < N; i++){
        for(int j = V; j >= goods[i].v; j--){
            for(int k = M; k >= goods[i].m; k--){
                dp[j][k] = max(dp[j][k],dp[j-goods[i].v][k-goods[i].m]+goods[i].w);
            }
        }
    }
    cout << dp[V][M] << endl;
}

分组背包问题

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

模板题:9. 分组背包问题

#include <bits/stdc++.h>

using namespace std;

int main(){
    int N,V;
    cin >> N >> V;
    vector<vector<pair<int,int>>> goods;
    for(int i = 0; i < N; i++){
        int x;
        cin >> x;
        vector<pair<int,int>> good;
        for(int j = 0; j < x; j++){
            int v,w;
            cin >> v >> w;
            good.push_back(pair<int,int>(v,w));
        }
        goods.push_back(good);
    }
    vector<int> dp(V+1,0);
    for(int i = 0; i < N; i++){
            for(int j = V; j >= 0; j--){
                for(auto t : goods[i]){
                    if(j >= t.first){
                        dp[j] = max(dp[j],dp[j-t.first]+t.second);
                    }
                    
                }
                    
            }
        
    }
    cout << dp[V] << endl;
}

思考:多重背包问题是分组背包问题的一种特殊情况。
分组背包问题是物品分成 N 组,每次只能选每组中的 1 种物品,假设某个组内有 s 种物品,每种物品就一件,则一共有 s+1 种情况,即可以都不选,也可以选 i 号物品。
多重背包问题是每组就 1 种物品,但是每种物品有 s 件,则一共也是 s+1 种情况。
因此都是需要再加一层循环,多重背包是选择物品件数,分组背包是选择哪一种物品。(但是多重背包可以进行优化,分组背包更一般,只能用三层循环)

背包问题求方案数

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

#include <bits/stdc++.h>
using namespace std;

const int mod = 1e9+7;

int main(){
    int N,V;
    cin >> N >> V;
    vector<int> v(N);
    vector<int> w(N);
    for(int i = 0; i < N; i++){
        cin >> v[i] >> w[i];
    }
    // dp[j]为体积为j背包可以获得的最大价值(不一定装满)
    vector<int> dp(V+1,0); 
    // cnt[j]为体积为j的背包恰好装满的方案数
    vector<int> cnt(V+1,0); 
    cnt[0] = 1;
    for(int i = 0; i < N; i++){
        for(int j = V; j >= v[i]; j--){
            if(dp[j] < dp[j-v[i]]+w[i]){
                dp[j] = dp[j-v[i]]+w[i];
                cnt[j] = cnt[j-v[i]];
            }else if(dp[j] == dp[j-v[i]]+w[i]){
                cnt[j] = (cnt[j] + cnt[j-v[i]])%mod;
            }
        }
    }
    // 体积为V的背包可以获得的最大价值(不一定装满),即最优选法
    int maxw = dp[V];  
    int re = 0;
    for(int i = 1; i <= V; i++){
    	// 装满体积为i的背包可获得的价值为maxw的方案数累加
        if(dp[i]==maxw){
            re += cnt[i];
        }
    }
    cout << re << endl;
}

改进:将cnt的初始状态全部设置为1,表示状态可以从任意起点开始转移,因此最终cnt[j]求得的是最优选法的方案总数。

#include <bits/stdc++.h>
using namespace std;

const int mod = 1e9+7;

int main(){
    int N,V;
    cin >> N >> V;
    vector<int> v(N);
    vector<int> w(N);
    for(int i = 0; i < N; i++){
        cin >> v[i] >> w[i];
    }

    vector<int> dp(V+1,0); 
    // 初始状态全部设为1
    vector<int> cnt(V+1,1); 
    for(int i = 0; i < N; i++){
        for(int j = V; j >= v[i]; j--){
            if(dp[j] < dp[j-v[i]]+w[i]){
                dp[j] = dp[j-v[i]]+w[i];
                cnt[j] = cnt[j-v[i]];
            }else if(dp[j] == dp[j-v[i]]+w[i]){
                cnt[j] = (cnt[j] + cnt[j-v[i]])%mod;
            }
        }
    }
    cout << cnt[V] << endl;
}

背包问题求具体方案

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

思路:https://www.acwing.com/solution/content/2687/
题目要求输出字典序最小的解,假设存在一个包含第 1 个物品的最优解,为了确保字典序最小那么我们必然要选第一个。那么问题就转化成从 2~N 这些物品中找到最优解。之前的 f ( i , j ) f(i,j) f(i,j)记录的都是前 i 个物品总容量为 j 的最优解,那么我们现在将 f ( i , j ) f(i,j) f(i,j)定义为从第 i 个元素到最后一个元素总容量为 j 的最优解。接下来考虑状态转移:
f ( i , j ) = m a x ( f ( i + 1 , j ) , f ( i + 1 , j − v [ i ] ) + w [ i ] ) f(i,j)=max(f(i+1,j),f(i+1,j−v[i])+w[i]) f(i,j)=max(f(i+1,j),f(i+1,jv[i])+w[i])
两种情况,第一种是不选第 i 个物品,那么最优解等同于从第 i+1 个物品到最后一个元素总容量为 j 的最优解;第二种是选了第 i 个物品,那么最优解等于当前物品的价值 w[i] 加上从第 i+1 个物品到最后一个元素总容量为 j−v[i] 的最优解。

计算完状态表示后,考虑如何的到最小字典序的解。首先 f ( 1 , V ) f(1,V) f(1,V)肯定是最大价值,那么我们便开始考虑能否选取第 1 个物品。

如果 f ( 1 , m ) = f ( 2 , m − v [ 1 ] ) + w [ 1 ] f(1,m)=f(2,m−v[1])+w[1] f(1,m)=f(2,mv[1])+w[1],说明选取了第1个物品可以得到最优解。

如果 f ( 1 , m ) = f ( 2 , m ) f(1,m)=f(2,m) f(1,m)=f(2,m),说明不选取第一个物品才能得到最优解。

如果 f ( 1 , m ) = f ( 2 , m ) = f ( 2 , m − v [ 1 ] ) + w [ 1 ] f(1,m)=f(2,m)=f(2,m−v[1])+w[1] f(1,m)=f(2,m)=f(2,mv[1])+w[1],说明选不选都可以得到最优解,但是为了考虑字典序最小,我们也需要选取该物品。

#include <bits/stdc++.h>

using namespace std;

struct Good{
  int v,w;
  Good(int _v, int _w):v(_v),w(_w){}
};

int main(){
    int N,V;
    cin >> N >> V;
    vector<Good> goods;
    for(int i = 0; i < N; i++){
        int v,w;
        cin >> v >> w;
        goods.push_back(Good(v,w));
    }
    
    vector<vector<int>> dp(N+10,vector<int>(V+10,0));
    for(int i = N; i >= 1; i--){
        for(int j = 0; j <= V; j++){
            if(j >= goods[i-1].v && dp[i+1][j-goods[i-1].v]+goods[i-1].w > dp[i+1][j]){
                dp[i][j] = dp[i+1][j-goods[i-1].v]+goods[i-1].w;
            }else{
                dp[i][j] = dp[i+1][j];
            }
        }
    }
    // cout << dp[1][V] << endl;
    int vol = V;
    for(int i = 1; i <= N; i++){
        if(vol >= goods[i-1].v && dp[i+1][vol-goods[i-1].v]+goods[i-1].w == dp[i][vol]){
            cout << i << " "; 
            vol -= goods[i-1].v;
        }
    }
}

背包问题内外循环和初始状态

0-1背包问题思路就是每件物品选或者不选(多重背包也可以转化为0-1背包),在二维dp降成一维dp后,dp[j] 只记了对前 i 件物品做出选择的最大价值,记为状态 i 。
计算不同背包体积 j 下,对第 i+1 件物品进行选择后可以获得的最大价值。此时需要用到状态 i ,由于一维 dp[j] 只能记录一个状态的 i,因此第一层循环必须是物品种类 i 的循环。

518. 零钱兑换 II
题目:给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

分析:这是一个完全背包恰好装满求方案数问题
恰好装满 -> 初始状态应该设置成 dp[0]=1 ,其余为0。因此凑到总金额amount是从金额为0开始。
如果dp[1]=1,则dp[amount]可能由dp[1]转移而来,此时只凑了amount-1的金额。

完全背包 -> 第二层循环从1开始。

求方案数 -> 初始状态设置为1和0。(求最大价值初始状态设置为0和-inf)

    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1,0);
        dp[0] = 1;
        // 把coins放在外层循环,限制了coins使用顺序
        for(int k = 0; k < coins.size(); k++){
            for(int i = 1; i <= amount; i++){
                if(i-coins[k] >= 0){
                    dp[i] += dp[i-coins[k]];
                }   
            }
        }
        return dp[amount];
    }

322. 零钱兑换

    // 硬币i的体积为coins[i], 价值为1
    // 题目转化为完全背包问题恰好装满体积amount,价值最小
    int coinChange(vector<int>& coins, int amount) {
        int inf = 0x3f3f3f3f;
        vector<int> dp(amount+1,inf);
        dp[0] = 0;
        for(int k = 0; k < coins.size() ; k++){
            for(int i = coins[k]; i <= amount; i++){
                dp[i] = min(dp[i-coins[k]]+1,dp[i]);
            }
        }
        return dp[amount] == inf ? -1: dp[amount];
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值