从动态规划方面(dp)讨论背包问题

首先,看一个基础的问题:

#### 【物品无限的背包问题】: n种物品,每种均有无穷个,第i种物品的体积为v[i],价值为w[i],装C容量的背包,不超过C的情况下价值最多为?
输入:
3 3
1 2
3 1
2 2
输出:
6

//求以C为起点的,边权之和最大的路径,类似一个带权图,直接用递推的方法

//无限背包问题【递推法】
#include<cstdio>
#include<cstring>
#define maxn 1010
#define maxv 10010
int n, C, v[maxn], w[maxn], f[maxv];
int main(){
   scanf("%d%d", &C, &n);
   for (int i = 1; i <= n; ++i) 
     scanf("%d%d", v+i, w+i);
   memset(f, 0, sizeof(f));
   for(int i = 1; i <= C; ++i) //以C=1开始枚举每一个物品的价值
     for(int j = 1; j <= n; ++j)//第j个物品可以有多个
       if(i >= v[j] && f[i-v[j]] + w[j] > f[i]) 
      //当i阶段时大于第j个物品的体积即有能力拿时,并且拿后的价值大于不拿再执行
          f[i] = f[i-v[j]] + w[j];
   printf("%d\n", f[C]);
   return 0; 
}   

f[i]为当容积为i时,不超过i的情况所得价值最大的值。由最外的for循环由1到C一次次记录值并选择最优解。

//递推与记忆化搜索循环的不同开始值:

OK,既然有递推就肯定有(递归形式的)即记忆化搜索。这两种不同的就是:

递推是一次次的循环,最外层从1开始,重点f[i]=max(f[i],f[i-v[j]]+w[j])

记忆化搜索(递归)是一次次的调用自身,根据上式转变,最开始从C开始,重点f[i]=max(f[i],dp[i-v[i]]+w[j])

重点强调【记忆化搜索】:(百科解释)

算法上依然是搜索的流程,但是搜索到的一些解用动态规划的那种思想和模式作一些保存。

一般说来,动态规划总要遍历所有的状态,而搜索可以排除一些无效状态。

更重要的是搜索还可以剪枝,可能剪去大量不必要的状态,因此在空间开销上往往比动态规划要低很多。

记忆化算法在求解的时候还是按着自顶向下的顺序,但是每求解一个状态,就将它的解保存下来,以后再次遇到这个状态的时候,就不必重新求解了!!!

这种方法综合了搜索和动态规划两方面的优点,因而还是很有实用价值的。

//无限背包问题【记忆搜索法】
#include<cstdio>
#include<cstring>
#define maxn 1010
#define maxv 10010
int n, C, v[maxn], w[maxn], f[maxv];

int dp(int C)
{
    if(f[C]!=0)
      return f[C]; 
    for(int j = 1; j <= n; ++j)
      if(C >= v[j] && f[C-v[j]] + w[j] > f[C]) 
          f[C] = dp(C-v[j]) + w[j];

    return f[C];
}
int main(){
   scanf("%d%d", &C, &n);
   for (int i = 1; i <= n; ++i) 
      scanf("%d%d", v+i, w+i);
   memset(f, 0, sizeof(f));
   dp(C);

   printf("%d\n", dp(C));
   return 0; 
}   

;
;
;
;
;


【0-1背包问题】:跟上题一样,只是每个物品唯一。

//分析:

n件物品每样只有一个,选择拿或不拿。先考虑递推,上面的方法明显就不适合了。只凭剩余体积这个状态,无法得知每个物品是否用过。即原来的状态转移太乱了,任何时候都允许使用任何一种物品。需要让状态转移有序化。

多阶段决策问题”,即每做一次决策就可以得到解的一部分,所有决策做完后,完整解出来了。在回溯法中,每次决策对应于给一个结点产**生新的子树,即结点的层数就是下一个填充的位置cur。

此类问题多用动态规划解决,其中状态的转移类似回溯法中的解答树,其”层数“相当于递归函数中的”cur“,即动态规划中的”阶段“。

其状态方程为 d(i,j)=max(d[i+1][j], d[i+1][j-v[j]]+w[j]) d[i][j]表示当前第i层,背包剩余容量为j时接下来的最大重量和。即把第i,i+1,i+2…..n个物品装到容量为j的背包中的最大总重量。答案为d[1][C].代码:

//0-1背包问题【递归法】
#include<cstdio>
#include<cstring>
#define maxv 10010
#define maxn 110
int n, C, w[maxn], v[maxn], f[maxn][maxv];
int main(){
    scanf("%d%d", &C, &n);
    for(int i = 1; i <= n; ++i) 
      scanf("%d%d", v+i, w+i);
    memset(f, 0, sizeof(f)); 
    for(int i = n; i >= 1; --i)   //共有n层
      for(int j = 0; j <= C; ++j){
          //f[i][j] = i==1 ? 0 : f[i+1][j];
          f[i][j] = f[i+1][j];
          if(j>=v[i] && f[i+1][j-v[i]] + w[i]>f[i][j])              f[i][j] = f[i+1][j-v[i]] + w[i];
      }
    printf("%d\n", f[1][C]);
    return 0;
}       

