背包问题 - DP

背包问题属于线性DP,但相对难

01背包

\(n\)个物品,背包体积为\(m\),每个物品最多拿一个,问能拿的最大价值

二维解法

  • 状态表示: \(f[i][j]表示只选前i个物品,背包体积为j,背包的最大价值\)
  • 集合划分: 选或不选第\(i\)个物品
if(选第i个物品) f[i - 1][j] // 即为上次的最大价值
else f[i][j - v[i]] + w[i] // 即减去此物品的体积,加上它的价值
  • 状态转移: \(f[i][j] = max(f[i-1][j],\ f[i][j - v[i]]+w[i])\)

其中\(v[i]\)表示第\(i\)个物品的体积,\(w[i]\)表示第\(i\)个物品的价值

一维解法

根据观察我们发现,每一个状态\(f[i][j]\)实际上都只会用到上一次状态\(f[i-1][j]\)的值,因此舍弃第一维,将原数组作为滚动数组倒序枚举,从而降为为一维数组

  • 状态表示: \(f[i]表示背包体积为j,背包的最大价值\)
  • 集合划分: 选或不选第\(i\)个物品
if(选第i个物品) f[j] // 即为上次的最大价值
else f[j - v[i]] + w[i] // 即减去此物品的体积,加上它的价值
  • 状态转移: \(f[j] = max(f[j],\ f[j - v[i]]+w[i])\)

其中\(v[i]\)表示第\(i\)个物品的体积,\(w[i]\)表示第\(i\)个物品的价值

练习:T1290. 采药

AC代码

此题时间为体积,草药的价值就是价值

#include <iostream>
#include <cstring>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

const int N = 1100;
typedef long long LL;

int v[N], w[N], f[N];

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

    for(int i = 1; i <= n; i++)
    {
        for(int j = V; j >= w[i]; j --)
        {
            f[j] = max(f[j], f[j - w[i]] + v[i]);
        }
    }
    
    cout << f[V];
    return 0;
}

完全背包

\(n\)个物品,背包体积为\(m\),每个物品无限拿,问能拿的最大价值

  • 状态表示: \(f[i][j]表示只选前i个物品,背包体积为j,背包的最大价值\)

  • 集合划分:\(k\)个第\(i\)个物品

  • 状态转移: \(f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i])\)

其中\(v[i]\)表示第\(i\)个物品的体积,\(w[i]\)表示第\(i\)个物品的价值,\(k <= j\)

优化

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w ,  f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v]= max(            f[i-1,j-v]   ,  f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)

发现他们之间只差了一个w,因此,原方程可以优化为\(f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i])\)
发现他和01背包的\(f[i][j] = max(f[i-1][j],\ f[i][j - v[i]]+w[i])\)很像,其实他也可以优化为一维,但是和01背包的概念并不同

\(f[j] = max(f[j],f[j-v[i]]+w[i])\)
还是滚动数组,只不过这次是正序

练习:P1616 疯狂的采药

AC代码

#include <iostream>
#include <cstring>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

const int N = 1e7+10;
typedef long long LL;

LL v[N], w[N], f[N]; // 要开long long……

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

    for(int i = 1; i <= n; i++)
    {
        for(int j = v[i]; j <= V; j ++)
        {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    
    cout << f[V];
    return 0;
}

多重背包

\(n\)个物品,背包体积为\(m\),每个物品最多拿\(s[i]\)个,问能拿的最大价值

  • 状态表示: \(f[i][j]表示只选前i个物品,背包体积为j,背包的最大价值\)

  • 集合划分:\(k\)个第\(i\)个物品

  • 状态转移: \(f[i][j] = max(f[i-1][j-v[i]*k] + w[i]*k, f[i][j])\)

其中\(v[i]\)表示第\(i\)个物品的体积,\(w[i]\)表示第\(i\)个物品的价值,\(k <= min(j, s[i])\)

暴力代码

多重背包问题 I

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 1010;
int f[N][N], v[N], w[N], s[N], n, m;

int main()
{
    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] && v[i] * k <= j; k ++ )
                    f[i][j] = max(f[i - 1][j - v[i] * k] + w[i] * k, f[i][j]);
    cout << f[n][m] << endl;
    return 0;
}

