算法学习过程——动态规划(01背包,完全背包,多重背包,二进制优化多重背包)

引入

        假如你像鲁滨逊一样流落在荒岛上,不同的是荒岛遍布黄金,你的女朋友得知你流落荒岛的消息立刻来找你,在你离开之前要尽可能的多带走黄金为了以后和女朋友幸福的生活...你的背包大小是有限的,因此你要取装尽可能价值大的黄金,而不是混有许多杂质的黄金,请问你要如何分配这背包呢?

        以上就是典型的01背包模型,在有限的背包容量中尽可能存放最大的价值。

01背包

思路

        如何实现有限背包的价值最大化呢?我们可能有一些基本的猜想:

  • 先拿大的,等大的装不下了再装小的,直到把背包装满
  • 先看看那些价值高但是体积不是很大的,也就是所谓的性价比,装性价比最大的直到背包装不下为止

        其实两种想法都有一定的道理,但是又各有各的漏洞:

第一种你无法确定体积大价值一定大,所以你可能错过真正有价值的反而去存放一些价值低的。

第二种似乎已经是正确答案了,但是在最后你无法背包容量是否能装其他的性价比虽小但是背包还可以放得下的,虽然性价比小,但是有总比没有好。

        其实我们可以一步步来分析保证每一步都是拿到最优的结果,再让分析后面时直接使用前面已经取到的最优的结果。如果这句话你看不懂也没关系,我来通俗的解释一下:假如背包的容量为n,那我们是否可以利用一个这样的结果:在背包容量为 n - 1 的时候背包存储的最大价值 ,这给结果再加上 体积为 1 的价值最大的物品的价值。不就是背包容量为 n 的时候能取到的最大价值吗?现在再去看第一句话,是不是就可以理解了。

        因此为了实现价值最大化,我们遍历到的每一个物品时,都要判断这个物品要不要放进背包,放进背包后,背包的剩余容量要减去相应的物品体积。

        用公式来说明那就是:令 dp[i][j] 来表示前 i 个物品,在背包容量为 j 时能存放的最大价值

那么我们假设一共有 n 个物品,m 体积的背包,每个物品都有两个属性:体积 v 和价值 w。

dp[i][j] = max(dp[i-1][j],dp[i - 1][j - v[i] + w[i])

。二者分别是判断选不选第 i 个物品,如果不选那结果和第 i - 1 的结果是一样的,剩余容量也是一样,如果选,那剩余容量就要减去第 i 个物品的容量,价值则加上第 i 个物品的价值

代码演示 
#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 1009;
const ll p = 1e9 + 7;
int n, m;//分别表示物品的数量和背包的容量
int dp[N][N];//dp表示前i个物品使用体积j能存放的最大价值
int v[N], w[N];//v表示体积,w表示价值

void solve() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
        cin >> v[i] >> w[i];
    }

    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= m; ++j) {
            if (j >= v[i])
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
            else
                dp[i][j] = dp[i - 1][j];
        }
    }

    cout << dp[n][m];//最后的结果就是算上所有的n个物品,用完所有的j的体积,所存放的最大价值
}
int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int _ = 1;
    // cin >> _;
    while (_--) solve();
    return 0;
}

完全背包

思路

        完全背包与01背包不同的地方在于完全背包中,每一种物品都有无限个,而01背包只有一个,这也是01背包的名字由来。要么选0要么选1,完全背包则要考虑选几个。如果选用最朴素的做法,我们在01背包中加一个,除了判断选不选,再判断选几个接好了。

        公式表示为:dp[i][j] = max(dp[i-1][j],dp[i - 1][j -k* v[i] + k*w[i])

其中k表示每一个物品取k个,因此有 k * v[i] <= j,即在这个物品选取的总体积不得超剩余体积。

代码实现
#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 1009;
const ll p = 1e9 + 7;
int n, m;//分别表示物品的数量和背包的容量
int dp[N][N];//dp表示前i个物品使用体积j能存放的最大价值
int v[N], w[N];//v表示体积,w表示价值

void solve() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> v[i] >> w[i];

    for (int i = 1; i <= n; ++i)
        for (int j = 0; j <= m; ++j)
            for (int k = 0; k * v[i] <= j; ++k) 
                dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + w[i] * k);

    cout << dp[n][m];//最后的结果就是算上所有的n个物品,用完所有的j的体积,所存放的最大价值
}
int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int _ = 1;
    // cin >> _;
    while (_--) solve();
    return 0;
}

