【动态规划】背包问题详解

这篇具有很好参考价值的文章主要介绍了【动态规划】背包问题详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

动态规划(Dynamic Pogramming,简称dp)是运筹学的一个分支,是求解决策过程最优化的数学方法。背包问题则是dp问题里很常见的一类。本篇文章来详解一下背包问题。

1、基础知识

动态规划的理解方式有很多种,这里讲述的是yxc老师的闫氏dp法,个人认为是最好的理解方式并且非常好用。
动态规划 背包,基础算法与基础数据结构,动态规划,算法,c++
遇到dp问题,首先考虑状态表示,即如何(用什么、怎么样)把一个状态表示出来,区分两个不同状态的指标数量维度,我们要把相同的状态放入一个集合里面去,并且规定这个集合的属性(可能是最大值、最小值、元素数量等)。在此之后,我们要考虑状态计算,即如何把当前状态的集合计算出来。在这一步,我们一般采用划分集合的方式,即将一个大的集合划分为很多小的集合,这些小的集合可以计算出来,那么大的集合也就能计算出来了。一般要求是:划分集合时不重不漏

听起来很抽象,确实是这样。dp问题需要不断做题积累经验,才能真正体会到其中的精髓。下面用背包问题来走一遍这个流程,方便理解。

背包问题是动态规划问题里的一种很常见题型,具体来说就是,给一个容量确定的背包,给一些体积确定、价值确定的物品,问怎样带才能带走价值最大的物品。好比说打游戏时,打怪掉落了很多物资,但是背包有限,就要想想带走什么才对自己最有用。我们用闫氏dp法来走一遍流程。

首先,考虑如何表示一个背包的状态:1.背包容量;2.放哪些物品。因此,背包问题集合的维度是二维的。具体表示方法不同的题目不同。因为问题需要,所以集合的属性自然就是价值的最大值
然后就要考虑状态的计算了,即如何计算 f ( i , j ) f(i,j) f(i,j)的值。这个过程我们采取划分集合的方式,不同的题目有不同的考虑方式,下面用一些例题来实战。

2、01背包

01背包具体是:一个物品只有选(1)或不选(0)两种状态,固称为01背包。首先看一下例题 acwing 01背包

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 v i v_i vi, w i w_i wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0< v i v_i vi, w i w_i wi≤1000
输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8

我们可以 f ( i , j ) f(i,j) f(i,j)来表示只在前 i i i个物品里面选,且背包容量为 j j j

划分集合时可以将只在前 i i i个物品里选划分为两个小集合:

  1. 所有选了 i i i的选法: f ( i − 1 , j − v i ) + w i f(i-1,j-v_i)+w_i f(i1,jvi)+wi
  2. 所有不选 i i i的选法: f ( i − 1 , j ) f(i-1,j) f(i1,j)

这样就能不重不漏的把 f ( i , j ) f(i,j) f(i,j)分为两个小的集合了。同时由于 f ( i , j ) f(i,j) f(i,j)的属性是最大值,那么就可以表示为:
f ( i , j ) = m a x ( f ( i − 1 , j − v i ) + w i , f ( i − 1 , j ) ) f(i,j)=max(f(i-1,j-v_i)+w_i, f(i-1,j)) f(i,j)=max(f(i1,jvi)+wi,f(i1,j))
这样就得到了dp问题最重要的状态计算方程

得到了这个,这个题就可以直接做出来了:

#include <iostream>

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];

	//因为i等于0的时候f(i,j)一定是0(选0个物品),所以直接从1开始
    for (int i = 1; i <= n; i ++ )
        for (int j = 0; j <= m; j ++ )
        {
            f[i][j] = f[i - 1][j];
            //只有装得下物品i时,才考虑选了i的选法
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }

    cout << f[n][m];

    return 0;
}

dp问题就是这样神奇,看似很难,其实只要得到状态计算方程,很简洁的代码就能通过。

接下来,还能对代码进行优化。注意到 f ( i , j ) = m a x ( f ( i − 1 , j − v i ) + w i , f ( i − 1 , j ) ) f(i,j)=max(f(i-1,j-v_i)+w_i, f(i-1,j)) f(i,j)=max(f(i1,jvi)+wi,f(i1,j))每个 i i i状态都是由 i − 1 i-1 i1状态得到的,因此可以用滚动数组的方式来更新。意思是:一个二维数组,每行都是由上一行得到的,那么就可以优化为一个一维数组,每次用新的值覆盖当前值,也即相当于由上一行得到下一行。那么核心部分代码:

f[i][j] = f[i - 1][j];

