O(∩_∩)O哈哈~补一下动规的一些细节吧(1)——背包问题

i由于某个菜鸡读题出现失误,把一个很简单的题硬是当动规写了(重点是还没写出来)导致codeforse上掉了一百多分,于是决定补一下关于动规的一些细节处理问题(惨笑)。
说道动规就必须提一下背包(认真脸),接下来我将一个个讲解每种背包的一些细节处理问题:


01背包的细节处理
动规最主要的是进行推导状态转移方程,01背包众所周知是给你N 件物品和一个容量为V 的背包。放入第i 件物品耗费的空间是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值总和最大,显然对每件物品我们有两种选择:(dp(i,j)分别是还能未确认过的物品数,背包中还剩余的空间数)1.不装dp(i,j)=dp(i-1,j),2.装dp(i,j)=dp(i-1,j-c[i])
当然还有两点 1. 终止条件, i=0 , 无物品可选 dp(0,j)=0。2. j < w[i],背包剩余容量不足以放下第i个物品,dp(i,j)=dp(i−1,j)
但只是这样的话相当于要枚举每种情况,时间复杂的在大ON方,应该加上记忆化搜索;代码如下。

#include <cmath>
#include <cstdio>
#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;
int w[MAXN] ;
int v[MAXN] ;
int dp[MAXN][MAXN]; //记录搜索过的结果
int W , n ;

int Rec(int i, int j) {
    //Rec(i, j)计算过,直接拿来用
    if (dp[i][j] != -1) return dp[i][j];

    int res;
    if (i == 0) {
        res = 0;
    }
    else if (j < w[i]) {
        res = Rec(i-1, j);
    }
    else {
        res = max(Rec(i-1, j), Rec(i-1, j-w[i]) + v[i]);
    }
    return dp[i][j] = res; //记录
}

int main() {
    memset(dp, -1, sizeof(dp));
    cin>>W>>n;
    for(int i=0;i<n;i++)
    {
        cin>>w[i]>>v[i];
    }
    cout << Rec(n, W) << endl;
    return 0;
}

那么我们接下来要求输出取哪些,怎么办呢?
其实只要从最后一位找回去即可,代码如下:

int select[maxn];
int finding(int n)
{
    int j=W;
    for(int i=n;i>=1;i--)
    {
        if(dp[i][j]>dp[i-1][j])
        {
            select[i]=1;
            j-=v[i];
        }
        else{
            select[i]=0;
        }
    }
    for(int i=1;i<=n;i++)
    {
        if(select[i]==1)
            printf("%d ",i);
    }
        printf("\n");
}

注意:
我们看到的求解最优解的背包问题中,事实和桑有两种不太相同的问法。
1. 要求”背包恰好装满“ 时的最优解
2. 不要求背包一定要被装满时的最优解

一种区别这两种问法的实现方法是在初始化的时候有所不不同。

如果是第一种问法,要求恰好装满背包,那么在初始化时除了 dp[0]
为0, 其他dp[1…W]均设为−∞ ,这样就可以保证最终得到 dp[W] 是一种恰好装满背包的最优解
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将dp[0…W]全部设为0。

这是为什么呢?可以这样理解:初始化的dp数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装的状态下被 “恰好装满” ,此时背包价值为0。其他容量的背包均没有合法的解,属于未定义的状态,所以都应该被赋值为 −∞。当前的合法解,一定是从之前的合法状态推得的如果背包并非必须被装满,那么任何容量的背包都有一个合法解 “什么也不装”,这个解的价值为0,所以初始化时状态的值也就全部为0了。
优化点:
如果题目要求的数据范围过大明显这时再用二维数组就有点不合适了,填dp 表的顺序是从上到下,从左到右,从递归式可以看出,dp[i][j] 是由 dp[i−1][j] 和 dp[i−1][j−w[i]] 的值推来的。填写第i行的值,只依赖于上一行,第i−1行的值。下次填写第i+1 行的时候,只会用到第i 行的值,第i−1 行的值以后都不会再用到了。dp[i][j] 的值只依赖于第i−1 行的 dp[i−1][0…j] 这前 j+1 个元素, 与dp[i−1][j+1…W] 的值无关。所以,我们可以只存1行,就能完成整个dp过程。用dp[0…W] 存储当前行,更新dp[0…W] 的时候,我们按照 j=W…0 的递减顺序计算dp[j],这样可以保证计算dp[j] 时用到的dp[j]和dp[j−w[i]] 的值和原本的二维数组中的第i−1 行的值是相等的。更新完dp[j] 的值后,对dp[0…j−1] 的值不会产生影响。
故:

