动态规划01背包详解 (以洛谷P1734 最大约数和为例)

·摘要

动态规划算法通常用于求解某些具有最优性质的问题。基本思想是待解决的问题分解成若干个子问题,通过求解子问题来得到原问题的解。而递归求解的过程常常可以展开成一棵树,比如典型的斐波那契数列,但求解这棵树的时候,我们发现有很多需要重复计算的子问题,如果我们能将子问题的结果保存起来,那将会节省很多时间。而背包问题则是一类典型的运用动态规划方法求解的问题。背包问题又分为01背包和完全背包两种。
本文以洛谷的P1734 最大约数和为例,对一般的01背包问题进行分析,并利用滚动数组进行空间优化处理。

问题的重述

【题目描述】
选取和不超过S的若干个不同的正整数,使得所有数的约数(不含它本身)之和最大。
【输入格式】
输入一个正整数S。
【输出格式】
输出最大的约数之和。
【输入输出样例】
在这里插入图片描述

问题的分析

【算法】:动态规划
【数据结构设计】:数组
我们对一般的背包问题进行分析,并进行空间状态优化,进而将一般的模型应用在这个问题的解决上。

(一) 01背包问题模型:

1.1 问题情境:
给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi 。问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
1.2 具体实例和分析:
借助的b站博主的视频中的例子
我们设B(i,j)表示:从第1件到第i件物品,在背包称重为j的情况下,能够拿到的最大价值。这样理解的话,我们对该问题的求解即是求解B(4,8)。

(P.S. 特别的:我们有B(0,j)为0,B(i,0)为0。这里是为了编程方便数组遍历,所以赋予0意义:B(0,j)表示任意背包容量,我一件都不想拿,此时能拿到的最大值自然是0;B(i,0)则表示考虑前i件物品,但背包容量为0,装不了任何物品,此时能拿到的最大价值也为0。)

对于B(1,2),B(2,3)我们能比较轻易地通过观察得到最大价值分别是1和2.而对于B(4,8)我们就无法直接观察得出答案了。而对于每一件物品i,都有两种情况:
①背包容量不足导致不能选择i
②背包可以装下i,但可以主观地选择不选物品i,最终在选与不选的价值之间选择价值较大的那种选法;

我们稍作分析可以得到以上的两种情况的递推公式——
在这里插入图片描述
以B(4,8)为例,即若第四件物品过重,在目前容量为8时无法装下,则有B(4,8)=B(3,8).而若可以装下第四件物品,我们考虑两种情况:
①一种是选择第四件物品:B(4,8)=B(3,8-w(4))+p(4)=B(3,3)+p(4) (w(4)表示第四件物品的重量,p(4)表示第四件物品的价值),上式则表示在一定取第四件物品的情况下,能拿到的最大价值为:在空间容量为3时考虑前3件物品所能拿到的最大价值加上已经拿到的第4件物品的价值。

②另一种是不选择第4件物品,那么能拿到的最大价值则是前3件物品在背包容量为8时能拿到的最大的价值。及B(3,8)。与装不下第四号物品的情况一致。

在能放进第四件物品的前提下要获得最大的价值就要选取上述①②两种情况下的最大值,即B(3,3)+p(4)和B(3,8)两个值中的较大者。

结合上面的分析,我们知道要求B(4,8)我们可能要用到B(3,3)的值,可能要用到B(3,8)的值。而求取B(3,3)和B(3,8)的值我们又要按照公式进行比较和选择。综上我们可以,要隐约得到这样一个结论:要求B(i,j)的值,其实我们有必要知道前面一些数的值,假如这些值都知道了,那么B(i,j)的值也就可以得出了。
为此,我们用一个表格来描述这样一个通过前面的值来推出B(i,j)的值的过程——
在这里插入图片描述
该表格的第一行的数字0——8代表j,即此时的背包剩余容量。需要说明的是此表格【从左上填到右下】。且由上述的特别说明(斜体字部分)可知,表格空白处的第一行和第一列均为0;接着我们从B(1,1)开始按照给出的公式来进行填表(装不下,B(1,1)处填0)。此处省略过程,直接给出填好的表格——
在这里插入图片描述
按照上面的分析,我们可以知道其实求解B(4,8)的即完成上面的表格的填写,而当我们填完最后一个数字,B(4,8)也求出来了。
根据上述的思路,代码的实现过程其实也很容易:对二维数组进行遍历即可,给出下面的伪代码:

