这次对前面讲的两个背包问题:01背包问题和完全背包问题进行更深入的理解,下面来看思想吧。
01背包问题:
之前我们讲得思想很简单,每次只要求出从前i个物品中挑选出总重量不超过j的物品时总价值的最大值。既然我们可以通过求相应重量对应的最大价值这种方法来计算出答案,那么换个思路,我们是否可以通过求相应价值对应的最小重量来计算出答案?
既然这样想,那就来试试吧。
假设前i个物品中挑选出的价值总和为j,那么就有:
①:不选第i个物品,前i-1个物品中已经可以挑选出价值总和为j。
②:选中第i个物品,前i-1个物品中可以挑选出价值总和为j-v[i]。
对这两种情况的重量求最小值,那么就得到以下的迭代公式:
dp[i+1][j]=min(dp[i][j],dp[i][j-v[i]]+w[i]);
那么现在考虑一些边界问题吧:
由于前0个物品中什么都选不了,所以dp[0][j] = 0;(这里j代表的是不越界的任意值)
那么从第一行迭代计算下面的几行,很快发现第一行的值全部是0,由于dp[i+1][j]=min(dp[i][j],dp[i][j-v[i]]+w[i]);说明dp[i+1][j]取到的值一定是dp[i][j]的值,即其余的所有值计算出来都是0。
这个问题很严重,所以需要重新分析算法。从头再看一遍,从逻辑上讲,这个算法并没有什么错误的地方,一定是自己设计时忽略了一些不显眼的地方。
突然发现,如果得到的值都是0,那么这个0代表的就是当价值不超过j时对应的最小重量。(当价值不超过j时的最小重量当然是0,因为你什么都不选价值当然是小于j的)。这时才发现自己写代码时思维受到之前算法的限制太严重了。因为之前的算法是重量不超过j时的最大价值,所以造成了思维误区,才导致得出了错误的推理。
再看看开始时给的定义:前i个物品中挑选出的价值总和为j。
那么就清楚了,仅当价值总和等于j时,才给它一个正确的值,否则给它一个不可能的值,由于每次筛选的是较小的数,为了保证对结果不产生影响,所以需要给这个位置一个充分大的值。
从前0个物品不可能得到价值j,所以给dp[0][j]赋一个充分大的值。
下面改一下边界值:dp[0][0] = 0,dp[0][j] = 999;(这里j是非零的不越界的任意值)
最终最下面一行的结果就是从所有物品中得到价值为j时的最小重量了,我们只需要遍历最后一行,找到最后一个值小于或等于W的下标就好了。
分析完了,下面看代码吧:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define INF 999
#define dp(M,N,length) dp[(M)*(length+1)+(N)]
static int rec(int *, int *, int *, int, int, int);
static int min(int, int);
int main()
{
int n, W;
double div, max = 0;
printf("Please input n & W : \n");
scanf("%d %d",&n,&W);
int * w = (int *)malloc(n * sizeof(int));
int * v = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; i++)
{
scanf("%d %d",&w[i],&v[i]);
div = v[i]*1.0/w[i];
max = max>div?max:div;
}
int len = max * W + 1;
int sdp = (n+1) * (len+1);
int * dp = (int *)malloc(sdp * sizeof(int));
for (int i = 0; i < len; i++)
dp(0,i,len) = INF;
dp[0] = 0;
printf("%d\n",rec(w,v,dp,n,W,len));
return 0;
}
int rec(w,v,dp,n,W,len)
int * w, * v;
int * dp;
int n, W, len;
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= len; j++)
{
if (j < v[i])
dp(i+1,j,len) = dp(i,j,len);
else
dp(i+1,j,len) = min(dp(i,j,len),dp(i,j-v[i],len)+w[i]);
}
}
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= len; j++)
{
printf("%6d",dp(i+1,j,len));
}
printf("\n");
}
for (int i = len;;i--)
if (dp(n,i,len) <= W || i < 0)
return i;
}
int min(int i, int j)
{
if (i < j)
return i;
else
return j;
}
01背包问题已经解出来了,算法的时间复杂度为O(n*sum(vi))。
完全背包问题:
同理,完全背包也可以这样解。由于完全背包没有物品选取次数的限制,所以不必拘泥于如何选取物品,只需要考虑每个物品被选取的次数就好了。01背包问题由于每个物品只能选取一次,所以动规取值时不能在同行取值,因为在同行取值意味着这个物品又被选取了一次,是不正确的。而完全背包问题每个物品可以选择多次,所以动规取值时可以在同行取值。
说到这里就等于告诉你们算法怎么写了。这次你无需多想,只需要在上面的01背包的代码基础上改一处就好了,只需要添加两个字符!
下面放出我的代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define INF 999
#define dp(M,N,length) dp[(M)*(length+1)+(N)]
static int rec(int *, int *, int *, int, int, int);
static int min(int, int);
int main()
{
int n, W;
double div, max = 0;
printf("Please input n & W : \n");
scanf("%d %d",&n,&W);
int * w = (int *)malloc(n * sizeof(int));
int * v = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; i++)
{
scanf("%d %d",&w[i],&v[i]);
div = v[i]*1.0/w[i];
max = max>div?max:div;
}
int len = max * W + 1;
int sdp = (n+1) * (len+1);
int * dp = (int *)malloc(sdp * sizeof(int));
for (int i = 0; i < len; i++)
dp(0,i,len) = INF;
dp[0] = 0;
printf("%d\n",rec(w,v,dp,n,W,len));
return 0;
}
int rec(w,v,dp,n,W,len)
int * w, * v;
int * dp;
int n, W, len;
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= len; j++)
{
if (j < v[i])
dp(i+1,j,len) = dp(i,j,len);
else
dp(i+1,j,len) = min(dp(i,j,len),dp(i+1,j-v[i],len)+w[i]);
}
}
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= len; j++)
{
printf("%6d",dp(i+1,j,len));
}
printf("\n");
}
for (int i = len;;i--)
if (dp(n,i,len) <= W || i < 0)
return i;
}
int min(int i, int j)
{
if (i < j)
return i;
else
return j;
}
这样的话,当W非常大,而vi普遍不大时,就可以通过这种办法求解了。然而如果价值变大的话,这种办法就不可解了,所以就需要根据问题的规模来改变算法。
我是算法吹,以后会给大家带来更多精彩的算法。