#include <iostream>
#include <cstring>
#define MAXN 10000
using namespace std;

int dp[MAXN];
int w[MAXN];
int v[MAXN]; 
int W , n ;

int solve(int n, int W) {
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++) { // i从1开始,递增
        for (int j = W; j >= 0; j--) { // j按递减顺序填表
            if (j < w[i]) {
                dp[j] = dp[j];//不做操作
            }
            else {
                dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
            }
        }
    }
    return dp[W];
}

int main() {
    cin>>W>>n;
    for(int i=0;i<n;i++)
    {
        cin>>w[i]>>v[i];
    }
    cout << solve(n, W) << endl;
    return 0;
}

完全背包问题
由刚才的01背包问题可知,DP(c)=max(DP(i−1,j),DP(i−1,j−w[i])+v[i])如果要知道(i,j)的值就一定要知道(i−1,j)的值和(i−1,j−w[i])的值,对二维数组而言要求其递推值其应该是逆推的,而对于完全背包问题其每一种物体有无限多件,令dp[i][j]:=从前i种物品中挑选总重量不超过j的物品的最大总价值,故其递推式为dp[i][j]=0(i=0),dp[i][j]=max(dp[i−1][j−k×w[i]]+k×v[i])(0 ≤ k ≤ ⌊j/wi⌋ 向上取整 1 ≤ i ≤ n)
这里的k可分为两种情况,k=0和k≠0,也就是第i种物品不选,或者至少选1个。
* k=0时,即不选择第i种物品,dp[i][j]=dp[i−1][j],
* k≠0时,即至少选一个第i种物品,dp[i][j]=dp[i][j−w[i]]+v[i]
有dp[i][j] = max ( dp[i−1][j] , dp[i][j−w[i]] + v[i] )其dp[i][j−w[i]] + v[i]的部分保证了其取了一个后还可以继续取这种物品,但对01背包来说其dp是还有i种可以取的,背包还有j的空间的情况的价值,故每放入一个其j会减少,因此j要按逆序进行,而对多重背包问题其dp 代表的是在有i 种物品背包体积为j 的情况下的最大价值,故这里的j 要进行顺序(一定要注意这里)。

#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;

int dp[MAXN][MAXN];
int w[MAXN] ;
int v[MAXN] ;
int W , n ;

int solve(int n, int W) {
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= W; j++) {
            if (j < w[i]) {
                dp[i][j] = dp[i-1][j];
            }
            else {
                dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] +v[i]);
            }
        }
    }
    return dp[n][W];
}

int main() 
{
    memset(dp, -1, sizeof(dp));
    cin>>W>>n;
    for(int i=0;i<n;i++)
    {
        cin>>w[i]>>v[i];
    }
    cout << solve(n, W) << endl; 
    return 0;
}


空间的优化同上01背包的情况,只要将j的遍历顺序改变即可。


最后
多重背包
有n 种物品和一个容量为W 的背包。第i 种物品有mi 个,每件重量为wi , 价值为vi ,求从这n 种物品中挑选重量总和不超过W 的物品的最大价值。
多重背包问题,最简单的解法,就是转化成0-1背包问题。第i 个物品有 mi 个, 等价于有mi 个相同的物品。但直接拆分成 mi 件物品并不是最好的方法。我们可以利用二进制来拆分。例如 m1=13=20+21+22+6 ,我们将第一种物品共13件,拆分成 20,21,22,6 这四件, 13以内的任何数字都可以通过这四种数字组合而成。
下面给出一个模板

const int N = 100, W = 100000;
int cost[N], weight[N], number[N];
int dp[W + 1];

int knapsack(int n, int w)
{
    for (int i = 0; i < n; ++i)
    {
        int num = min(number[i], w / weight[i]);
        for (int k = 1; num > 0; k*=2)
        {
            if (k > num) k = num;
            num -= k;
            for (int j = w; j >= weight[i] * k; --j)
                dp[j] = max(dp[j], dp[j - weight[i] * k] + cost[i] * k);
        }
    }
    return  dp[w];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值