dp问题——背包问题及一些应用

本文详细介绍了动态规划在解决背包问题中的应用,包括01背包、完全背包、多重背包、分组背包等基础类型,并探讨了装满背包的方案数、最优价值时的方案数及具体方案等问题。此外,还展示了如何将背包问题应用于正整数分组问题,提供了解题思路和代码实现。
摘要由CSDN通过智能技术生成

1.基础

(1) 01背包

// 状态表示
f[i][j]表示从前i个物品中选且总体积不超过j的所有方案。
// 转移
针对第i个物品做出选择。
1.不选第i个物品。 相当于从前i - 1个物品中选且总体积不超过j的所有方案表示为f[i - 1][j].
2.选择第i个物品。剔除第i个物品后,相当于从前i - 1个物品中选且总体积不超过j - v[i]的所有方案.
表示为f[i - 1][j - v] + w。【w,v分别为第i个物品的价值和体积】
注意:前提是背包剩余体积不少于v.
3.综上,f[i][j] = max(f[i - 1][j],f[i - 1][j - v] + w).
// 边界处理
i == 1时,会用到f[0][j]这个状态,根据状态表示,这个意味着从前0个物品中选,价值当然为0.
所以f[0][j] = 0;
// 优化空间
滚动数组 + 从大到小枚举j
#include<iostream>

using namespace std;

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

int main()
{
    int n,v;
    cin >> n >> v;
    for(int i = 1; i <= n; i ++)
    {
        int vv,w;
        cin >> vv >> w;
        for(int j = v; j >= vv; j --)
        {
            f[j] = max(f[j - vv] + w,f[j]);
        }
    }
    cout << f[v] << endl;
}

(2) 完全背包

// 状态表示
f[i][j] 表示 从前i个物品中选且总体积不超过j的所有方案。
// 转移
根据第i个物品转移
1.不选择第i个物品: f[i - 1][j].
2.选择第i个物品:
  (1) 选1个: f[i - 1][j - v] + w;
  (2) 选2个: f[i - 1][j - 2 * v] + 2 * w.
  ...
  (k)选k个: f[i - 1][j - k * v] + k * w.
综上: f[i][j] = max(f[i - 1][j - k * v] + k * w) k >= 0 && k * v <= j.(*)_
 = max(f[i - 1][j],f[i - 1][j - v] + w,f[i - 1][j - 2 * v] + 2 * w,f[i - 1][j - 3 * v] + 3 * w... f[i - 1][j - k * v] + k * w).

// 优化
根据(*)可以得到:
f[i][j - v] = max(f[i - 1][j - v],f[i - 1][j - 2 * v] + w,f[i - 1][j - 3 * v] + 2 * w...f[i - 1][j - k * v] + (k - 1) * w)

所以f[i][j] = max(f[i - 1][j],f[i][j - v] + w).

// 优化空间:
滚动数组 + 体积从小到大循环
#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;
        cin >> v >> w;
        for(int j = v; j <= m; j ++) f[j] = max(f[j],f[j - v] + w);
    }
    
    cout << f[m] << endl;
    
    return 0;
}

(3) 多重背包

// 状态表示
f[i][j] 表示 从前i个物品中选且总体积不超过j的所有方案。
// 转移
根据第i个物品转移
1.不选择第i个物品: f[i - 1][j].
2.选择第i个物品:
  (1) 选1个: f[i - 1][j - v] + w;
  (2) 选2个: f[i - 1][j - 2 * v] + 2 * w.
  ...
  (s)选s个: f[i - 1][j - s * v] + s * w.
所以f[i][j] = max(f[i - 1][j - k * v] + k * w) k >= 0 && k <= s

// 优化
二进制优化
// 真得需要每个物品枚举s次吗?
可不可以用一些数通过它们之间相互组合 表示 1 ~ s 内 的所有数。

每个数只有选或不选两种情况,所以转变为了 01 背包。

这些数的组成为1,2,4,8...2 ^ k,c。
2^k <= s && 2^(k + 1) > s
c = s - (1 + 2 + 4 + ... + 2 ^ k)。
#include<iostream>

using namespace std;

const int N = 11010,M = 2010;
int f[M];
int v[N],w[N];
int cnt;

int main()
{
    int n,m;
    cin >> n >> m;
    for(int i = 1; i <= n; i ++)
    {
        int a,b,s;
        cin >> a >> b >> s;
        int k = 1;
        while(k <= s)
        {
            cnt ++;
            v[cnt] = k * a;
            w[cnt] = k * b;
            s -= k;
            k *= 2;
        }
        if(s > 0)
        {
            cnt ++;
            v[cnt] = s * a;
            w[cnt] = s * b;
        }
    }
    n = cnt;
    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;
}

(4) 分组背包

// 状态表示
f[i][j] 表示从前i组内选,且总体积不超过j的所有方案的集合
// 转移
根据选择第i组的哪个物品:
不选: f[i - 1][j];
选i组第一个物品: f[i - 1][j - v[i][1]] + w[i][1]。
选i组第二个物品: f[i - 1][j - v[i][2]] + w[i][2]。
...
选i组第k个物品:  f[i - 1][j - v[i][k]] + w[i][k]。


