DP 背包问题

闲言:

唠点闲话,蓝桥杯要10.17举办,我也开学了,放在这上面的时间会相对减少了,因为要照顾绩点嘛,话说这几天还挺累的,因为下周要考试了,终于把高数从头到尾复习了一遍,接下来就还有英语,大物还得复习,哎,还是要坚持住啊,吃得苦中苦方为人上人嘛,加油喽,文化课学累了,就来学一下算法吧,背包问题也学了挺久了,今天简单整理一下,不过可能整理不完,会慢慢补全的。

(01)背包

题目概述:

(01)背包就是每件物品只可以使用一次,然后求总体积不超过背包容量的总价值最大。

数据范围

(0) (<) (N),(V) (leq) (1000)
(0) (<) (V_i,W_i) (leq) (1000)

思路:

状态表示:(f(i,j)) 其中的(i)表示只从前(i)个物品选,(j)表示选出来的物品总体积 (leq) j,因此(f(i,j))就表示从前(i)个物品中选出总体积不超过(j)的最大价值。
状态计算:集合的划分
我们划分为不含(i)个物品与包含(i)个物品两个集合。

不含第(i)个物品的话,用状态方程表示就是:(f(i, j) = f(i-1,j))
包含第(i)个物品,则我们可以考虑先把第(i)个物品去掉,因为我们在去掉已经确定了价值的第(i)个物品的情况下并不会影响剩下(i-1)个最大值,最后再将(w[i])加上即可,则状态转移方程:

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

因此我们可以得到这道题的状态转移方程:

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

因此可以写出代码

#include <iostream>

using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for(int j = 1; j <= n; j++) f[j][0] = 0; //表示在体积为0的情况下 价值为0

    for(int i = 1; i <= n; i++)
    {
        for(int j = 0; j <= m; j++)
        {
            f[i][j] = f[i-1][j]; //不考虑第i件物品
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i])
        }
    }

    cout << f[n][m] << endl;

    return 0;
}

然后我们可以考虑优化成一维:

  • 先都去掉一维,则变成了
    for(int i = 1; i <= n; i++)
    {
        for(int j = 0; j <= m; j++)
        {
            f[j] = [j]; //不考虑第i件物品
            if(j >= v[i]) f[j] = max(f[j], f[j - v[i]] + w[i])
        }
    }

想想这样合适吗,当然不合适,j是从小到大枚举的,因此(j - v[i])是已经被更新了,即是在当前第(i)层被更新,那么现在再把它变成二维就等价于(f[i][j] = max(f[i][j], f[i][j-v[i]] + w[i])),而原答案就是(f[i][j] = max(f[i][j], f[i - 1][j-v[i]] + w[i]))应该是从(i - 1)层更新过来的,因此需要改一下第二层(for)循环,让(j)从大到小枚举,(j - v[i] < j),因此这里的(j - v[i])用的还是是(i - 1)层的,所以符合原来二维状态方程,同时,调节了第二层循环之后,(j >= v[i])变成恒等式,直接去掉(if)就好了,对于, (f[i][j] = f[i-1][j]) -> (f[j] = f[j]),等价吗,当然等价,因为算(f[j])的时候,左边(f[j])是第(i)层的,待更新,右边的值是(i - 1)层的(j),我们既然要用(i - 1)层的更新i层的,那么新的(f[j])就完全等价于旧的(f[j]), 那么f[j] = f[j]就完全可以直接省略就好。

因此代码变成了这样

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

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

int main()
{
    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 = m; j >= v[i]; j--)
        {
            f[j] = max(f[j], f[j-v[i]] + w[i]);
        }
    }

    cout << f[m] << endl;

    return 0;
}

完全背包

题目概述:

每种物品可以有无限件可用,求最大价值

数据范围:

(0) (<) (N),(V) (leq) (1000)
(0) (<) (V_i,W_i) (leq) (1000)

思路:

状态表示:(f(i,j)) 其中的(i)表示只从前(i)种物品选,(j)表示选出来的物品总体积 (leq) j,因此(f(i,j))就表示从前(i)种物品中选出总体积不超过(j)的所有选法。
状态计算:集合划分
(i)种物品选(0)(1)(2)(3)(……)(k-1)(k)个。
如果第(i)种物品,一个也不选,则说明我们只考虑前(i-1)种物品,即:(f(i-1, j))
如果选第(i)种物品选(k)个,则我们可以采取与(01)背包类似的思路,去掉已经确定了价值的(k)个第(i)种物品,因为这并不会影响前(i-1)种物品我们选法的最大价值,因此得到方程:

[f(i, j) = f(i-1, j - v[i] * k) + w[i] * k ]

这里注意,第(i)种物品如果我们不选的话,就等价于(f(i-1, j)),因此不选的情况我们也可以并到上式子中,因此我们得到最终式子:

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

可以得到朴素版代码

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];

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

    cout << f[n][m] << endl;

    return 0;
}

但是看本题的数据范围就知道这个(O(N^3))的代码是一定会(TLE)的,然后进行优化:
将式子展开:

[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, ...) ]

观察上下两式,我们看出式子(1)(f(i-1, j-v) + w, f(i-1, j-2*v) + 2*w, f(i-1, j-3*v) + 3*w, ...)可以由式子(2):(f(i, j-v)) (+) (w)得到,于是我们就可以进行合并,我们可以得到这样的式子:

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

