背包问题
01背包
思路
dp分析法:
1.状态划分:
(1)
判断是个什么集合?
01背包问题是所有选法的集合
集合的限制条件是什么?
01背包问题是在前i个物品中选取总体积不超过j的物品
(2)
集合的属性是什么?(属性一般有:最大值,最小值,数量)
01背包问题的属性是求最大值
2.状态划分:
集合划分 : 将集合划分为两个区间,区间之和表示的是所有选法的集合,故每个子区间表示一部分选法的集合
怎么去划分?
01背包问题可以划分为:包含前i个物品,和不包含前i个物品
感觉dp其实就是一种递推关系
二维
利用二维数组的特性,将物品个数和剩余可用体积关联。
每次更新都由上层状态更新而来。所以使用0——m是的遍历顺序是没问题的。
因为有f[i]这一维存上一次状态的最大价值。
#include <iostream>
using namespace std;
const int N = 1010;
int dp[N][N];
int v[N], w[N];
int n, m;
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++)
        {
            dp[i][j] = dp[i - 1][j];  //不含i的选法的最大值
             //如果计算含i的最大值,就要先看还能不能放下第i个物品
             //放的下,那么计算方式是:先减去第i个物品,计算出第i - 1个物品最大值,再加上第i个物品就行了
            if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
        }
    }
    
    cout << dp[n][m] << endl;  //dp[n][m]就是求在前n件物品中,物品总体积不超过m的最大价值,与题意相符
    return 0;
}
一维
二维选择削减了保存上一维状态的数组,节省了部分空间。
故对于算法上的优化通过体积从m——v[i]的遍历顺序。
因为每次更新是f[j] = max(f[j], f[j - v[i]] + w[i]);
每次由体积大的调用体积小的,而体积小的存的数据还是未被更新过的,也就是上一层的数据。
故而通过反向遍历实现了调用上一层来更新当前数据的算法。
#include <iostream>
using namespace std;
const int N = 1010;
int dp[N];
int v[N], w[N];
int n, m;
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--)
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    
    cout << dp[m] << endl; 
    return 0;
}
二维费用的01背包
#include <iostream>
using namespace std;
const int N = 1010;
int v[N], w[N], c[N], f[N][N];
int n, m, h;
int main()
{
    cin >> n >> m >> h;
    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> c[i] >> w[i];
    }
    
    for(int i = 1; i <= n; i++)
    {
        for(int j = m; j >= v[i]; j --)
        {
            for(int k = h; k >= c[i]; k --)
            {
                f[j][k] = max(f[j][k], f[j - v[i]][k - c[i]] + w[i]);
            }
        }
    }
    
    cout << f[m][h] << endl;
    return 0;
}
12. 背包问题求具体方案
思路
背包问题求具体方案类似于求最短路径问题
对于求具体方案来说,可以由最后的最大价值逆推
原因是:01背包问题的集合划分就是依靠第i个物品选不选
若选择当前物品,那么当前价值一定是由上一级的f[i - 1][j],或者f[i - 1][j - v[i]] + w[i]转化而来的
若是f[i][j] == f[i - 1][j],那么一定不选第i件物品
否则一定要选第i件物品
当然还有f[i - 1][j] == f[i - 1][j - v[i]] + w[i]的情况,
此时若没有字典序的要求,则都需要去判断,或者这两条路都可以满足题意
本题要求最小字典序,则第i个物品若能选则一定要选!
这样字典序一定最小
本题思路即将最大价值反过来存,然后从1开始往回推,这样就是正向的字典序
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N], w[N];
int n, m;
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i];
    for (int i = n; i >= 1; 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 + 1][j - v[i]] + w[i]);
            }
        }
    }
    
    //此时f[i][m]为最大值
    int j = m;
    for (int i = 1; i <= n; i ++) {
        if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {
            cout << i << ' ';
            j -= v[i];
        }
    }
    
    return 0;
}
习题
Acwing 423 采药
思路
01背包板子题
#include <iostream>
using namespace std;
const int N = 110;
int v[N], f[N][N * 10], w[N];
int main() {
    int s, n;
    cin >> s >> n;
    for (int i = 1; i <= n; i ++) {
        cin >> v[i] >> w[i];
    }
    for (int i = 1; i <= n; i ++) {
        for (int j = 1; j <= s; j ++) {
            f[i][j] = f[i - 1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }
    }
    cout << f[n][s] << endl;
    return 0;
}
426. 开心的金明
思路
金明的预算方案简化版,其实就是01背包问题
#include <iostream>
using namespace std;
const int N = 30, M = 30010;
int f[M];
int main() {
    int m, n;
    cin >> m >> n;
    for (int i = 1; i <= n; i ++) {
        int v, w;
        cin >> v >> w;
        w = v * w;
        for (int j = m; j >= v; j --) {
            f[j] = max(f[j], f[j - v] + w);
        }
    }
    
    cout << f[m] << endl;
    
    return 0;
}
AcWing 1024. 装箱问题
思路
类似板子题,在板子题的基础上变化了一些。如将体积当做价值,然后算答案的补数
#include <iostream>
using namespace std;
const int N = 50, M = 20010;
int f[N][M], v[N];
int main() {
    int s, n;
    cin >> s >> n;
    for (int i = 1; i <= n; i ++) cin >> v[i];
    for (int i = 1; i <= n; i ++) {
        for (int j = 1; j <= s; j ++) {
            f[i][j] = f[i - 1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + v[i]);
        }
    }
    cout << s - f[n][s] << endl;
    return 0;
}
AcWing 278. 数字组合
思路
就是01背包求方案数问题。
集合f[i,j]表示的是在前i个物品中,选择总体积恰好等于j的集合
集合划分为包括第i个物品和不包括第个物品
方案数就为这两种集合价值之和:
f[i, j] = f[i - 1, j] + f[i - 1, j - vi];
#include <iostream>
using namespace std;
const int N = 10010;
int f[N];
int n, m;
int main() {
    cin >> n >> m;
    f[0] = 1;
    for (int i = 1; i <= n; i ++) {
        int v;
        cin >> v;
        for (int j = m; j >= v; j --) {
            f[j] += f[j - v];
        }
    }
    
    cout << f[m] << endl;
}
AcWing 1022. 宠物小精灵之收服
思路
这是道二维费用01背包问题,顾名思义,它需要花费两种费用,思考方式和01背包相同
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010, M = 510;
int V1, V2, n;
int f[N][M];
int main() {
    cin >> V1 >> V2 >> n;
    for (int i = 1; i <= n; i ++) {
        int v1, v2;
        cin >>  v1 >> v2;
        for (int j = V1; j >= v1; j --) {
            for (int k = V2 - 1; k >= v2; k --) {
                f[j][k] = max(f[j][k], f[j - v1][k - v2] + 1);
            }
        }
    }
    
    cout << f[V1][V2 - 1] << ' ';
    
    int k = V2 - 1;
    while (k > 0 && f[V1][k - 1] == f[V1][V2 - 1]) k --;
    cout << V2 - k << endl;
    
    return 0;
}
AcWing 1020. 潜水员
思路
在本题中,所求得为二维费用的背包问题
属性为求最小值
如此,集合f[i, j, k]表示的就不是在前i个物品中,选择总体积不超过j,总花费不超过k的价值
而是在前i个物品中,选择总体积至少为j,总花费至少为k的价值。这样一来j,k都有可能为负数,但没关系,用0来存放就行,会一直被更新成最小值的
同时,求最小值时可以将所有状态初始化为INF,表示不可达状态,f[0, 0, 0]为0,表示这种状态是合法的
#include <iostream>
#include <cstring>
using namespace std;
const int N = 22, M = 80;
int f[N][M];
int n, m, t;
int main() {
    cin >> n >> m >> t;
    memset(f, 0x3f, sizeof f);
    f[0][0] = 0;
    for (int i = 1; i <= t; i ++) {
        int v1, v2, w;
        cin >> v1 >> v2 >> w;
        for (int j = n; j >= 0; j --) {  
        //这里j,k下限必须为0,因为当j超出下限,k恰好等于下限的情况依然是正确的
            for (int k = m; k >= 0; k --) {
                f[j][k] = min(f[j][k], f[max(0, j - v1)][max(0, k - v2)] + w);
            }
        }
    }
    
    cout << f[n][m] << endl;
    return 0;
}
完全背包
思路
完全背包集合划分依据:
若不包括第i个数就是f[i - 1, j]
若包括第i个数就是
从j, j - v, j - 2v, j - 3v,...., j - sv中找到最大值,然后加上w[i]。有些类似LIS问题的思考方式,从前面取最大值然后加上当前这位即为最大值
通过寻找规律,可以发现j, j - v, j - 2v, j - 3v,...., j - sv的最大值就是f[i, j - v] + w[i],所以可以优化一维,形式上和01背包长得差不多
01背包和完全背包题意略有不同,完全背包可以无限取自己,但01背包只能每样取一次
分别:
01背包问题:每次滚动,需要用到上一轮的值,所以一维中j要从m到v[i],从后往前遍历,这样不会用到当前这轮的值
一直用的是上一轮的值,递归起来不会有错误
完全背包问题:每次滚动可以不用上一轮的值,因为可以无限取自己,所以可以从当前轮开始取值,故而一维可以从v[i]到m
从前往后遍历,这样递归会用到这一轮的值,但就是完全背包的特性
在01背包中,每次只会在没更新过的里面取值,也就是不会用到相同的一个,而完全背包只会在更新过的里面取值,会取到相同的一个值。
二维
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int dp[N][N];
int v[N], w[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++)
        {
            dp[i][j] = dp[i - 1][j];
            if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);  //与01背包几乎一样,只是下标略有不同
                                                   //此处01背包是dp[i - 1][j - v[i]] + w[i]因为一直递推的是上一轮的值
                                                   //而完全背包可以从当前轮取,所以直接就是i
                                                   //dp[i][j - v[i]] + w[i]
        }
    }
    cout << dp[n][m];
    return 0;
}
一维
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int dp[N];
int v[N], w[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++)   //这里与01背包不同,01是从m到v[i],完全是v[i]到m
        {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m];
    return 0;
}
习题
1023. 买书
思路
完全背包求方案数问题,和01背包求方案数问题类似
思路也差不多,套个完全背包的板子就行
#include <iostream>
using namespace std;
const int N = 1010;
int f[N];
int a[5] = {0, 10, 20, 50, 100};
int n;
int main() {
    cin >> n;
    f[0] = 1;
    for (int i = 1; i <= 4; i ++) {
        for (int j = a[i]; j <= n; j ++) {
            f[j] += f[j - a[i]];
        }
    }
    
    cout << f[n] << endl;
    
    return 0;
}
1021. 货币系统
思路
完全背包求方案数,与01背包类似
#include <iostream>
using namespace std;
int n, m;
const int N = 20, M = 3010;
long long f[M];
int main() {
    cin >> n >> m;
    f[0] = 1;
    for (int i = 1; i <= n; i ++) {
        int v;
        cin >> v;
        for (int j = v; j <= m; j ++) {
            f[j] += f[j - v];
        }
    }
    
    cout << f[m] << endl;
    return 0;
}
532. 货币系统
思路
这题首先要理解题目意思
理解后可以得知,答案就是从a数组中选出的,将a数组排序后,观察每个数是否能被前面的数表示
若能被表示说明不选,不能则选
就可以转化成一个完全背包求方案数问题
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 110, M = 25010;
int f[M];
int a[N];
int main() {
    int T;
    cin >> T;
    while (T --) {
        int n, cnt = 0;
        cin >> n;
        for (int i = 1; i <= n; i ++) cin >> a[i];
        sort(a + 1, a + 1 + n);
        memset(f, 0, sizeof f);
        f[0] = 1;
        for (int i = 1; i <= n; i ++) {
            if (!f[a[i]]) cnt ++;
            for (int j = 1; j <= a[n]; j ++) {
                if (j >= a[i]) f[j] += f[j - a[i]];
            }
        }
        cout << cnt << endl;
    }
    
    return 0;
}
多重背包
思路
和完全背包略有不同,完全背包是每个物品可以选无限次,多重背包是每个物品只能取si次,问最大价值
与01背包的不同在于多了一重枚举,枚举每种物品买多少件
当数据范围较大时,可以使用二进制优化,或者单调队列优化
暴力写法
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int v[N], w[N], s[N];
int dp[N][N];
int 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 = 1; j <= m; j++) //多重背包问题当数据范围较小的时候,类似于完全背包
            for(int k = 0; k <= s[i] && k * v[i] <= j; k++)
                dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);
    // for(int i = 1; i <= n; i ++)   //多重背包不能用完全背包的方式优化成一维
    //     for(int j = v[i]; j <= m; j++)
    //         dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    cout << dp[n][m] << endl;
    return 0;
}
二进制优化
多重背包问题
当数据范围较大时,不便用完全背包的暴力做法写
大概率会超时,此时,有了一种新的优化方式——二进制优化
二进制优化:
指将每个物品的最大个数分为若干个含有二进制个数的份数
例如:14可以是1,2,4,7
本来第4位应该是8,但是8会超过最大个数14,最后一个是用来补缺的,二进制之和与最大个数的差值
好处:
将本来很大的个数,要循环一整个s[i]的次数,优化成logs次
只需要把它们分成若干份,然后用01背包问题的解决办法
看是否能选就OK了
#include <iostream>
using namespace std;
const int N = 25000, M = 2010;
int dp[N];
int v[N], w[N];
int n , m;
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] = k * a;
            w[cnt] = b * k;
            s -= k;    //以二进制分份
            k *= 2;
        }
        if(s)   //假如最后还有剩余,这就是补缺
        {
            cnt ++;
            v[cnt] = s * a;
            w[cnt] = s * b;
        }
    }
    
    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 << dp[m] << endl;
    return 0;
}
多重背包一般都是O(n^3)的时间复杂度,所以对于数据大小卡的很死,故而有此优化版本。
思路:
优化s,将s化为logs级别。
原理:二进制。通过将s分解为若干个二进制整数,而二进制整数的任意组合可以组成0——s的任意一个数。
然后将他们放进一个全新的集合中。从而转化为01背包问题
#include <iostream>
#include <vector>
using namespace std;
const int N = 2010;
int v[N], w[N], s[N], f[N];
int n, m;
struct Good{
    int v, w;
};
int main()
{
    //多重背包二进制优化版本,对于n = 1000的O(n^3)算法复杂度,优化效果比较好
    vector<Good> good;  // 定义一个Good类的vector
    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 k = 1; k <= s[i]; k *= 2)
        {
            s[i] -= k;
            good.push_back({v[i] * k, w[i] * k}); //将s个物品分为若干个含有二进制整数个物品
        }
        
        if(s[i] > 0) 
        {
            good.push_back({v[i] * s[i], w[i] * s[i]}); //如果有些数不能被二进制数全分,就把剩余的数直接加入集合
                                                    //例如:10: 1 2 4 3,最后一个三显然不是完全分完的    
        }
    }
    
    
    for(auto t : good)  //01背包板子
    {
        for(int j = m; j >= t.v; j --)
        {
            f[j] = max(f[j], f[j - t.v] + t.w);
        }
    }
    
    cout << f[m] <<endl;
    return 0;
}
单调队列优化版
多重背包使用单调队列优化版
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 20010;
int n, m;
int f[N], g[N], q[N];
int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ )
    {
        int v, w, s;
        cin >> v >> w >> s;
        memcpy(g, f, sizeof f);
        for (int j = 0; j < v; j ++ )
        {
            int hh = 0, tt = -1;
            for (int k = j; k <= m; k += v)
            {
                if (hh <= tt && q[hh] < k - s * v) hh ++ ;
                while (hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt -- ;
                q[ ++ tt] = k;
                f[k] = g[q[hh]] + (k - q[hh]) / v * w;
            }
        }
    }
    cout << f[m] << endl;
    return 0;
}
习题
1019. 庆功会
本题数据范围较小,直接用\(n^3\)的暴力写法即可,板子题
#include <iostream>
using namespace std;
const int N = 6010;
int n, m;
int f[N];
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) {
        int v, w, s;
        cin >> v >> w >> s;
        for (int j = m; j >= v; j --) {
            for (int k = 0; k <= s && k * v <= j; k ++) {
                f[j] = max(f[j], f[j - k * v] + k * w);
            }
        }
    }
    
    cout << f[m] << endl;
}
分组背包
思路
顾名思义,就是将需要买的物品分组,然后每组只取一件物品,问最大价值
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int dp[N];
int v[N][N], w[N][N], s[N];
int n, m;
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> s[i];
        for(int j = 1; 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--)//01背包思想。因为要用到上一轮的数,所后往前遍历,保证用的时候没被覆盖
            for(int k = 1; k <= s[i]; k++)  //循环s[i]次,找到本组中能取到的最大值
                if(j >= v[i][k]) dp[j] = max(dp[j], dp[j - v[i][k]] + w[i][k]);
            
            
    cout << dp[m] << endl;
    return 0;
}
习题
487. 金明的预算方案
思路
本题是典型分组背包问题,不过枚举每组的物品方法使用的是二进制枚举法
#include <iostream>
#include <algorithm>
#include <vector>
#define v first
#define w second
using namespace std;
typedef pair<int, int> PII;
const int N = 32010, M = 65;
int f[N];
vector<PII> informal[M];    //附件
PII formal[M];              //主件
int n, m;
int main() {
    cin >> m >> n;
    for (int i = 1; i <= n; i ++) {
        int v, w, q;
        cin >> v >> w >> q;
        if (!q) formal[i] = {v, v * w};
        else informal[q].push_back({v, v * w});
    }
    
    for (int i = 1; i <= n; i ++) {
        if (formal[i].v) {                           //若该主件存在
            for (int j = m; j >= 0; j --) {
                //使用二进制枚举法枚举可能有的状态
                for (int k = 0; k < 1 << informal[i].size(); k ++) {
                    int x = formal[i].v, y = formal[i].w;
                    //这是一组中选择某(几)件物品后,得到的体积和价值
                    for (int h = 0; h < informal[i].size(); h ++) {
                        if (k >> h & 1) {
                            x += informal[i][h].v;
                            y += informal[i][h].w;
                        }
                    }
                    if (j >= x) f[j] = max(f[j], f[j - x] + y);
                }
            }
        }
    }
    
    cout << f[m] << endl;
    
    return 0;
}
1013. 机器分配
思路
分组背包 + 背包问题求具体方案
分组背包和多重背包类似,都需要循环每组(种)物品个数,然后确定最大价值。
但实际代码上与01背包类似
求具体方案数:
可以是先逆序存储最大价值,然后正序由最大价值推出具体方案
也可以不改变顺序,直接推出最大价值,然后用数组存储由最大价值推出的具体方案
#include <iostream>
using namespace std;
const int N = 11, M = 16;
int f[N][M];
int w[N][M];
int n, m;
int way[N];
int main() {
     cin >> n >> m;
     for (int i = 1; i <= n; i ++)
        for (int j = 1; j <= m; j ++) 
            cin >> w[i][j];
            
    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 <= m; k ++) {
                if (j >= k) f[i][j] = max(f[i][j], f[i - 1][j - k] + w[i][k]);
            }
        }
    }
    
    cout << f[n][m] << endl;
    
    int j = m;
    for (int i = n; i >= 1; i --) {
        for (int k = 0; k <= m; k ++) {   //k==0表示不取当前分组的物品
            if (f[i][j] == f[i - 1][j - k] + w[i][k]) {
                way[i] = k;
                j -= k;
                break;
            }
        }
    }
    
    for (int i = 1; i <= n; i ++) cout << i << ' ' << way[i] << endl;
    return 0;
}
混合背包
思路
混合背包是由01背包,完全背包,多重背包组成的混合体,事实上这三种背包在集合的确定上是相通的
且当前物品的最大价值和上件物品是什么样的没有关系,故能直接计算
对于每种不同的物品,用各自的方法计算即可。
同时由于多重背包使用二进制优化方法时和01背包状态转移类似,可以合并
7. 混合背包问题
#include <iostream>
using namespace std;
const int N = 1010;
int f[N];
int n, m;
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) {
        int v, w, s;
        cin >> v >> w >> s;
        if (!s) {
            for (int j = v; j <= m; j ++) {
                f[j] = max(f[j], f[j - v] + w);
            }
        }else {
            if (s == -1) s = 1;            //若当前为01背包,则只有一种
            //多重背包二进制优化模板
            for (int k = 1; k <= s; k *= 2) {
                for (int j = m; j >= k * v; j --) {
                    f[j] = max(f[j], f[j - k * v] + k * w);
                }
                s -= k;
            }
            if (s) {
                for (int j = m; j >= s * v; j --) {
                    f[j] = max(f[j], f[j - s * v] + s * w);
                }
            }
        }
    }
    
    cout << f[m] << endl;
    
    return 0;
}
有依赖的背包问题
思路
树形dp + 分组背包
将每个节点当做一个物品组。用dfs遍历到叶子结点,然后一层层向上的物品组中选择最优解
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int n, m;
const int N = 110;
int f[N][N];
int v[N], w[N];
int h[N], e[N], ne[N], idx;
void add(int a, int b) {
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx ++;
}
void dfs(int u) {
    for (int i = h[u]; ~i; i = ne[i]) {
        int son = e[i];
        dfs(son);
        
        for (int j = m - v[u]; j >= 0; j --) {    //按体积划分
            for (int k = 0; k <= j; k ++) {
                f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
            }
        }
    }
    
    for (int i = m; i >= v[u]; i --) f[u][i] = f[u][i - v[u]] + w[u]; //添加该节点的价值
    for (int i = 0; i < v[u]; i ++) f[u][i] = 0;
    //本节点中,不可能存在比该节点体积小的还有价值的方案,因为有依赖,该节点必须选择
}
int main() {
    cin >> n >> m;
    
    memset(h, -1, sizeof h);
    
    int root;
    for (int i = 1; i <= n; i ++) {
        int p;
        cin >> v[i] >> w[i] >> p;
        if (p == -1) {
            root = i;
        }else add(p, i);
    }
    
    dfs(root);
    
    cout << f[root][m] << endl;
    return 0;
}
背包问题求方案数
思路
求最优方案数可以分成两步
第一步求出最优方案,也就是最大价值
第二部求最大价值下的方案数具体有多少种
而求出当前i,j下最大价值,然后求出相应的方案数即可一步步递推出最终结果
集合划分:
当前最大价值f[i, j]若是等于f[i - 1, j],那么相应方案一定从它转化而来,cnt += g[i - 1, j]
若是等于f[i - 1, j - v] + w,相应方案一定从它转化过来,cnt += g[i - 1, j - v]
若是两种价值相等,说明相应方案可以从这两种价值对应的方案同时转化过来也就可以两个同时加上
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int f[N];                         //记录最大价值
int g[N];                         //记录方案数
int n, m;
int main() {
    cin >> n >> m;
    
    memset(f, -0x3f, sizeof f);
    f[0] = 0;
    //上面两行不加也可以,因为0就已经相当于没有价值,求最大值可以不用加上负无穷,但是求最小值必须赋值正无穷
    g[0] = 1;
    for (int i = 1; i <= n; i ++) {
        int v, w;
        cin >> v >> w;
        for (int j = m; j >= v; j --) {
            int maxv = max(f[j], f[j - v] + w);
            int cnt = 0;
            if (maxv == f[j]) cnt += g[j];
            if (maxv == f[j - v] + w) cnt += g[j - v];
            g[j] = cnt % mod;
            f[j] = maxv;
        }
    }
    
    int res = 0, cnt = 0;
    for (int i = 0; i <= m; i ++) res = max(res, f[i]);    //找到最优解
    for (int i = 0; i <= m; i ++) {
        if (res == f[i]) {
            cnt = (cnt + g[i]) % mod;                      //对最优解的方案数加和
        }
    }
    
    cout << cnt << endl;
    return 0;
}
734. 能量石
思路
贪心 + dp
在dp之前,需要从题目中找到某些性质,可以使题目通过某种变化变成可以使用dp解决
本题性质为对于任意两个石头,si * li+1 < si+1 * li
然后就是01背包求最大价值
这题集合划分依据为恰好等于体积j的,所以需要赋值负无穷
因为不是最多为j的最大价值,所以需要遍历数组f,找到最大价值
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010, M = 110;
int f[N];
struct Stone {
    int s, e, l;
    bool operator < (const Stone &W) const {       //重载<运算符 令顺序为si * li+1 < si+1 * li
        return s * W.l < W.s * l;
    }
}stone[M];
int main() {
    int T;
    cin >> T;
    for (int H = 1; H <= T; H ++) {
        int n, m = 0;
        cin >> n;
        for (int i = 1; i <= n; i ++) {
            int s, e, l;
            cin >> s >> e >> l;
            stone[i] = {s, e, l};
            m += s;
        }
        
        sort(stone + 1, stone + 1 + n);
        memset(f, -0x3f, sizeof f);              //集合划分依据为恰好等于体积j的,所以需要赋值负无穷
        f[0] = 0;
        
        for (int i = 1; i <= n; i ++) {
            int s = stone[i].s, e = stone[i].e, l = stone[i].l;
            for (int j = m; j >= s; j --) {
                f[j] = max(f[j], f[j - s] + e - (j - s) * l);
            }
        }
        
        int res = 0;
        for (int i = 0; i <= m; i ++) res = max(res, f[i]);
        cout << "Case #" << H << ": " << res << endl;
    }
    
    return 0;
}

 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号