就可以优化为:

f[j] = f[j];

恒等式可以省略;其次第二个核心代码:

if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);

可以优化为:

if (j >= v[i]) f[j] = max(f[j], f[j - v[i]] + w[i]);

而由于循环是递增的,所以直接从v[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]);

但是这里有一个问题:注意到 f [ j − v [ i ] ] f[j-v[i]] f[jv[i]]一定小于 f [ j ] f[j] f[j],所以它一定会在 f [ j ] f[j] f[j]之前更新,那么更新 f [ j ] f[j] f[j]时,就相当于用本层的 f [ j − v [ i ] ] f[j-v[i]] f[jv[i]]来更新,但计算公式要求用上一层的 f [ j − v [ i ] ] f[j-v[i]] f[jv[i]]来更新,为解决这个问题,只需要把第二层循环逆向进行即可。这样能保证更新 f [ j ] f[j] f[j]时用到的是上一层的 f [ j − v [ i ] ] f[j-v[i]] f[jv[i]]

那么得到01背包问题的终极版代码:

#include <iostream>

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 -- )
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m];

    return 0;
}

3、完全背包

完全背包就是给出一些物品,但是物品数量不限。例题 acwing完全背包

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 v i v_i vi, w i w_i wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0< v i v_i vi, w i w_i wi≤1000
输入样例

4 5
1 2
2 4
3 4
4 5

输出样例

10

还可以 f ( i , j ) f(i,j) f(i,j)来表示只在前 i i i个物品里面选,且背包容量为 j j j
但是划分集合的时候,就和01背包略有不同了。因为物品是无限使用,所以划分为:

  1. i i i件物品选了0个的选法: f ( i − 1 , j ) f(i-1,j) f(i1,j)
  2. i i i件物品选了1个的选法: f ( i − 1 , j − v i ) + w i f(i-1,j-v_i)+w_i f(i1,jvi)+wi
  3. i i i件物品选了2个的选法: f ( i − 1 , j − 2 v i ) + 2 w i f(i-1,j-2v_i)+2w_i f(i1,j2vi)+2wi
    ……
    i i i件物品选了s个的选法: f ( i − 1 , j − s v i ) + s w i f(i-1,j-sv_i)+sw_i f(i1,jsvi)+swi,( s × v i ≤ j s×v_i≤j s×vij);

这样就能做到不重不漏地把 f ( i , j ) f(i,j) f(i,j)划分为了 s s s个集合。所以可以得到状态计算方程
f ( i , j ) = m a x ( f ( i − 1 , j − k v i ) + k w i ) , ( k = 0 , 1 , 2 , … , s ) f(i,j)=max(f(i-1,j-kv_i)+kw_i),(k=0,1,2,…,s) f(i,j)=max(f(i1,jkvi)+kwi)(k=0,1,2,,s)

那么题解就有了:

#include <iostream>

using namespace std;

const int N = 1010;

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

    return 0;
}

可以进行进一步优化。这里注意到一个神奇的事情:
动态规划 背包,基础算法与基础数据结构,动态规划,算法,c++
也即, v i ≥ j v_i≥j vij时,有 f ( i , j ) = f ( i , j − v i ) + w i f(i,j)=f(i,j-v_i)+w_i f(i,j)=f(i,jvi)+wi
有了这个,就可以对完全背包进行优化了:

#include <iostream>

using namespace std;

const int N = 1010;

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

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

    return 0;
}

这样少了一重循环,时间复杂度就变成O(mn)了。
再和01背包一样进行一维优化,得到最终版代码:

#include <iostream>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[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 ++ )
    	//注意这里的区别是j递增,和01背包相反,一定要想清为什么
        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;
}

j j j是递增的原因:公式中 f ( i , j ) f(i,j) f(i,j)的更新就是用本层的 f ( i , j − v i ) f(i,j-v_i) f(i,jvi)

4、多重背包问题

完全背包就是给出一些物品,但是物品数量有限。例题 acwing多重背包

有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 v i v_i vi, w i w_i wi, s i s_i si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N≤1000
0<V≤2000
0< v i v_i vi, w i w_i wi, s i s_i si≤2000
输入样例

4 5
1 2 3
2 4 1
3 4 3
4 5 2

输出样例

10