因此这样就可以不用再枚举(k)了,只需要枚举两个状态即可。
(f(i, j) = max(f(i-1, j), f(i, j-v) + w))(01)背包的状态转移方程并不同,(01)背包需要从(i-1)转移而来,而完全背包是从(i)转移而来。
优化后的代码

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];

int main()
{
    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 = 1; j <= m; j++)
        {
            f[i][j] = f[i-1][j];
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
        }
    }

    cout << f[n][m] << endl;

    return 0;
}

还可以优化成一维:参考上面的01背包二维优化成一维,因为我们这里的状态f[i][j]是第i件物品选几个,我们用第i层的更新第i层的,这里我们从小到大枚举,因此得到代码。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];

int main()
{
    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 = v[i]; j <= m; j++)
        {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }

    cout << f[m] << endl;

    return 0;
}

多重背包(I)

题目概述:

每件物品只能选有限个,然后求总体积不超过背包容量的总价值最大。

数据范围

(0) (<) (N),(V) (leq) (100)
(0) (<) (V_i,W_i) (leq) (100)

思路:

题目只有(0-100)的数据范围,因此(O(n^3))是完全可以的,那么这道题就跟完全背包的朴素做法思路基本一样了,只是多了一个限定条件,所选物品有限所以直接写就好。

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;
int n, m;
int f[N][N];
int v[N], w[N], s[N];

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

多重背包 (II)

题目概述:

每件物品只能选有限个,然后求总体积不超过背包容量的总价值最大。

数据范围

(0) (<) (N) (leq) (1000)
(0) (<) (V) (leq) (2000)
(0) (<) (v_i,w_i,s_i) (leq) (2000)

思路:

二进制优化

这个如果再用朴素做法就不行了,(O(NVS)) 大约要(4*10^9),肯定不行,而若我们对(S)进行二进制拆开然后将整个问题当成(01)背包来做,那么复杂度就被有优化为了(O(NVlogS)) 大约为(2*10^7),优化了两个数量级。
那为什么我们不用优化完全背包的方法来优化多重背包呢?先从状态转移方程出发

[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-s*v]+s*w) ag{1} ]

而:

[f[i][j-v] = max(f[i-1][j-v], f[i-1][j-2*v]+w, ... ,f[i-1][j-s*v]+(s-1)*w, f[i-1][j-(s+1)*v]+s*w) ag{2} ]

可见(2)式比(1)式多了一项,因此不能这样优化。
进入二进制优化正题
假设有件物品的数量为(200),那么我们可以分成(1, 2, 4, 8, 16, 32, 64)这么几组,还剩下一个(73)不再拆分,分成的每一组我们最多只能拿一次那么就可以按照(01)背包做了。
那会出现什么问题吗,例如我们想拿3件但是并没有啊,这问题并不会出现,(1 + 2 = 3)啊, 对于例子,(1,2)可以凑出(1-3)的任何一个数字,同理(1, 2)加一个4就可以凑出(1-7)任何一个数字,再同理,假设我们可以把(n)分出一组(1, 2, ..., 2^k, C)(C)不足以(2^{k+1}),否则我们就是(1, 2, ..., 2^k, 2^{k+1}),我们可以用(1, 2, ..., 2^k)凑出(2^{k+1} - 1)(根据等差数列求和)中的任何一个数字,那么再加一个(C)就可以凑出(1-n)中任何一个数。那么这样再做一遍(01)背包即可,复杂度就由(O(NVS))优化到了(O(NVlogS))

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 12000;
int v[N], w[N];
int n, m, f[N];

int main() {
    cin >> n >> m;
    int cnt = 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; //每个物品a 价值为b
            s -= k;
            k *= 2;
        }
        if (s > 0) {
            cnt++;
            v[cnt] = a * s, w[cnt] = b * s;
        }
    }
    n = cnt;
    for (int i = 1; i <= n; i++) {
        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) 组物品和一个容量是 (V) 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 (v_{ij}),价值是 (w_{ij}),其中 (i) 是组号,(j) 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式

第一行有两个整数 (N,V),用空格隔开,分别表示物品组数和背包容量。

接下来有 (N) 组数据:

每组数据第一行有一个整数 (S_i),表示第 (i) 个物品组的物品数量;
每组数据接下来有 (S_i) 行,每行有两个整数 (v_{ij}),(w_{ij}),用空格隔开,分别表示第 (i) 个物品组的第 (j) 个物品的体积和价值;

输出格式

输出一个整数,表示最大价值。

数据范围

(0<N,V≤100)
(0<S_i≤100)
(0<v_{ij},w_{ij}≤100)

思路:

还是大同小异的代码与思路,因为这里是每层只能选一种我们从(i-1)层更新,所以写成一维从大到小枚举

[f[j] = max(f[j], f[j - v[i][k]] + w[i][k]) ]

思路

#include <iostream>

using namespace std;

const int N = 110;
int s[N], w[N][N], v[N][N];
int n, m, f[N];

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> s[i];
        for (int j = 0; j < s[i]; j++) {
            cin >> v[i][j] >> w[i][j];
        }
    }

    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]);
                }
            }
        }
    }
    cout << f[m] << endl;
    return 0;
}

原文地址:https://www.cnblogs.com/ZhengLijie/p/13608901.html