再来一个记忆化搜索:

//0-1背包问题【记忆搜索法】
#include<cstdio>
#include<cstring>
#define maxv 10010
#define maxn 110
int n, C, w[maxn], v[maxn], f[maxn][maxv];
int dp(int i,int j)
{
    if(i>n)
      return 0;//超过范围
    if(f[i][j]!=0)
      return f[i][j];//表示算过的数据不用算。
      /*一个tips:在“固定终点的最长短路”中,例硬币问题,路径长度S可为0,所以不能用d=0表示d值没有被算过,所以初始值不可为0,应设为-1.上面可直接简化为if(f[i][j]>=0)。或者多设一个数组vis[][]*/
    else 
    {
      f[i][j]=dp(i+1,j);
      if(j>=v[i] && f[i][j] < ( dp(i+1,j-v[i])+w[i] ) )
        f[i][j]=dp(i+1,j-v[i])+w[i];
    }
    return f[i][j];

}
int main(){
    scanf("%d%d", &C, &n);
    for(int i = 1; i <= n; ++i) scanf("%d%d", v+i, w+i);
    memset(f, 0, sizeof(f)); //

    printf("%d\n", dp(1,C));
    return 0;
} //上面dfs中if(n>n) return 0;if(f[i][j]!=0) return f[i][j];       

//如何编写记忆化搜索

(上面的这个tips,再补充一下:搜索记忆法两个关键,一个初始化,一个递归。初始化直接决定了递归中的条件判定!这条件判定的作用就是判断此次状态是否被求解过,避免重复求解。

1.当你把初始值定为0时,就要所求值是否有0的可能!所以在if(d!=0) return d; 前再加一个if(i>n) return 0;更加保险!

2.当你把初始值定为-1时,这个比较保险,if(d!=-1) return d;但是记忆化搜索到最底层数据时,你要另外赋值,不然难道返回-1吗? ………有趣的是,我刚才实验了一下,将初始值改为-1,由于没有底层状态判断,它会一直调用自己直到数组下标最大值,但是返回的值却是0,并非-1,我有点疑惑,希望知道的朋友能告诉我。…………………….背包情况可用if(i==n) return 0; 可解决上面重复循环的问题,当然,具体代码不同,所判断条件不同,但重点就是注意最底层状态的判断及返回值

//递推与记忆化搜索,比较

递推法:for循环将顺序变得非常明显,但是把每一个可能的(有的不需要)情况都枚举了(递推的关键是边界和计算顺序!!!)。

记忆搜索法:程序更加直观,通过每一次对自身函数的调用,只会计算有需要的值,省去大量不必要的状态(记忆搜索法 的关键是初始化和递归)

动态规划是一种问题求解方法,但它有几种不同的基本思路:递归,递推,记忆化搜索,但基本忽略递归,时间复杂度为(2^n)会超时,其它为(n^2)。

贴一下递归的代码,当然是不推荐的,只供了解:

//0-1背包问题【递归法】
#include <stdio.h>
int c[100],w[100];
int  f(int n, int v)
{
    int max1,max2,max;
    if(n==0)
        return 0;
    else
    {
        if(v<c[n])
           max1=f(n-1,v);
        else
            max2=w[n]=f(n-1,v-c[n])+w[n];
        if(max1>max2)
            return max1;
        else
            return max2;    

    }
}
void main(void)
{
    int n,v;
    int i,max;
    printf("请输入背包的容量:");
    scanf("%d",&v);
    printf("请输入物品的个数:");
    scanf("%d",&n);
    for(i=1;i<=n;i++)
    {
        printf("请输入第%d个物品的重量和价值:",i);
        scanf("%d%d",&c[i],&w[i]);
    }
    max=f(n,v);
    printf("背包取得的最大收益为%d\n", max);
}

总计一下思路:

先从【无限背包问题】引出“动态规划”,再介绍了由‘递推’和‘记忆化搜索’两种不同思路来实现”动态规划“,重点介绍了记忆化搜索的概念。 再从【0-1背包问题】引出“多阶段解决问题”思想,从阶段,层来理解“动态规划”真谛,再详细介绍如何编写记忆化搜索法,粗略比较了递推与记忆化搜索,万事大吉!

好了,最后申明:上述部分代码分析来源于算法竞赛入门经典,记忆化搜索由本人撰写,所以有点C++和C杂,但是在devc上编译成功,仅供参考。

最后感言,本来只是想讨论背包问题,但是这一路分析下来,发现牵扯了太多东西,有些东西真的是一环套一环,然后这篇分析的很浅显,如有错误,愿闻其详!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值