所以 f[i][j] = max(f[i - 1][j - v[i][k]] + w[i][k]) k >= 0 && k <= 第i组物品总数。

// 注意循环顺序。
#include<iostream>

using namespace std;

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

int main()
{
    int n,m;
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) 
    {
        cin >> s[i];
        for(int j = 1; j <= s[i]; j ++) cin >> v[i][j] >> w[i][j];
    }

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

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

(5) 求装满背包的方案数

// 状态表示
f[i][j] 表示从前i个物品中选且总体积恰好为j的方案的集合。
// 根据具体的背包类型进行转移
01背包 :  f[i][j] = f[i - 1][j] + f[i - 1][j - v].
完全背包 :f[i][j] = f[i - 1][j] + f[i][j - v]。
多重背包 :f[i][j] = f[i - 1][j] + f[i - 1][j - k * v]。k >= 1 && k <= s。
分组背包 :f[i][j] = f[i - 1][j] + f[i - 1][j - v[i][k]]。k >= 1 && k <= 第i组物品总数。

// 边界
注意这里的状态定义是:体积恰好为j。
这意味着初始时只有f[i][0]是合法的。因为根据定义
从前i个物品中选,体积恰好为0的方案数只有什么都不选这1种。
其它状态都无法确定,所以为0。(因为需要计数,0在计数的过程中是不会产生任何影响的)

(6) 求背包价值达到最优时的方案数

// 01 背包
f[i][j] 表示 从前i个物品中选,且总体积恰好为j的所有方案的集合
用g[i][j]表示 f[i][j] 取最大值时的方案数
f[i][j] = max(f[i - 1][j],f[i - 1][j - v] + w).
1.如果f[i - 1][j] > f[i - 1][j - v] + w
说明f[i][j]由f[i - 1][j] 转移过来
所以g[i][j] = g[i - 1][j].
2.如果f[i - 1][j] < f[i - 1][j - v] + w
说明f[i][j]由f[i - 1][j - v]  + w转移过来.
所以g[i][j] = g[i - 1][j - v].
3.如果f[i - 1][j] = f[i - 1][j - v] + w
说明这两种情况都能使得f[i][j]值最大
所以总的方案数g[i][j] = g[i - 1][j] + g[i - 1][j - v].

(7) 求背包价值达到最优时的具体方案

// 记录终点状态由谁转移过来即可
// 01 背包
f[i][j] = f[i - 1][j] 或 f[i - 1][j - v] + w.
分别代表着 不选第i个物品 和 选第i个物品
可以令g[i][j] = 0 或 1 表示 f[i][j] 这个状态由 f[i - 1][j] 或 f[i - 1][j - v] + w 转移。

(8) 二维费用的背包

(9) 混合背包

(10) 有依赖的背包

2.应用

(1)正整数分组(01背包)

题目链接:正整数分组

// 思路
假设数组总和为sum
最理想的情况下分成两组,每一组和都为sum / 2,差值为0.
故只要让两组的值尽可能的接近sum / 2即可。
又因为sum1 + sum2 = sum,
所以只要操作一组的值接近sum / 2,另一组可通过sum - sum1 得出。
于是问题转换成:
在n个数中选,选出若干数,使之和在不超过sum / 2的情况下最大
这就是一个01背包问题:
1.背包容积:sum / 2;
2.物品体积:a[i]; 物品价值:a[i].
#include<iostream>

using namespace std;

const int N = 10010;

int f[N],a[N];

int main()
{
    int n;
    cin >> n;
    int sum = 0;
    for(int i = 0; i < n; i ++) 
    {
        cin >> a[i];
        sum += a[i];
    }
    
    for(int i = 0; i < n; i ++)
    {
        for(int j = sum / 2; j >= a[i]; j --)
        f[j] = max(f[j - a[i]] + a[i],f[j]);
    }
    
    cout << sum - 2 * f[sum / 2] << endl;
}

扩展

题目链接:正整数分组v2

// 经典二分【最大值最小】
#include<iostream>
#include<algorithm>
using namespace std;

typedef long long ll;
const int N = 50010;
int n,k;
int a[N];

int check(ll mid)
{
    ll sum = 0;
    int cnt = 0;
    for(int i = 0; i < n; i ++)
    {
        sum += a[i];
        if(sum > mid) sum = a[i],cnt ++;
    }
    if(sum) cnt ++;
    return cnt;
}

int main()
{
    cin >> n >> k;
    ll sum = 0;
    for(int i = 0; i < n; i ++) 
    {
        cin >> a[i];
        sum += a[i];
    }
    
    ll l = 1,r = sum;
    while(l < r)
    {
        ll mid = l + r >> 1;
        if(check(mid) <= k) r = mid;
        else l = mid + 1;
    }
    
    cout << l << endl;
    
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值