DP(2)--背包DP(0-1 背包,完全背包,多重背包,混合背包)

滚动数组: 让数组滚动起来,每次都使用固定的几个存储空间,来达到压缩,节省存储空间的作用。
一般用于递推和动态规划中

一维数组
比如:求斐波那契数列第100项
long long arr[3];
arr[0] = 1;
arr[0] = 1;
for (int i = 2; i < 100; ++i)
    arr[i%3] = arr[(i-1)%3] + arr[(i-2)%3];

二维数组
int dp[1000][1000];
for (int i = 1; i < 1000; ++i)
    for (int j = 0; j < 1000; ++j)
        dp[i][j] = dp[i-1][j] + dp[i][j-1];

运用滚动数组
int dp[2][1000];
for (int i = 1; i < 1000; ++i)
    for (int j = 0; j < 1000; ++j)
        dp[i%2][j] = dp[(i-1)%2][j] + dp[i%2][j-1];
        
0-1 背包
有n个物品和一个容量为c的背包,每个物品有重量w[i]和v[i]两种属性,
选取若干个物品放入背包使背包中的物品总价值最大且背包中物品的总重量不超过背包的容量。
设DP状态f[i][j]为在前i个物品中选取,容量为j的背包所能达到的最大价值
状态转移: 假设当前已经处理好了前i-1个物品的所有状态,
那么对于第i个物品,当其不放入背包时,背包的剩余容量不变,背包中物品的总价值也不变,故这种情况的最大价值为f[i-1][j];
当其放入背包时,背包的剩余容量会减小w[i],背包中物品的总价值会增大 v[i],故这种情况的最大价值为 f[i-1][j-w[i]]+v[i].
由此可以得出状态转移方程:
    f[i][j] = max(f[i-1][j], f[i-1][j-w[i]]+v[i])
由于对f[i]有影响的只有f[i-1],可以去掉第一维,直接用f[i]表示处理到当前物品时背包容量为i的最大价值
 
洛谷 2871
#include <iostream>
using namespace std;

//int dp[2][12881];
int dp[12881];
int w[3403];
int v[3403];

int main()
{
    int i, j, n, c;
    cin >> n >> c;
    for (i = 1; i <= n; ++i)
        cin >> w[i] >> v[i];

    // 常规滚动数组实现
    //for (i = 1; i <= n; ++i)
    //    for (j = 1; j <= c; ++j)
    //        if (j >= w[i])
    //            dp[i%2][j] = max(dp[(i-1)%2][j], dp[(i-1)%2][j-w[i]]+v[i]);
    //        else
    //            dp[i%2][j] = dp[(i-1)%2][j];

    //cout << dp[n%2][c] << endl;

    // 对dp[i][]产生影响的只有dp[i-1][], 所以数组可以去掉一维
    for (i = 1; i <= n; ++i)
        for (j = c; j >= w[i]; --j)
            dp[j] = max(dp[j], dp[j-w[i]]+v[i]);

    cout << dp[c] << endl;

    return 0;
}


完全背包

完全背包与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。

由于, f[i][j-w[i]]已由f[i][j-2*w[i]]更新过,对于第i件物品,重复选取,实现如下:
   for (j = w[i]; j <= t; ++j) // j 按从小到大顺序枚举,可实现重复选取
    dp[j] = max(dp[j], dp[j-w[i]] + v[i]);

洛谷 1616
#include <iostream>
using namespace std;

long long dp[10000001];
int w[10001];
int v[10001];

int main()
{
    int i, j, n, t;
    cin >> t >> n;
    for (int i = 1; i <= n; ++i)
        cin >> w[i] >> v[i];

    for (i = 1; i <= n; ++i)
        for (j = w[i]; j <= t; ++j) // j 按从小到大顺序枚举,可实现重复选取
            dp[j] = max(dp[j], dp[j-w[i]] + v[i]);

    cout << dp[t] << endl;

    return 0;
}


多重背包问题
多重背包与 0-1 背包的区别仅在于一个物品可以选取m次。

单调队列求滑动窗口最值问题
/*https://www.luogu.com.cn/problem/P1886 */
#include <iostream>
#include <list>
#include <vector>
using namespace std;

int arr[100001];

int main()
{
    int i, n, k;
    cin >> n >> k;
    for (i = 0; i < n; ++i)
        cin >> arr[i];

    list<int> ls1; 
    list<int> ls2;
    vector<int> vec1;
    vector<int> vec2;

    for (i = 0; i < n; ++i)
    {
        // 单调递减队列求滑动窗口最大值
        while (ls1.size() > 0 && arr[ls1.back()] < arr[i])  //  新加入元素不满足单调递减
            ls1.pop_back();
        ls1.push_back(i);
        if (i-ls1.front()+1 > k)   // 队列中元素数量超过窗口大小
        {
            ls1.pop_front();
        }
        if (i+1 >= k)
            vec1.push_back(arr[ls1.front()]);

        // 单调递增队列求滑动窗口最小值
        while (ls2.size() > 0 && arr[ls2.back()] > arr[i])
            ls2.pop_back();
        ls2.push_back(i);
        if (i-ls2.front()+1 > k)
        {
            ls2.pop_front();
        }
        if (i+1 >= k)
            vec2.push_back(arr[ls2.front()]);
    }

    for (i = 0; i < vec2.size(); ++i)
        cout << vec2[i] << " ";
    cout << endl;
    for (i = 0; i < vec1.size(); ++i)
        cout << vec1[i] << " ";
    cout << endl;


    return 0;
}

/* https://www.luogu.com.cn/problem/P1776 */

