C语言动态规划多种背包问题分析讲解

目录
  • 写在前面
  • 01背包问题
  • 完全背包问题
  • 多重背包问题 I
  • 多重背包问题 II
  • 为什么可以这样优化呢
    • 一 、二进制与十进制
    • 二 、动态规划的时间复杂度估算
    • 三 、多重背包
  • 分组背包问题

写在前面

之前讲过简单DP,经典01背包问题,在这我将会把背包问题更深入的讲解,希望能帮助大家更好的理解。

01背包问题

C语言数学问题与简单DP01背包问题详解

先回忆一下这个图

在这我再将01背包问题代码发一遍,可以用来做对比。

二维:

#include<bits/stdc++.h>

using namespace std;

const int MAXN = 1005;
int v[MAXN];    // 体积
int w[MAXN];    // 价值
int f[MAXN][MAXN];  // f[i][j], j体积下前i个物品的最大价值 

int main()
{
    int n, m;
    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 = 1; j <= m; j++)
        {
            //  当前背包容量装不进第i个物品,则价值等于前i-1个物品
            if(j < v[i])
                f[i][j] = f[i - 1][j];
            // 能装,需进行决策是否选择第i个物品
            else
                f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
        }           

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

    return 0;
}

一维:

#include<bits/stdc++.h>

using namespace std;

const int MAXN = 1005;
int f[MAXN];  // 

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

    for(int i = 1; i <= n; i++) {
        int v, w;
        cin >> v >> w;      // 边输入边处理
        for(int j = m; j >= v; j--)
            f[j] = max(f[j], f[j - v] + w);
    }

    cout << f[m] << endl;

    return 0;
}

完全背包问题

完全背包问题和01背包问题的区别就在于完全背包问题中每件物品都有无限件可用。我们也可以先来试一下暴力写法。

#include<iostream>
using namespace std;

const int N = 1010;

int n, m;
int dp[N][N], 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 ++ )
            for(int k = 0; k * v[i] <= j; k ++ )//因为每一件物品都有无限件可用,我们只需要找出单件价值最高的商品就可以了
                dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);
    cout << dp[n][m] << endl;
}

优化思路:

我们列举一下更新次序的内部关系:

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2v]+2w , f[i-1,j-3v]+3w , …)

f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2v] + w , f[i-1,j-3v]+2*w , …)

由上两式,可得出如下递推关系:

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

有了上面的关系,那么其实k循环可以不要了,核心代码优化成这样:

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

这个代码和01背包的非优化写法很像啊!!!我们对比一下,下面是01背包的核心代码

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

两个代码其实只有一句不同(注意下标)

f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包

f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题

因为和01背包代码很相像,我们很容易想到进一步优化。核心代码可以改成下面这样

for(int i = 1 ; i<=n ;i++)
    for(int j = v[i] ; j<=m ;j++)//注意了,这里的j是从小到大枚举,和01背包不一样
    {
            f[j] = max(f[j],f[j-v[i]]+w[i]);
    }

综上所述,完全背包的最终写法如下:

#include<iostream>
using namespace std;
const int N = 1010;
int f[N];
int v[N],w[N];
int main()
{
    int n,m;
    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]<<endl;
}

多重背包问题 I

我们先来看这多重背包问题和01背包问题是不是很像,将s×v,s×w是不是就可以看成01背包问题了?

for(ll i=1;i<=n;i++)
    {
        cin>>a>>b>>c;
        for(ll j=1;j<=c;j++)
        {
            v[cnt]=a;
            w[cnt]=b;
            cnt++;
        }//将多重背包一个一个拆出来
    }

然后转换成01背包问题解决。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

const ll N=1e5+100;

