·摘要
动态规划算法通常用于求解某些具有最优性质的问题。基本思想是待解决的问题分解成若干个子问题,通过求解子问题来得到原问题的解。而递归求解的过程常常可以展开成一棵树,比如典型的斐波那契数列,但求解这棵树的时候,我们发现有很多需要重复计算的子问题,如果我们能将子问题的结果保存起来,那将会节省很多时间。而背包问题则是一类典型的运用动态规划方法求解的问题。背包问题又分为01背包和完全背包两种。
本文以洛谷的P1734 最大约数和为例,对一般的01背包问题进行分析,并利用滚动数组进行空间优化处理。
问题的重述
【题目描述】
选取和不超过S的若干个不同的正整数,使得所有数的约数(不含它本身)之和最大。
【输入格式】
输入一个正整数S。
【输出格式】
输出最大的约数之和。
【输入输出样例】
问题的分析
【算法】:动态规划
【数据结构设计】:数组
我们对一般的背包问题进行分析,并进行空间状态优化,进而将一般的模型应用在这个问题的解决上。
(一) 01背包问题模型:
1.1 问题情境:
给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi 。问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
1.2 具体实例和分析:
我们设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背包算法视频课