f ( i , j ) f(i,j) f(i,j)来表示只在前 i i i个物品里面选,且背包容量为 j j j
划分集合的时候,因为物品是有限使用,所以划分为:

  1. i i i件物品选了0个的选法: f ( i − 1 , j ) f(i-1,j) f(i1,j)
  2. i i i件物品选了1个的选法: f ( i − 1 , j − v i ) + w i f(i-1,j-v_i)+w_i f(i1,jvi)+wi
  3. i i i件物品选了2个的选法: f ( i − 1 , j − 2 v i ) + 2 w i f(i-1,j-2v_i)+2w_i f(i1,j2vi)+2wi
    ……
    i i i件物品选了 s i s_i si个的选法: f ( i − 1 , j − s v i ) + s w i f(i-1,j-sv_i)+sw_i f(i1,jsvi)+swi

这样就能做到不重不漏地把 f ( i , j ) f(i,j) f(i,j)划分为了 s s s个集合。所以可以得到状态计算方程
f ( i , j ) = m a x ( f ( i − 1 , j − k v i ) + k w i ) , ( k = 0 , 1 , 2 , … ) f(i,j)=max(f(i-1,j-kv_i)+kw_i),(k=0,1,2,…) f(i,j)=max(f(i1,jkvi)+kwi)(k=0,1,2,)
注意这里的 k v i kv_i kvi一定是小于 j j j的。

那么就可以得到题解:

#include <iostream>

using namespace std;

const int N = 110;

int f[N][N];
int v[N], w[N], s[N];
int n, m;

int main()
{
    cin >> n >> m;

    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i] >> s[i];

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

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

    return 0;
}

但是浅看一下时间复杂度,三重循环,最坏 1000 × 2000 × 2000 1000×2000×2000 1000×2000×2000,大约是四十亿,明显超时了(C++每秒大概运算一亿次),所以就要优化。这里的优化是一个技巧,叫二进制优化

假设有1023个物品,那么进行分组:20,21,22,23,24,…,29。也即1,2,4,8,…,512。用这几个数,就能组合出0 ~ 1023中所有的数。那么原来的1024(0 ~ 1023)个数,就变成了10个数进行组合。假如物品数不是2n-1个,例如45,那么就分组:20,21,22,23,24,45-24。也就是1,2,4,8,16,14。用这几个数就能组合出0 ~ 45所有的数了。

把分的每个组想成一个物品,其实就转化成了01背包问题,因为每组只能是选或不选。因此用01背包的代码即可解决,重点是分组。这时看一下时间复杂度,也就是 V × N × l o g s i V×N×logs_i V×N×logsi,差不多 1000 × 2000 × l o g 2000 1000×2000×log2000 1000×2000×log2000,大概两千万,可以接受。那么给出优化的多重背包问题:

#include <iostream>

using namespace std;

//物品总数大概是N(组数)*log si(每组里的小组数)
//1000*log2000≈11000
const int N = 11010, M = 2010;

int f[N];
int v[N], w[N];
int n, m;

int main()
{
    cin >> n >> m;
    
    //分组
    int cnt = 0;
    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;
}

5、分组背包问题

分组背包问题简单来说就是每组只能选一个物品或不选
例题 acwing分组背包

有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 v i j v_{ij} vij,价值是 w i j w_{ij} wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N 组数据:
每组数据第一行有一个整数 S i S_i Si,表示第 i 个物品组的物品数量;
每组数据接下来有 S i S_i Si 行,每行有两个整数 v i j v_{ij} vij, w i j w_{ij} wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0< S i S_i Si≤100
0< v i j v_{ij} vij, w i j w_{ij} wij≤100
输入样例

3 5
2
1 2
2 4
1
3 4
1
4 5

输出样例

8

f ( i , j ) f(i,j) f(i,j)来表示只在前 i i i组里面选,且背包容量为 j j j
划分集合的时候,因为物品是有限使用,所以划分为:

  1. i i i组物品不选的选法: f ( i − 1 , j ) f(i-1,j) f(i1,j)
  2. i i i组物品选第1个的选法: f ( i − 1 , j − v i 1 ) + w i 1 f(i-1,j-v_{i1})+w_{i1} f(i1,jvi1)+wi1
  3. i i i组物品选第2个的选法: f ( i − 1 , j − v i 2 ) + w i 2 f(i-1,j-v_{i2})+w_{i2} f(i1,jvi2)+wi2
    ……
    i i i组物品选第 s i s_i si个的选法: f ( i − 1 , j − v i s i ) + w i s i f(i-1,j-v_{is_i})+w_{is_i} f(i1,jvisi)+wisi

由于思路较为简单,所以直接给出一维优化后的代码:文章来源地址https://www.toymoban.com/news/detail-738959.html

#include <iostream>

using namespace std;

const int N = 110;

int v[N][N], w[N][N], s[N];
int f[N];
int n, m;

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