ll v[N],w[N];
ll f[N];
int main()
{
    ll n,m;
    ll cnt=1;
    cin>>n>>m;
    ll a,b,c;
    for(ll i=1;i<=n;i++)
    {
        cin>>a>>b>>c;
        for(ll j=1;j<=c;j++)
        {
            v[cnt]=a;
            w[cnt]=b;
            cnt++;
        }//将多重背包一个一个拆出来
    }
    for(ll i=1;i<=cnt;i++)
    {
        for(ll j=m;j>=v[i];j--)
        {
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }//01背包
    cout<<f[m];
    return 0;
}

多重背包问题 II

这道题和1看起来没什么区别,但是数据范围变了,数据范围变了如果不优化就话超时,那怎么优化呢?

我们只需要将转换成01背包问题那一部分优化了就可以了。

int cnt = 0;     // 将物品重新分组后的顺序
    for (int i = 1; i <= n; i ++)
    {
        int a, b, s;    // a 体积, b 价值, s 每种物品的个数
        scanf("%d %d %d", &a, &b, &s);

        int k = 1;   // 二进制拆分 打包时每组中有 k 个同种物品
        while (k <= s)  // 即y总说的: 最后一组的物品个数 < 2^(n+1)   1 2 4 8 16 ... 2^n 2^(n+1)
        {
            cnt ++;
            v[cnt] = a * k;  // 每组的体积
            w[cnt] = b * k;  // 每组的价值
            s -= k;
            k *= 2;  // 注意是 k * 2,每次增长一倍,不是k * k
        }

        if (s > 0)   // 二进制拆分完之后 剩下的物品个数分为新的一组
        {
            cnt ++;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }

为什么可以这样优化呢

我们知道任何一个数都可以转化成二进制的数,那二进制和十进制的区别在哪呢?

一 、二进制与十进制

  • 普通遍历问题

遍历 n 个物品, 采用二进制计数方法遍历与采用十进制技术方法遍历的时间复杂度是一样的

举例来说, 对于十进制数 8

十进制遍历: 0,1,2,3,4,5,6,7 共 8 次遍历

二进制遍历: 000, 001, 010, 011, 100, 101, 110, 111 共 8 次遍历

  • 多重背包问题

同样的道理, 对于多重背包问题, 采用二进制的遍历方法不能优化时间复杂度

优化的原因在于引入了动态规划

二 、动态规划的时间复杂度估算

动态规划的时间复杂度 ≈≈ 问题的总个数 × 问题要做出的选择数

如, 对于 01 背包问题, 问题的总个数为N⋅V (N 为物品个数, V 为背包容量), 问题要做出的选择数为 2(选或不选)

则 01 背包问题的时间复杂度约为 2N⋅V

三 、多重背包

如果不采用动态规划的做法, 就像普通的遍历问题那样, 是否采用二进制的计数方法对时间复杂度的优化没有任何关系

但采用二进制的计数方法会影响问题的总个数与问题的选择数的乘积, 即动态规划做法下多重背包的时间复杂度

多重背包的动态规划时间复杂度

十进制遍历方法

问题的总个数为 N⋅V, N 为物品的种类数, V 为背包容量

问题的选择数约为 Smax,Smax 为每种物品数量的最大值

十进制下多重背包问题的 DP 时间复杂度为: N⋅V⋅Smax

二进制遍历方法

十进制下, 一种物品有 si个, 二进制下, 变为 1, 2, … , lgsi 个物品, 则共有 lgs1+lgs2+…+lg⁡sn 个物品, 约为 Nlgsmax 个物品

问题的总个数为 N⋅V⋅lgsmax

问题的选择数为 2

十进制下多重背包问题的 DP 时间复杂度为: 2N⋅V⋅lgsmax

最后请看代码

#include <bits/stdc++.h>

using namespace std;

const int N = 11 * 1000 + 10, M = 2010;

int v[N], w[N];
int f[M];

int main()
{
    int  n, m;
    scanf("%d %d", &n, &m);

    int cnt = 0;     // 将物品重新分组后的顺序
    for (int i = 1; i <= n; i ++)
    {
        int a, b, s;    // a 体积, b 价值, s 每种物品的个数
        scanf("%d %d %d", &a, &b, &s);

        int k = 1;   // 二进制拆分 打包时每组中有 k 个同种物品
        while (k <= s)  // 即y总说的: 最后一组的物品个数 < 2^(n+1)   1 2 4 8 16 ... 2^n 2^(n+1)
        {
            cnt ++;
            v[cnt] = a * k;  // 每组的体积
            w[cnt] = b * k;  // 每组的价值
            s -= k;
            k *= 2;  // 注意是 k * 2,每次增长一倍,不是k * k
        }

        if (s > 0)   // 二进制拆分完之后 剩下的物品个数分为新的一组
        {
            cnt ++;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }

    n = cnt;  // 所有的组数即为 01背包中的物品个数

    // 写01背包模板
    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]);

    printf("%d", f[m]);

    return 0;
}

分组背包问题

  • 状态表示:f[i][j]

集合:从前i组物品中选,且总体积不超过j的所有方案的集合.

属性:最大值

  • 状态计算:

思想-----集合的划分

集合划分依据:根据从第i组物品中选哪个物品进行划分.

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

请看代码

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

const int N=110;
int f[N][N];  //只从前i组物品中选,当前体积小于等于j的最大值
int v[N][N],w[N][N],s[N];   //v为体积,w为价值,s代表第i组物品的个数
int n,m,k;

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

    for(int i=1;i<=n;i++){
        for(int j=0;j<=m;j++){
            f[i][j]=f[i-1][j];  //不选 不选表示不选第 i 组物品的所有物品,只从前 i−1 组物品里面选
            for(int k=0;k<s[i];k++){
                if(j>=v[i][k])     f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
            }
        }
    }
    cout<<f[n][m]<<endl;
}

