四种背包问题总结

01背包问题

问题描述:
有N件物品和一个容量为W的背包。第i件物品的费用(即体积,下同)是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
思路:
定义 d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j]从0到i+1个物品中选出总重量不超过j的物品时总价值的最大值。 d p [ 0 ] [ j ] = 0 dp[0][j]=0 dp[0][j]=0
递推关系式:
d p [ i + 1 ] [ j ] = { d p [ i ] [ j ] j < w [ i ]   m a x ( d p [ i ] [ j ] , d p [ i ] [ j − w [ i ] ] + v [ i ] ) 其他 dp[i+1][j]= \begin{cases} dp[i][j] & \text{$j<w[i]$ }\\ max(dp[i][j],dp[i][j-w[i]]+v[i]) & \text{其他} \end{cases} dp[i+1][j]={dp[i][j]max(dp[i][j],dp[i][jw[i]]+v[i])j<w[i] 其他
将动态规划问题视作填表过程:
(图与本题无关,摘自其他博主,仅作示例)
在这里插入图片描述
代码:

int dp[MAX_N+1][MAX_W+1];
void solve(){
	for(int i=0;i<n;i++){
		for(int j=0;j<=W;j++){
			if(j<w[i])
				dp[i+1][j]=dp[i][j];
			else 
				dp[i+1][j]=max(dp[i][j],dp[i][j-w[i]]+v[i]);
		}
	}
	cout<<dp[n][W];
}

但如果背包问题的规模扩大,例如当 1 ⩽ n ⩽ 100 1\leqslant n\leqslant 100 1n100, 1 ⩽ W ⩽ 1 0 9 1 \leqslant W \leqslant 10^9 1W109时,则上述的动态规划由于规模限制而无法进行。
可以更换思路,之前是针对不同重量计算最大价值,可以更改为,针对不同价值计算最小重量
定义dp[i+1][j]:前i个物品挑选出价值总和为j时总重量最小值(不存在时就令为充分大的数INF)
初始值为:
d p [ 0 ] [ 0 ] = 0 d p [ 0 ] [ j ] = I N F dp[0][0]=0\\ dp[0][j]=INF dp[0][0]=0dp[0][j]=INF
代码:

int dp[MAX_N+1][MAX_N*MAX_V+1];
void solve(){
	fill(dp[0],dp[0]+MAX_N*MAX*V+1,INF);
	dp[0][0]=0;
	for(int i=0;i<n;i++){
		for(int j=0;j<=MAX_N*MAX_V;j++){
			if(j<v[i])
				dp[i+1][j]=dp[i][j];
			else 
				dp[i+1][j]=min(dp[i][j],dp[i][j-v[i]]+w[i]);
		}
	}
	int res=0;
	for(int i=0;i<MAX_N*MAX_V;i++)if(dp[n][i]<=W)res=i;
	cout<<res;
}

法二:
通过重复利用一个数组:

只要我们在求 d p [ j ] dp[ j ] dp[j]时不覆盖 d p [ j − w [ i ] ] dp[ j - w[i] ] dp[jw[i]],那么就可以不断递推至所求答案。所以我们采取倒序循环.

int dp[MAX_W+1]void solve(){
	for(int i=0;i<n;i++){
		for(int j=W;j>=w[i];j--){
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}	
	}
	cout<<dp[W];
}

完全背包问题

思路:
因为同一种类的物品可以选择任意多件,所以相当于在内层增加了一个件数的循坏,寻找最优的件数选择。
但是三重循环的复杂度太高,因此进行改进,利用递推的特性,将循环部分替代成 d p [ i + 1 ] [ j − w [ i ] ] + v [ i ] dp[i+1][j-w[i]]+v[i] dp[i+1][jw[i]]+v[i],因为从 d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j]中选择k个的计算, k ⩾ 1 k\geqslant 1 k1的情况在 d p [ i + 1 ] [ j − w [ i ] ] dp[i+1][j-w[i]] dp[i+1][jw[i]]中已经计算完成了。
递推关系式:
d p [ 0 ] [ j ] = 0 d p [ i + 1 ] [ j ] = m a x { d p [ i ] [ j − k × w [ i ] ] + k × v [ i ] ∣ 0 ≤ k } dp[0][j]=0\\ dp[i+1][j]=max\{dp[i][j-k\times w[i]]+k\times v[i]|0\leq k\} dp[0][j]=0dp[i+1][j]=max{dp[i][jk×w[i]]+k×v[i]0k}
优化后的递推关系式:
d p [ i + 1 ] [ j ] = m a x { d p [ i ] [ j − k × w [ i ] ] + k × v [ i ] ∣ 0 ≤ k } ⟺ m a x ( d p [ i ] [ j ] , d p [ i + 1 ] [ j − w [ i ] ] + v [ i ] ) dp[i+1][j]=max\{dp[i][j-k\times w[i]]+k\times v[i]|0\leq k\} \Longleftrightarrow \\ max(dp[i][j],dp[i+1][j-w[i]]+v[i]) dp[i+1][j]=max{dp[i][jk×w[i]]+k×v[i]0k}max(dp[i][j],dp[i+1][jw[i]]+v[i])
代码:
注意:跟01背包非常相似。

void solve(){
	for(int i=0;i<n;i++){
		for(int j=0;j<=W;j++){
			if(j<w[i])
				dp[i+1][j]=dp[i][j];
			else 
				dp[i+1][j]=max(dp[i][j],dp[i+1][j-w[i]]+v[i]);
		}
	}
	cout<<dp[n][W];
}

法二:
同样利用一维数组

int dp[MAX_W+1];
void solve(){
	for(int i=0;i<n;i++){
		for(int j=w[i];j<=W;j++){
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
	}
	cout<<dp[W];
}

变式题1

oxx 和 xjj 决定和小伙伴们一同坐船前往 Xiamen。去 Xiamen 的船票一张 p 元。

当他们满怀兴致地来到港口时发现居然只有不设找零的自动售票机,只能使用一元,五元,十元,二十元,五十元,一百元的纸币,且一次至多买 k 张船票。因此他们不得不去银行取钱。而 oxx 是个大懒人,他希望取的纸币数量越少越好,因此他想知道他们一行 n 人要都买到票至少需要取多少张纸币。

输入格式
第一行三个整数 n,k,p (1≤n≤103,1≤k≤10,1≤p≤103) 分别表示 oxx 需要购买船票张数,一次至多买船票数量,单张船票价格。

输出格式
输出一个整数,表示 oxx 至少要取多少张纸币。

思路:
可以看成完全背包问题,不同的物品就是每次购买的船票从1~k所需要的钱数。

#include <algorithm>
#include <iostream>
using namespace std;
int dp[1111], n, p, k;
int mod[6] = {100, 50, 20, 10, 5, 1};
int item[11];
int main() {
    cin >> n >> k >> p;
    for (int i = 1; i <= k; i++) {
        int money = i * p;
        for (int j = 0; j <= 5; j++) {
            item[i] += money / mod[j];
            money %= mod[j];
        }
    }
    for (int i = 1; i <= n; i++)
        dp[i] = 11111111;

    for (int i = 1; i <= k; i++) {
        for (int j = i; j <= n; j++)
            dp[j] = min(dp[j - i] + item[i], dp[j]);
    }
    cout << dp[n];
    return 0;
}

多重背包问题

问题描述:
有N件物品和一个容量为W的背包。第i件物品的费用(即体积,下同)是w[i],价值是v[i],最多可选m[i]个,求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
朴素想法:
d p [ i ] [ j ] dp[i][j] dp[i][j]为到第i个物品为止总重量不超过j的所有选法中最大可能的价值。
递推关系式:(复杂度 O ( N W m ) O(NWm) O(NWm))
d p [ 0 ] [ j ] = 0 d p [ i + 1 ] [ j ] = m a x { d p [ i ] [ j − k × w [ i ] ] + k × v [ i ] ∣ 0 ≤ k ≤ m [ i ] } dp[0][j]=0\\ dp[i+1][j]=max\{dp[i][j-k\times w[i]]+k\times v[i]|0\leq k\leq m[i]\} dp[0][j]=0dp[i+1][j]=max{dp[i][jk×w[i]]+k×v[i]0km[i]}
代码:
(进行了优化)
将原本的 m i m_i mi个物品转化为用 2 k 2^k 2k表示的k+2个物品( m i = 1 + 2 + 4 + . . . + 2 k + a m_i=1+2+4+...+2^k+a mi=1+2+4+...+2k+a),然后看作普通的01背包DP。

vector<int> weight;
vector<double> value;
for (int i = 0; i < n; i++)
{
    int w,v,num;
    cin >> w >> v >> num;
    for (int j = 1; j <= sum; j <<= 1)
    {
        weight.push_back(w * j);
        value.push_back(v * j);
        sum -= j;
    }
    weight.push_back(w * sum);
    value.push_back(v * sum);
}
void solve() {
    int dp[W + 1];
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= N; i++) {
        int num = m[i];
        for (int k = 1; num > 0; k <<= 1) {
            int mul = min(k, num);
            for (int j = W; j >= w[i] * mul; j--) {
                dp[j] = max(dp[j], dp[j - w[i] * mul] + v[i] * mul);
            }
            num -= mul;
        }
    }
    cout << dp[W];
}

变式题1

题目
蒜头君酷爱收集萌萌的娃娃。蒜头君收集了 6种不同的娃娃,第 i 种娃娃的萌值为 i(1≤i≤6)。现在已知每种娃娃的数量 mi​,蒜头君想知道,能不能把娃娃分成两组,使得每组的娃娃萌值之和相同。

输入格式
输入一行,输入 6 个整数,代表每种娃娃的数量 mi​(0≤mi​≤20,000)。

输出格式
输出一行。如果能把所有娃娃分成萌值之和相同的两组,请输出Can be divided.,否则输出Can’t be divided.。

分析
核心思想:01背包+二进制思想。把n个价值为i的物品划分成log2n个价值为i_1,i_2…的商品,然后再用01背包。

代码

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;

int dp[1000000];
int m[7];
int val[200];

int main()
{
    int sum=0,flag=1;
    memset(dp,0,sizeof(dp));
    //对m[i]个物品进行划分1,2,4,8....2的k次方,n-(1+2+..2的k次方)
    for(int i=1;i<=6;i++){
        cin>>m[i];
        sum+=i*m[i];
        for(int k=1;k<=m[i];k*=2){
            val[flag++]=k*i;
            m[i]-=k;
        }
        if(m[i]>0){
            val[flag++]=m[i]*i;
        }
    }
    //val是新的价值,flag-1是物品数
    if(sum%2==0){//01背包问题
        sum/=2;
        for(int i=1;i<flag;i++){//外层是物品的依次递增
            for(int j=sum;j>=val[i];j--){//限制条件是萌值不超过总和的一半,
                dp[j]=max(dp[j],dp[j-val[i]]+val[i]);
            }
        }
        if(dp[sum]==sum)
            cout<<"Can be divided."<<endl;
        else
            cout<<"Can't be divided."<<endl;
    }
    else
        cout<<"Can't be divided."<<endl;
    return 0;
}

分数背包问题

与01背包条件类似,但可以取分数件物品。
贪心:
求出单位质量的物品价值,从高到低取。

#include <algorithm>
#include <iostream>
using namespace std;
double W;
int n;
struct goods {
    double w, v;
    double argv;
} good[111];
bool cmp(goods a, goods b) { return a.argv > b.argv; }

int main() {
    cin >> n >> W;
    for (int i = 0; i < n; i++) {
        cin >> good[i].w >> good[i].v;
        good[i].argv = good[i].v / good[i].w;
    }
    sort(good, good + n, cmp);
    double ans = 0;
    int index = 0;
    while (W > 0 && index < n) {
        if (W > good[index].w) {
            ans += good[index].v;
            W -= good[index].w;
        } else {
            ans += good[index].argv * W;
            W = 0;
        }
        index++;
    }
    printf("%.6lf", ans);
    return 0;
}

DP:
(无优化版本)类似于完全背包的dp

#include <cstring>
#include <iostream>
using namespace std;
int n;
double W;
double w[111], v[111], s[111];
double dp[111][1000];

int main() {
    cin >> n >> W;
    for (int i = 1; i <= n; i++) {
        cin >> w[i] >> v[i];
        s[i] = v[i] / w[i];
    }
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= W; j++) {
            for (int k = 0; k <= w[i]; k++) {
                if (j >= k)
                    dp[i][j] = max(dp[i][j], dp[i - 1][j - k] + k * s[i]);
            }
        }
    }
    printf("%.6lf\n", dp[n][(int)W]);
    return 0;
}

优化版本:类似于多重背包的优化。优化后作为01背包处理。

for (int i = 0; i < n; i++) {
        int w, v;
        cin >> w >> v;
        double argv = (double)v / w;
        for (int j = 1; j <= w; j <<= 1) {
            w[cnt] = j;
            v[cnt++] = argv * j;
            w -= j;
        }
        w[cnt] = w;
        v[cnt++] = argv * w;
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值