到了这里,关于【动态规划】背包问题详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 背包问题算法全解析:动态规划和贪心算法详解

    计算机背包问题是动态规划算法中的经典问题。本文将从理论和实践两个方面深入探讨计算机背包问题,并通过实际案例分析,帮助读者更好地理解和应用该问题。 背包问题是一种经典的优化问题。有的时候我们需要将有一堆不同重量或者体积的物品放入背包,但是背包容量

    2024年02月09日
    浏览(39)
  • Python 算法基础篇:背包问题的动态规划解法

    背包问题是计算机科学中一个重要的组合优化问题,动态规划是解决该问题的高效算法技术。本篇博客将重点介绍背包问题的动态规划解法,包括状

    2024年02月16日
    浏览(31)
  • Acwing-基础算法课笔记之动态规划(背包问题)

    01背包中的0和1指的是放与不放,而且不能出现放多个的情况,背包只能放相同物品中的一个; 首先是对 d [ i ] [ j ] d[i][j] d [ i ] [ j ] 数组的解释,该数组表示的是只看前 i i i 个物品,总体积是 j j j 的情况下,总价值最大是多少; 不选第 i i i 个物品,只考虑前 i − 1 i-1 i −

    2024年04月17日
    浏览(38)
  • 算法基础复盘笔记Day09【动态规划】—— 背包问题

    ❤ 作者主页:欢迎来到我的技术博客😎 ❀ 个人介绍:大家好,本人热衷于 Java后端开发 ,欢迎来交流学习哦!( ̄▽ ̄)~* 🍊 如果文章对您有帮助,记得 关注 、 点赞 、 收藏 、 评论 ⭐️⭐️⭐️ 📣 您的支持将是我创作的动力,让我们一起加油进步吧!!!🎉🎉 1. 题目

    2023年04月22日
    浏览(35)
  • Java数据结构与算法----动态规划(背包篇)

    1.1.算法思路 0/1背包是动态规划、背包问题中最经典的问题啦!它主要的问题是: 给定n种物品、这n种物品的重量分别是,价值分别是 ,而你有一个容量为C的背包,请问如何求出所能拿的最大价值呢? 对于动态规划,我们先需要找到一条推导公式,然后确定边界: 我们设

    2024年02月07日
    浏览(42)
  • 算法系列--动态规划--背包问题(1)--01背包介绍

    💕\\\"趁着年轻,做一些比较cool的事情\\\"💕 作者:Lvzi 文章主要内容:算法系列–动态规划–背包问题(1)–01背包介绍 大家好,今天为大家带来的是 算法系列--动态规划--背包问题(1)--01背包介绍 背包问题是动态规划中经典的一类问题,经常在笔试面试中出现,是非常 具有区分度 的题

    2024年04月16日
    浏览(37)
  • 算法系列--动态规划--背包问题(3)--完全背包介绍

    💕\\\"Su7\\\"💕 作者:Lvzi 文章主要内容:算法系列–动态规划–背包问题(3)–完全背包介绍 大家好,今天为大家带来的是 算法系列--动态规划--背包问题(3)--完全背包介绍 链接: 完全背包 可以发现完全背包问题和01背包问题还是特比相似的 分析: 完全背包问题 是 01背包问题 的推广

    2024年04月25日
    浏览(31)
  • 动态规划-背包问题详解

    首先给出背包的容量,接着: 01背包问题:给出每个物品的体积和质量,每个物品最多只能使用一次 完全背包问题:给出每个物品的体积和质量,每个物品可以无限次使用 多重背包问题:给出每个物品的体积、质量和数量,每个物品使用量必须在每个物品给定的数量之类 分

    2024年01月24日
    浏览(29)
  • 【动态规划】背包问题详解

    动态规划(Dynamic Pogramming,简称dp)是运筹学的一个分支,是求解决策过程最优化的数学方法。 背包问题 则是dp问题里很常见的一类。本篇文章来详解一下背包问题。 动态规划的理解方式有很多种,这里讲述的是yxc老师的闫氏dp法,个人认为是最好的理解方式并且非常好用。

    2024年02月06日
    浏览(35)
  • 动态规划-----背包类问题(0-1背包与完全背包)详解

    目录 什么是背包问题? 动态规划问题的一般解决办法: 0-1背包问题: 0 - 1背包类问题  分割等和子集:  完全背包问题:  完全背包类问题 零钱兑换II: 背包问题(Knapsack problem)是一种组合优化的NP完全问题。 问题可以描述为:给定一组物品,每种物品都有自己的重量和价格

    2024年04月17日
    浏览(28)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包