因为只用到了第i-1列,所以可以仿照01背包的套路逆向枚举体积

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

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

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

    for(int i=0;i<n;i++){
        for(int j=m;j>=0;j--){
            for(int k=0;k<s[i];k++){    //for(int k=s[i];k>=1;k--)也可以
                if(j>=v[i][k])     f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
            }
        }
    }
    cout<<f[m]<<endl;
}

到此这篇关于C语言动态规划多种背包问题分析讲解的文章就介绍到这了,更多相关C语言 背包问题内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C语言使用DP动态规划思想解最大K乘积与乘积最大问题

    最大K乘积问题 设I是一个n位十进制整数.如果将I划分为k段,则可得到k个整数.这k个整数的乘积称为I的一个k乘积.试设计一个算法,对于给定的I和k,求出I的最大k乘积. 编程任务: 对于给定的I 和k,编程计算I 的最大k 乘积. 需求输入: 输入的第1 行中有2个正整数n和k.正整数n是序列的长度:正整数k是分割的段数.接下来的一行中是一个n位十进制整数.(n<=10) 需求输出: 计算出的最大k乘积. 解题思路:DP 设w(h,k) 表示: 从第1位到第K位所组成的十进制数,设m(i,j)

  • C语言动态规划之背包问题详解

    01背包问题 给定n种物品,和一个容量为C的背包,物品i的重量是w[i],其价值为v[i].问如何选择装入背包的物品,使得装入背包中的总价值最大?(面对每个武平,只能有选择拿取或者不拿两种选择,不能选择装入某物品的一部分,也不能装入物品多次) 声明一个数组f[n][c]的二维数组,f[i][j]表示在面对第i件物品,且背包容量为j时所能获得的最大价值. 根据题目要求进行打表查找相关的边界和规律 根据打表列写相关的状态转移方程 用程序实现状态转移方程 真题演练: 一个旅行者有一个最多能装M公斤的背

  • C语言数学问题与简单DP背包问题详解

    目录 数学 买不到的数目 蚂蚁感冒 饮料换购 简单DP 背包问题 二维 一维 数学 顾名思义,数学类的题就是都可以用数学知识求解. 买不到的数目 这是第四届蓝桥杯省赛C++A组,第四届蓝桥杯省赛JAVAC组的一道题 小明开了一家糖果店. 他别出心裁:把水果糖包成4颗一包和7颗一包的两种. 糖果不能拆包卖. 小朋友来买糖的时候,他就用这两种包装来组合. 当然有些糖果数目是无法组合出来的,比如要买 10 颗糖. 你可以用计算机测试一下,在这种包装情况下,最大不能买到的数量是17. 大于17的任何数字

  • C语言动态规划多种背包问题分析讲解

    目录 写在前面 01背包问题 完全背包问题 多重背包问题 I 多重背包问题 II 为什么可以这样优化呢 一 .二进制与十进制 二 .动态规划的时间复杂度估算 三 .多重背包 分组背包问题 写在前面 之前讲过简单DP,经典01背包问题,在这我将会把背包问题更深入的讲解,希望能帮助大家更好的理解. 01背包问题 C语言数学问题与简单DP01背包问题详解 先回忆一下这个图 在这我再将01背包问题代码发一遍,可以用来做对比. 二维: #include<bits/stdc++.h> using name

  • C语言详细分析讲解struct与union使用方法

    目录 一.struct 的小秘密 二.结构体与柔性数组 三.C语言中的 union 四.小结 一.struct 的小秘密 C语言中的 struct 可以看作变量的集合 struct 的问题:空结构体占用多大内存?下面编写程序看一下吧: #include <stdio.h> struct TS { }; int main() { struct TS t1; struct TS t2; printf("sizeof(struct TS) = %d\n", sizeof(stru

  • C语言详细分析讲解多文件的程序设计

    目录 一.多文件与编译器链接 二.多文件之间的相互访问 三.关于#include 四.头文件使用的一些原则 五.再论全局变量 六.注意事项 七.实验程序 八.小结 一.多文件与编译器链接 如下图所示,.o 为目标文件,链接器将不同的目标文件装配组合在一起形成一个可执行文件. 二.多文件之间的相互访问 每个文件可以定义功能接口(可被其它文件访问的函数或数据) 源文件:代码实现文件,后缀为.c 头文件:源文件的接口定义文件,后缀为.h 当需要使用其它文件提供的功能时,包含对应的头文件 语法: #in

  • C语言详细分析讲解关键字enum与sizeof及typedef的用法

    目录 一.枚举类型的使用方法 二.sizeof 关键字的用法 三.typedef 的意义 四.小结 一.枚举类型的使用方法 enum 是 C 语言中的一种自定义类型 enum 值是可以根据需要自定义的整型值 第一个定义的 enum 值默认为 0 默认情况下的 enum 值是在前一个定义值的基础上加 1 enum 类型的变量只能取定义时的离散值 enum 中定义的值是C语言中真正意义上的常量 在工程中 enum 多用于定义整型常量 下面看一段 enum 的使用代码吧: #include<stdio

  • C语言详细分析讲解关键字const与volatile的用法

    目录 一.const 只读变量 二.const 全局变量的分歧 三.const 的本质 四.const 修饰函数参数和返回值 五.volatile 解析 六.小结 一.const 只读变量 const 修饰的变量是只读的,本质还是变量 const 修饰的局部变量在栈上分配空间 const 修饰的全局变量在全局数据区分配空间 const 只在编译期有用,在运行期无用 const 修饰的变量不是真的常量,它只是告诉编译器该变量不能出现在赋值符号的左边. 二.const 全局变量的分歧 在现代C语言编

  • C语言详细分析讲解关键字goto与void的作用

    目录 一.关于goto 二.void 的意义 三.小结 一.关于goto 高手潜规则:禁用 goto 项目经验:程序质量与 goto 的出现次数成反比 最后的判决:将 goto 打入冷宫 下面看一段 goto 副作用分析的代码: #include <stdio.h> #include <malloc.h> void func(int n) { int* p = NULL; if( n < 0 ) { goto STATUS; } p = (int*)malloc(sizeof

  • C语言详细分析讲解流程控制语句用法

    目录 一.分支语句 1.if语句 2.switch语句 二.循环语句 1.for语句 2.break和continue语句 3.循环嵌套 4.while和do…while语句 一.分支语句 1.if语句 流程控制语句可以让程序中的语句不再从上到下逐条执行 分支是一种流程控制语句,可以把程序中某些语句忽略掉不去执行 if关键字可以用来编写分支语句,只有当表达式为真时,才会执行对应语句 如果多个分支的逻辑表达式之间存在互斥关系,则可以采用else关键字把他们合并成一个分支语句 一个分支语句中的多个逻

  • C语言详细分析讲解内存管理malloc realloc free calloc函数的使用

    目录 C语言内存管理 一.动态空间申请 二.动态空间的扩容 三.释放内存 C语言内存管理 malloc && realloc && free && calloc c语言中为了进行动态内存管理,<stdlib.h>中提供了几个函数帮助进行内存管理. 我们知道,C语言中是没有C++中的容器或者说是python中list,set这些高级的数据结构的,我们一旦申请了一段内存空间以后这一段空间就归你了,比如我们举个例子,我们申请一个数组 int nums[

  • C语言指针基础知识实例讲解

    对程序进行编译的时候,系统会把变量分配在内存单位中,根据不同的变量类型,分配不同的字节大小.比如int整型变量分配4个字节,char字符型变量分配1个字节等等.被分配在内存的变量,可以通过地址去找到,内存区每一个字节都有一个编号,地址也可以形象的理解成我们生活中的住址,通过住址找到每一个人所在的地方.指针作为一个变量用来存放地址,可以通过指针来改动变量. 上图就是一个简单的定义一个一级指针变量和利用指针改变变量数值的过程.int*表示整型指针,*p表示解引用操作,就是利用指针找到a的地址然后再改

随机推荐