1.	初始化表格table[n_item][capacity]所有元素为0   
2.	    for :0开始遍历物品标号i  
3.	            for :0开始遍历背包容量capacity   
4.	                 	if(当前容量j大于等于第i件物品重量)    //能装下  
5.	                   table[i][j]=max(table[i-1][j],table[i-1][j-w[i]]+p[i]);  
6.	                    else                                //装不下   
7.	                        table [i][j]=table[i-1][j];  
8.	最后输出table[n_item][capacity],即表格的最后一个元素,问题得到解决。

(二)空间状态压缩:

在完成上面的表格的过程中,我们可以发现这样一个事实——任何一个B(i,j),直接决定他的值的只有它的上面一行的数据,即B(i-1,j)和B(i-1,j-w[i])决定 B(i,j)。所以对于某一行的数据来说并不需要i-1行以前的数据,前面的空间被浪费了。同时,对于w[i](i物品的重量)一定是大于0的,所以B(i,j)其实是由B(i-1,j)和其左边的数据决定的。即:(以B(4,8)为例)
在这里插入图片描述

1.	初始化一维数组v[capacity]所有元素为0   
2.	    for :0开始遍历物品标号i  
3.	            for : 从capacity开始倒序遍历背包容量        //即  capacity-->0   
4.	                    if(当前容量j大于等于第i件物品重量)    //能装下  
5.	                        v[j]=max(v[j],v[j-w[i]]+p[i]);  
6.	                    else                                //装不下   
7.	                        v[j]=v[j];    //装不下其实不用处理,为了方便叙述写上   
8.	最后输出v[capacity],即数组的最后一个元素,问题得到解决。 

(三) 本题的解题思路:

言归正传,在理解上述01背包后,我们可以很简单的解决这一题了。通过对比,我们知道这题背包容量capacity就是输入的数字S,物品的价值就是物品编号的约数和。
这样分析之后我们即可知道只要求出因子之和完成对p数组的初始化即可按照背包问题的模板解决问题了。而求出因子和只需要写个函数计算即可,这里不予赘述。直接给出核心部分的伪代码:

1.	初始化一维数组v[S]所有元素为0   
2.	    for :0开始遍历物品标号i至S  
3.	            for : 从S开始倒序遍历背包容量          		//即  S-->0   
4.	                  if(当前容量j大于等于第i件物品重量)  	//能装下  
5.	                     	v[j]=max(v[j],v[j-w[i]]+p[i]);   
6.	最后输出v[S],即数组的最后一个元素,问题得到解决。   

下面给出题目完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int tosum(int a);
int isprime(int a);

int p[1001]={0};    //i的因数和
int capcity;        //背包容量  即不超过输入的数
int n_item;         //拿的物品的数量,当前拿??或当前不拿n_item号物品
int w[1001]={0};    //重量,1的重量是1  1000的重量的1000
int v[1001];


int main()
{
    int i,j;

    scanf("%d",&n_item);
    capcity=n_item;

    //先完成对重量数组w和价值数组p的初始化
    for(i=1;i<=n_item;i++){
        w[i]=i;
        p[i]=tosum(i);
    }

    //常规01背包解法
    for(i=0;i<=n_item;i++){
        for(j=capcity;j>=0;j--){
            if( j >= w[i] ){ //背包可以装下
                v[j]=( v[j] >= v [j-w[i]] + p[i] )? v[j] : v[j-w[i]] + p[i];
            }
        }
    }

    printf("%d",v[n_item]);

    return 0;
}

int isprime(int a)
{
    int i;
    if(a==2||a==1)
        return 1;
    else
    {
        for(i=2;i<=a;i++){
            if(a%i==0)
                break;
        }
        if(i==a)
            return 1;
        else
            return 0;
    }
}

int tosum(int a)
{
    int i;
    int sum=0;
    if(isprime(a)){ //是素数
        if(a!=1)
            sum=1;
        else
            sum=0;
    }
    else{
        for(i=1;i<a;i++){
            if(a%i==0){
                sum+=i;
            }
        }
    }
    return sum;
}

详解的话可以看看b站的01背包算法视频课

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Blanche117

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值