二进制优化

多重背包不能采用完全背包的一维优化

f[i , j ] = max( f[i-1,j] ,f[i-1,j-v]+w ,f[i-1,j-2v]+2*w ,..... f[i-1,j-Sv]+S*w, )
f[i , j-v]= max( f[i-1,j-v] ,f[i-1,j-2v]+w, ..... f[i-1,j-Sv]+(S-1)*w, f[i-1,j-(S+1)v]+S*w )

为什么多出来了一项!

这是因为完全背包可以无限取,所以没有最后一项的问题

那么我们就只能采用另一种优化方式:二进制优化

  • 我们知道用\(2^0,2^1,2^2,...,2^k\)可以表示任意\(2^{k+1}\)内的数字(不含\(2^{k+1}\)

那么\(s[i]\)自然可以分成\(2^0,2^1,2^2,...,2^k\)的形式,取\(1~s[i]\)个物品就变成了,在\(k\)个物品\(2^0,2^1,2^2,...,2^k\)中01背包,时间复杂度从\(O(n^3)\)大大降低成\(O(n^2logS)\)

代码

题目:多重背包问题 II

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 12010, M = 2010;
int w[N], v[N], f[M], cnt;


int main()
{
    int n, m; // n是物品数,m是容量
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) 
    {
        int a, b, s;
        cin >> a >> b >> s;
        int k = 1;
        while(k <= s) // 拆分
        {
            v[++ cnt] = k * a;
            w[cnt] = k * b;
            s -= k;
            k *= 2;
        }
        if(s > 0) // 有边角料
        {
            v[++ cnt] = s * a;
            w[cnt] = s * b;
        }
    }
    n = cnt; // 更新物品数量
    for (int i = 1; i <= n; i ++ ) // 01背包
        for (int j = m; j >= v[i]; j -- )
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[m] << endl;
    return 0;
}

其实还有一个单调队列优化,但是本蒟蒻不会╮(╯﹏╰)╭

分组背包

\(n\)个物品,背包体积为\(m\),每个物品属于一个组,每组中最多取一个物品,问能拿的最大价值

状态表示:\(f[i][j]表示从前i组中,背包体积为j的最大价值\)
状态转移: \(f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k])\)

其中\(v[i][k]\)表示第\(i\)个组,第\(k\)个物品的体积,\(w[i][k]\)表示第\(i\)组,第\(k\)个物品的价值

写出代码

for(int i = 1; i <= n; i++){
    for(int j = 0; j <= m; j++){
        f[i][j] = f[i - 1][j];  //不选
        for(int k = 0; k < s[i]; k++){
            if(j >= v[i][k])   
		f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
        }
    }
}

和01背包很像,所以同理,也可以进行一维优化,因为只用到了上次的状态,所以倒序

for (int i = 1; i <= n; i ++ )    
    for (int j = m; j >= 0; j -- )
        for(int k = 0; k < s[i]; k++)
            if(v[i][k] <= j)
            f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);

例题:分组背包

代码

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#define INF 0x3f3f3f3f

using namespace std;

const int N = 210, M = 35;
typedef long long LL;

int f[N];

struct node
{
    int w, v;
}s[M]; // 记录物品属性
vector<int> idxs[M]; // 记录组数

int main()
{
    int V, n, t;
    cin >> V >> n >> t;
    for(int i = 1; i <= n; i ++)
    {
        int p;
        cin >> s[i].v >> s[i].w >> p;
        idxs[p].push_back(i); // 放入对应组
    }

    for(int i = 1; i <= n; i ++)
    {
        for(int j = V; j >= 0; j --)
        {
            for(int k = 0; k < idxs[i].size(); k ++)
            {
                int idx = idxs[i][k]; // 取出编号
                if(j >= s[idx].v) // 背包装得下?
                    f[j] = max(f[j], f[j - s[idx].v] + s[idx].w); // 根据编号取出属性
            }
        }
    }

    cout << f[V] << endl;
    
    return 0;
}

混合背包就是混合,进行if判断,判断要用到背包类型,没什么特别之处,因此不记录

posted @ 2022-07-23 15:41  MoyouSayuki  阅读(57)  评论(0编辑  收藏  举报
:name :name