背包问题(01背包和完全背包)

一、01背包(1)

问题描述

有n个重量和价值分别为wi和vi的物品,从这些物品中挑选出总价值不超过W的物品,求这些方案中价值最大的一种。

穷竭搜索

利用递归针对每种物品是否放入背包进行搜索,这种方法很容易想到,但是时间复杂度可能比较大,最坏的情况需要O(2n),当给出的n不太大时可以用。

void solve() {
    int rec(int i, int j);
    cout << rec(n - 1, W);
}
int rec(int i, int j) {//从第0到i这i+1个物品中选出总重量不超过j
    int res;
    if (i == -1)//没有物品可以挑选
        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  res;
}

由于上面朴素的算法时间复杂度太大,下面我们利用记忆化数组dp[i][j]对其进行优化,优化后的算法时间复杂度为O(nW),dp[i][j]表示从0到i这i+1个物品的总重量不超过j的物品的总价值最大者。这里注意我们要先对dp数组进行初始化memset(dp,-1,sizeof(dp))

void solve() {
    int rec(int i, int j);
    cout << rec(n - 1, W);
}
int rec(int i, int j) {
    if(dp[i][j] >= 0)//如果数组中已经存在,直接返回
        return dp[i][j];
    int res;
    if (i == -1) 
        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;//数组中不存在的存入数组
}

动态数组

上面已经提到过记忆化数组,下面我们进一步研究这个数组,我们可以直接根据递推关系填满这个数组,dp[i+1][j]表示从0到i这i+1个物品的总重量不超过j的物品的总价值最大者。而这样做的时间复杂度同样是O(nW)。

void solve() {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j <= W; j++) {
            if (j < w[i])//无法挑选物品
                dp[i + 1][j] = dp[i][j];
            else//挑和不挑中选一个价值更大的
                dp[i + 1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]);   
        }
    }
    cout << dp[n][W] << endl;
}

状态转移

我们同样可以寻找每个状态和其之后状态的关系来解题,我们可以将“前i个物品中选取总重不超过j时的状态”转换成“前i+1个物品中选取总重不超过j时的状态”和“前i+1个物品中选取总重不超过j+w[i]时的状态“。

void solve() {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j <= W; j++) {
            dp[i + 1][j] = max(dp[i + 1][j], dp[i][j]);//转换成上述第一种状态
            if(j + w[i] <= W)//转换成上述第二种状态
                dp[i + 1][j + w[i]] = max(dp[i + 1][j + w[i]],dp[i][j] + v[i]);
        }
    }
    cout << dp[n][W] << endl;
}

二、01背包(2)

问题描述

问题与第一个一样,只是上述三种解法是针对n和W较小时的,一旦n和W变得很大,即使0(nW)的复杂度也是很大的。

改变dp对象

我们发现之前dp数组是针对重量的,我们可以改变dp的对象,换成针对不同价值j求最小的重量。dp[i+1][j]表示前i个物品中挑选出价值总和不超过时总重量的最小值。

void solve() {
    fill(dp[0], dp[0] + 10001, 10000);//对第一行初始化为充分大的数10000
    dp[0][0] = 0;//前0个物品什么都不能挑选
    for (int i = 0; i < n; i++) {
        for (int j = 0; j <= 10000; j++) {
            if (j < v[i])
                dp[i + 1][j] = dp[i][j];
            else
                dp[i + 1][j] = min(dp[i][j], dp[i][j - v[i]] + w[i]);
        }
    }
    int res = 0;
    for (int i = 0; i <= 10000; i++)
        if (dp[n][i] <= W)//小于总重的最大价值
            res = i;
    cout << res << endl;
}

三、完全背包

问题描述

问题与第一个类似,只是每个物品可以被多次挑选。

区别

01背包问题每个物品只能被挑选一次,所以每次需要讨论的对象是下一个,而完全背包问题每个物品可以被挑选多次,所以每次下一个讨论的对象还是本身。

void solve() {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j <= W; j++) {
            if (j < w[i])
                dp[i + 1][j] = dp[i][j];
            else
            for(int k = 1; k * w[i] <= j; k++)
                dp[i + 1][j] = max(dp[i + 1][j], dp[i][j - k * w[i]] + k * v[i]);   
        }
    }
    cout << dp[n][W] << endl;
}

我们还可以把上述的一个判断换成一条语句,让k从0开始。

void solve() {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j <= W; j++) {
            for(int k = 0; k * w[i] <= j; k++)
                dp[i + 1][j] = max(dp[i + 1][j], dp[i][j - k * w[i]] + k * v[i]);   
        }
    }
    cout << dp[n][W] << endl;
}

但是这样就存在了一个三重循环,时间复杂度为O(nW2),我们发现,这个算法中存在很多的多余计算,比如在dp[i+1][j]中的第k个情况,其实已经在dp[i+1][j-w[i]]中第k-1个情况计算过了。

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

四、空间节省

上面利用动态数组的算法都是利用的二维数组dp,其实我们会发现,利用递推关系时,每次需要改变值的数组都是在同一行的,所以我们可以不断的覆盖前一行,只利用以为数组便可以求解。

01背包

void solve() {
    for (int i = 0; i < n; i++) {
        for (int j = W; j >= w[i]; j--) {//从后往前循环
            dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        }
    }
    cout << dp[W];
}

完全背包

void solve() {
    for (int i = 0; i < n; i++) {
        for (int j = w[i]; j <= W; j++) {//从前往后循环
            dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        }
    }
    cout << dp[W];
}

需要注意的是两个问题的j的循环方向不同,前者需要从后往前循环,因为数组后面需要用到前面改变之前的值,而后者是针对的对象是自己。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值