这是朴素的做法,当物品个数和背包容量稍大一点的时候就会超时。我们是否可以优化它呢?

对于 j 容量的背包 ,我们要从以下中找到最大值:

 dp[i - 1][j] , dp[i - 1][j - v] + w , dp[i - 1][j - 2 * v] + w * 2..... dp[i - 1][j - k * v] + w * k

对于第 j - v 容量来说:

                     dp[i - 1][j - v] ,         dp[i - 1][j - 2 * v] + w[i] ..... dp[i - 1][j - k * v[i]] + w[i]*(k-1)

对于每一项都少一个w,因此 j 的最大值比 j - 1 的最大值多 w 

所以可以直接利用 j - 1 的最大值的结果加上 wdp[i][j] = max(dp[i-1][j],dp[i - 1][j -v[i]]+ w[i])

#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 1009;
const ll p = 1e9 + 7;
int n, m;
ll dp[N][N];
ll v[N], w[N];

void solve() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> v[i] >> w[i];

    for (int i = 1; i <= n; ++i)
        for (int j = 0; j <= m; ++j) {
            if (j >= v[i])
                dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i]);
            else
                dp[i][j] = dp[i - 1][j];
        }

    cout << dp[n][m];
}
int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int _ = 1;
    // cin >> _;
    while (_--) solve();
    return 0;
}

多重背包

思路

        假定每一种物品既不是只有一个也不是无限个,而是有个数的,朴素的做法和完全背包一样都是遍历别个物品能取的最大价值然后取最大值

代码实现

        如果前面的都理解了,我相信多重背包的朴素做法也是没问题的。

#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 109;
const ll p = 1e9 + 7;
int n, m;
int dp[N][N];
int v[N], w[N], s[N];

void solve() {
    cin >> n >> m;

    for (int i = 1; i <= n; ++i) cin >> v[i] >> w[i] >> s[i];
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= m; ++j) {
            for (int k = 0; k <= s[i] && k * v[i] <= j; ++k) {
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - k * v[i]] + w[i] * k);
            }
        }
    }

    cout << dp[n][m];
}
int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int _ = 1;
    // cin >> _;
    while (_--) solve();
    return 0;
}

二进制优化多重背包

思路

        三个for循环的情况只适用于数据较少的时候,在数据多的时候还是要选择来优化。我们知道了每种物品的数量,但我们是否真的要一个一个的加,然后一个一个的比较呢?我们是否有办法两个两个的加,这样不就快了一倍吗。既然都两个两个加了,是否能好多个一起加?

        不过我们还要想到一个问题,那就是如果一次多个,剩余容量不足以加多个的情况下不就不是最优解了吗?因此我们不只能一次加多个,也能一次加少量。我们可以每次加2的幂数个,2的幂数最大结果不能超过物品的个数,这样就把 n 优化到 log 级别。如果不理解的话可以换个角度,一个物品体积为 v,价值为 w,分成多个问题使得 体积价值分别为 2v,2w;4v,4w;......等等

        不过这样会使数组的大小变大,变大的倍数为最大能取到的2的幂数的上位取整,如最大物品数为1000,就要乘10。

代码实现

        

#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 25009;
const int M = 2509;
const ll p = 1e9 + 7;
int n, m;
int dp[N];
int v[N], w[N];

void solve() {
    cin >> n >> m;

    int cnt = 0;//表示当前数组取到的个数,默认是0
    for (int i = 1; i <= n; ++i) {
        int a, b, s;//分别表示体积,价值,个数
        cin >> a >> b >> s;
        int k = 1;
        while (k <= s) {
            cnt++;
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }

        if (s) {//假如还有剩余,则算在一起
            cnt++;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }

    n = cnt;
    //往后便是01背包
    for (int i = 1; i <= n; ++i) {
        for (int j = m; j >= v[i]; --j) {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }

    cout << f[m];
}
int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int _ = 1;
    // cin >> _;
    while (_--) solve();
    return 0;
}

总结

        题目可以随便找oj基本都有模板题哦。

        自用复习以及分享给大家,有任何问题可以留言或私信。

  • 36
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值