/*  朴素思想

for (i = 0; i < n; ++i)          // 枚举物品种类
{
    cin >> v >> w >> m;
    for (j = W; j >= w; --j)     // 枚举背包容量
        for (k = 1; k <= m; ++k) // 枚举该种物品的数量
            dp[j] = max(dp[j], dp[j - k*w] + k*v);
}


为了获取dp[j], 需要使用dp[j-w], dp[j-2w], ... , dp[j-k*w]的状态。
当取k件该种物品,需要为这k件物品预留k*w的空间,剩下的j-k*w的空间就是之前存储的状态。由于第二层循环的j是递减的,当下一次容量为j-1时,整个遍历过程可理解为向左平移一格。
依此类推,当j递减到j-w时就和初始的状态重合了,
所以根据第i个物品的w,我们可以将整个dp数组划分成w个等价类,
分别以0,1,2,…,w-1为开始元素。
用单调递减队列对
     每个等价类f[i][j]=max(f[i−1][j],f[i−1][j−w]+v,f[i−1][j−2w]+2v,....+f[i−1][j−m*w]+m*v)求最大值,
时间复杂度就可以降至O(n*m)。
可看做求大小为m的滑动窗口最大值问题
*/

#include <iostream>
#include <vector>
#include <list>
using namespace std;

int main()
{
    int n, W , i, j, k;
    int v, w, m;   // value, weight, amount
    cin >> n >> W;
    vector<int> dp(W+1, 0);
    vector<int> prev;      // 滚动数组,记录上一组dp[]的值
    list<int> ls;          // 单调递减队列求滑动窗口最大值,滑动窗口的大小为m[i]

    for (i = 0; i < n; ++i)
    {
        prev = dp;
        cin >> v >> w >> m;
        for (j = 0; j < w; ++j)
        {
            ls.clear();
            for (k = j; k <= W; k += w)
            {
                /* 对于当前种类的物品,他的数量最多有m个,重量为w,
                 要用到的最早的状态是全取时k-s*w,所以要保证队首元素要在这个范围内。*/
                if (!ls.empty() && ls.front() < k - w*m)
                    ls.pop_front();

                /* k - ls[head]  背包剩余容量
                   (k - ls[head])/w 背包剩余容量能存几个重量为w的物品
                   (k - ls[head])/w*v 取这几个物品的价值 */
                if (!ls.empty())
                    dp[k] = max(dp[k], prev[ls.front()] + (k-ls.front())/w*v);

                /* 为了保持容量队列单调递减,背包容量k的dp值应小于队尾元素的dp值 */
                while (!ls.empty() && prev[ls.back()] + (k-ls.back())/w*v <= prev[k])
                    ls.pop_back();

                ls.push_back(k);
            }
        }
    }

    cout << dp[W] << endl;

    return 0;
}

/*
  任何一个非负整数,可以用一个以1为首项,2为公比的等比数列中的数来表示
  通过二进制优化,可将多重背包转换为0-1背包
*/

#include <iostream>
using namespace std;
const int MAXN = 10000;
int arrV[MAXN]; // value
int arrW[MAXN]; // weight
int dp[100001];

int main()
{
    int n, W , i, j, k, cnt = 0;
    int v, w, m;   // value, weight, amount
    cin >> n >> W;

    for (i = 0; i < n; ++i)
    {
        cin >> v >> w >> m;
        k = 1;
        do {
            m -= k;
            arrV[cnt] = v*k;
            arrW[cnt] = w*k;
            ++cnt;
            k = k << 1;
        } while (k <= m);

        if (m > 0)
        {
            arrV[cnt] = v*m;
            arrW[cnt] = w*m;
            ++cnt;
        }
    }

    for (i = 0; i < cnt; ++i)
        for (j = W; j >= arrW[i]; --j)
            dp[j] = max(dp[j], dp[j - arrW[i]] + arrV[i]);

    cout << dp[W] << endl;

    return 0;
}

混合背包
混合背包就是将前面三种的背包问题混合起来,有的只能取一次,有的能取无限次,有的只能取k次。

解题思想
for (循环物品种类) {
  if (是 0 - 1 背包)
    套用 0 - 1 背包代码;
  else if (是完全背包)
    套用完全背包代码;
  else if (是多重背包)
    套用多重背包代码;
}

/* https://www.luogu.com.cn/problem/P1833 */
#include <vector>
#include <stdio.h>
using namespace std;

int dp[1001];

int main()
{

    int  i, j, k, n, t, c, p, total;
    int tsh, tsm, teh, tem;
    scanf("%d:%d %d:%d %d", &tsh, &tsm, &teh, &tem,&n);
        total = (teh - tsh) * 60 + tem - tsm;

    vector<int> vecT; // time
    vector<int> vecV; // value;
    vector<bool> vec; // true: 0-1, false: complete

    for (i = 0; i < n; ++i)
    {
        scanf("%d %d %d", &t, &c, &p);
        if (p == 0 || p == 1)
        {
            vecT.push_back(t);
            vecV.push_back(c);
            vec.push_back(!!p);
        }
        else if (p > 1)
        {
            k = 1;
            do {
                p -= k;
                vecT.push_back(t*k);
                vecV.push_back(c*k);
                vec.push_back(true);
                k = k << 1;
            } while (k <= p);

            if (p > 0)
            {
                vecT.push_back(t*p);
                vecV.push_back(c*p);
                vec.push_back(true);
            }
        }
    }

    for (i = 0; i < vec.size(); ++i)
        if (vec[i]) // 0-1
        {
            for (j = total; j >= vecT[i]; --j)
                dp[j] = max(dp[j], dp[j-vecT[i]] + vecV[i]);
        }
        else        // complete
        {
            for (j = vecT[i]; j <= total; ++j) // j 按从小到大顺序枚举,可实现重复选取
                dp[j] = max(dp[j], dp[j-vecT[i]] + vecV[i]);
        }

    printf("%d\n", dp[total]);

    return 0;
}
 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值