背包基础知识随笔(写的一般)

目录

1)01 背包

暴力版: 

①dp[i ][j] 至多用j的空间拿完前i件物品 能取得的最大值

 ②dp[i ][j] 刚好用j的空间拿完前i件物品 能取得的最大值 

③dp[i ][j] 至少用j的空间拿完前i件物品 能取得的最大值

2)完全背包

01背包的思路转换

另一写法:

 3)多重背包

暴力版:

① 二进制优化 O(n*m*log(cnt))

 ②单调队列优化 O(n*m)‘

4)分组背包

5)二维费用的背包问题

6)有依赖的背包问题

7)背包问题求方案 

8)完结撒花


1)01 背包

传送阵: https://acm.sdut.edu.cn/onlinejudge3/problems/2825https://acm.sdut.edu.cn/onlinejudge3/problems/2825

   

暴力版: 

用1表示拿了,0表示没拿,每种情况可以用n个01数来表示,比如5个东西拿了第1 2 4个,表示出来就是1 1 0 1 0,所以从0 0 0 0 0~1 1 1 1 1(0~2^n -1)枚举就可以计算出所有的情况

1000个物品就是跑 2^1000 ,绝对不行的

int w[N], v[N];
int ans, m, space, maxx = 0;
scanf("%d %d", &n, &m); // m为背包空间
for (i = 1; i <= n; i++)
{
    scanf("%d %d", &v[i], &w[i]);
}
for (k = 0; k < 1 << n; k++)
{
    space = 0; // space记录当前情况需要的空间
    ans = 0;   // ans记录当前情况的价值
    for (i = 0; i < n; i++)
    {
        if (k & (1 << i)) // 1<<i,此时这个1出现在第i+1位,说明的是第i+1件物品是否拿了
        {
            space += v[i + 1];
            ans += w[i + 1];
        }
    }
    if (space <= m)
        maxx = max(maxx, ans);
}
cout << maxx;

①dp[ i ][ j ] 至多用j的空间拿完前i件物品 能取得的最大值

        也就是用j的空间,在前i件物品的范围内,拿到的物品所构成的dp数组价值最大,此处的至多是价值的最多

        第i件物品不选则为dp[i-1][j],选就是前i-1件物品用(j-v[i])的空间能拿到的最大价值加w[i];公式为     dp[i][j] = max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);

        又因为拿i的结果仅和拿完i-1的结果有关,所以拿完i-2,i-3件物品等等的状态便没有必要保存了,可以用滚动数组来进行空间的优化,所以我们只讨论两种情况就是,拿完i件和拿完i-1件,所以考虑到用%2的方式,因为%2的结果只有0和1

        这里我们用1和0表示拿了前i件和拿了前i-1件的状态,和上边的01代表拿了和没拿是不一样的,而且没有任何关系。

#include <iostream>
#include <algorithm>
#include <cmath>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int w[N], v[N], dp[2][N];
int main()
{
	int n, m, i, j, k;
	while (~scanf("%d %d", &n, &m))
	{
		memset(dp, 0, sizeof(dp));
		for (i = 1; i <= n; i++)
		{
			scanf("%d %d", &v[i], &w[i]);
		}
		for (i = 1; i <= n; i++)
		{
			for (j = 0; j <= m; j++)
			{
				if (j >= v[i])
					dp[i % 2][j] = max(dp[(i + 1) % 2][j], dp[(i + 1) % 2][j - v[i]] + w[i]);
				else
					dp[i % 2][j] = dp[(i + 1) % 2][j];
			}
		}
		printf("%d\n", dp[n % 2][m]);
	}
	return 0;
}

因为用到的有关i-1的数据(j-v[i])一定是小于j的,所以可以考虑从后往前更新,这样更新后面时,数组里存的还是上一组的数据,从而实现一维写法。所以,一维写法必须是从后往前遍历的

#include <iostream>
#include <algorithm>
#include <cmath>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int w[N], v[N], dp[N];
int main()
{
    int n, m, i, j, k;
    while (~scanf("%d %d", &n, &m))
    {
        memset(dp, 0, sizeof(dp));
        for (i = 1; i <= n; i++)
        {
            scanf("%d %d", &v[i], &w[i]);
        }
        for (i = 1; i <= n; i++)
        {
            for (j = m; j >= v[i]; j--)
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
                //如果不拿更好的话,就不变,因为dp[j]存的本来就是拿完i-1的最优状态
        }
        printf("%d\n", dp[m]);
    }
    return 0;
}

 当未开始for  j循环时,dp[]里存的是dp[i-1]时的情况,所以dp[j]直接就是i-1的可以直接用,然后j-v[i]也是i-1的,但是它里面存储的数据在更新dp的更大的数的时候不能变,所以就从大到小遍历,这样更小的数就会在更后面遍历

 ②dp[ i ][ j ] 刚好用j的空间拿完前i件物品 能取得的最大值 

此处刚好是指 j 的空间刚好放满物品,不一定是最大价值(最优方案)

int dp[N][M];
memset(dp, 0x8f, sizeof(dp));
for (i = 1; i <= n; i++)
{
    scanf("%d %d", &v[i], &w[i]);
}
dp[0][0] = 0;
for (i = 1; i <= n; i++)
{
    for (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];
    }
}
int maxx = 0;
for (i = 1; i <= m; i++)
{
    maxx = max(dp[n][i], maxx);
}
printf("%d\n", maxx);

        最开始全初始化成负无穷,在更新时   dp[i][j] = max(dp[i-1][j], dp[i-1][j - v[i]] + w[i]);我们两个for循环模拟了所有放置情况,但是如果第i件物品放置的情况到不了dp[0][0]的话那这种放置情况是不合法的,我们就要去除他;如果dp[i-1][j - v[i]] 仍然时负的很大的数,加上w[i]也是无济于事的,所以要想更新完是正的,说明dp[i-1][j - v[i]]是正的,往上推,必然是用dp[0][0]的值更新过的,也就是能做到我们假设的刚好用i的空间

        最后答案不一定用满了m的空间(但一定小于等于m,同时可能一个也拿不了,那价值就是0,所以maxx初始为0),所以for(i~m)更新了一遍maxx,当然也可用上述的方法来优化

③dp[ i ][ j ] 至少拿完前 i 件物品满足 j 的价值需求,所需要的背包最小体积

i为前i件物品,能拿完j的价值,dp[i][j]表示空间能取得的最小值,或者dp[i][j]表示满足我j价值最少需要多大的空间来装j价值的物品。

 (没找到合适的,这个题二维费用,和一维也差不多,下面也有二位费用分块,虽然我在那也没写啥)  

题意:需要m氧气,n氮气,然后从k个气缸里选,既可以满足我的气体量,又要质量最小 

传传:https://www.acwing.com/activity/content/problem/content/1278/

          一维举例:每个物品有体积v[i],价值w[i]两个属性,要求拿到j价值( 可以超过m )的最小体积,那么假设dp[i][j]为拿完前 i 件物品,至少满足 j 价值的最小物品总体积(注意之前 j 是空间,dp[i][j] 的值是价值,而这里 j 是价值,dp[i][j]的值是空间 反过来的)

        在对价值的枚举过程,如果此时枚举的价值为 j,但是目前的这件物品的价值大于 j ,这种情况是要更新的,条件是拿到至少 j 的价值,那么我们取了一件价值大于 j 的物品,毫无疑问是合理 满足题意的,这时候我们也不需要再取别的物品就可以满足 j 空间的需求了,所以我们拿他和dp[i-1][0]来进行状态的更新

        举个栗子:dp数组的更新会有两种情况,比如我要价值为2的物品,目前我有两个物品,一个是体积为1,价值为1,一个是体积为10,价值为100,这时候我需要拿第二个物品,也就是dp[i][j] = dp[i-1][0]+10;;或者我要价值为4的物品,目前我有两个物品,一个是体积为1,价值为2,另一个是体积为4,价值为3,这时候我要全拿,也就是dp[i][j] = dp[i-1][j-3]+4;;然后会生成不同的两种选取方式,可能是拼凑版,也可能是只拿一个价值大的,如果是价值大的那我们在运算的时候可以取max(0, j - 我们当前要拿的物品价值),如果这个物品拿完之后爆掉的话,那我们就可以直接清0,然后就用dp[i-1][0]来更新,然后针对这两种情况取min,就可以得到最优方案。

        同时注意,是至少拿到 j 的价值时,所以在初始化的时候应该像(刚好)的那种情况来初始化数组,但是此处设的是正无穷,因为我们最后的dp数组取的是min;也就是dp[0][ j ]的时候是不合法的,也就是我拿了0件物品,但是我有价值,呵,怎么可能,这是不合法的状态,不能通过这种状态更新别的点,所以把这些状态初始化成不可能更新别的点的状态

所以这道题:

这道题考虑的是上述一维解法的二维费用问题,也就是把上面写的用滚动数组写成一维的,然后再加一维。两维表示的都是价值,但是是相互独立的。所以下述代码中的j和k代表的是氧气的容量和氮气的容量,然后最后算出的dp[j][k]表示的是我们将会拿的气缸的质量

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
// #include <bits/stdc++.h>
using namespace std;
const int N = 21 + 10, INF = 0x3f3f3f3f, M = 79 + 7;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int dp[N][M];
int main()
{
    memset(dp, 0x3f, sizeof(dp));
    dp[0][0] = 0;
    int n, m1, m2, i, j, k;
    int a, b, c;
    cin >> m1 >> m2 >> n;
    for (i = 1; i <= n; i++)//列举气缸的个数
    {
        cin >> a >> b >> c;
        for (j = m1; j >= 0; j--)//列举需要氧气的容量
        {
            for (k = m2; k >= 0; k--)//列举需要氮气的容量
            {
                dp[j][k] = min(dp[j][k], dp[max(0, j - a)][max(0, k - b)] + c);
            }
        }
    }
    cout << dp[m1][m2] << endl;
    return 0;
}

  dp[j][k] = min(dp[j][k], dp[max(0, j - a)][max(0, k - b)] + c);

就是当前 j k 和物品属性哪个大,物品属性更大就用 0 的位置来更新状态。

        通过上述思路可以发现,我们定义状态是,都是拿完前[i]件物品,这个物品顺序是题目给的,顺序的打乱不影响结果的计算,且i的状态之和i-1的状态有关,所以可以在for循环输入的时候就同时把dp[i][j] 进行更新,这样连v[i],w[i]也不用记录了,也是对空间的优化。但是有的题目时要求输出方案的,这种情况所有的数据都要保留,所以这种时候一般会把输入和数据处理分开,就像上面的。

2)完全背包

传送阵: https://acm.sdut.edu.cn/onlinejudge3/problems/2826

与01背包的区别就是每种物品无限个

01背包的思路转换

可以通过01背包的思想,拿两个可以看作拿了一个体积2*v[i],价值2*w[i]的物品,拿k个可以看作拿了一个体积k*v[i],价值k*w[i]的物品所以可以写出暴力版(注意k*v[i]<=j,总不能拿无限个吧)

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

首先也可像01背包中的①思路一样,假设dp[i][j]为刚好j的空间拿前i件物品取得的最大价值

后面直接讲②,dp[i][j]为用j的空间拿前i件物品取得的最大价值

dp[i][j]=max({dp[i-1][j],dp[i-1][j-v[i]]+w[i],dp[i-1][j-2*v[i]+2*w[i],dp[i-1][j-3*v[i]]+3w[i],.....});

              dp[i][j-v[i]]=max({dp[i-1][j-v[i]],dp[i-1][j-2*v[i]+1*w[i],dp[i-1][j-3*v[i]]+2*w[i],.....});

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

和01背包的公式区别在i和i-1

同理可用滚动数组优化

#include <iostream>
#include <algorithm>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10, INF = 0x3f3f3f3f;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int dp[2][N], w[N], v[N];
int main()
{
    int n, m, i, j, k;
    while (~scanf("%d %d", &n, &m))
    {
        memset(dp, 0, sizeof dp);
        for (i = 1; i <= n; i++)
            scanf("%d", &w[i]);
        for (i = 1; i <= n; i++)
            scanf("%d", &v[i]);
        for (i = 1; i <= n; i++)
        {
            for (j = 1; j <= m; j++)
            {
                if (j >= v[i])
                    dp[i % 2][j] = max(dp[(i + 1) % 2][j], dp[i % 2][j - v[i]] + w[i]);
                else
                    dp[i % 2][j] = dp[(i + 1) % 2][j];
            }
        }
        printf("%d\n", dp[n % 2][m]);
    }
    return 0;
}

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

所以在更新dp[i][j]的时候要求dp[i][j-v[i]]已经更完了,所以正向来降维

#include <iostream>
#include <algorithm>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10, INF = 0x3f3f3f3f;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int dp[N], w[N], v[N];
int main()
{
    int n, m, i, j, k;
    while (~scanf("%d %d", &n, &m))
    {
        memset(dp, 0, sizeof dp);
        for (i = 1; i <= n; i++)
            scanf("%d", &w[i]);
        for (i = 1; i <= n; i++)
            scanf("%d", &v[i]);
        for (i = 1; i <= n; i++)
        {
            for (j = v[i]; j <= m; j++)
            {
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
            }
        }
        printf("%d\n", dp[m]);
    }
    return 0;
}

 这里的顺序的原因是:dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]);我们要有dp[i-1]时的状态,那么不清0他就会自带上一次的状态,然后我们需要dp[i]时的状态,然而j-v[i]一定小于j,所以我们就需要顺序,保证之前已经更新过数据了

以上的做法时根据01背包做法转化来的,可以通过另一个角度来看此题:

另一写法:

同样定义dp[j]为j空间能取得的最大价值,最终答案就是dp[m](m为背包空间)

j的空间拿了第i个物品,也就是在j-v[i]空间拿到的最大价值+w[i],也就是dp[j-v[i]]+w[i];

#include <iostream>
#include <algorithm>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10, INF = 0x3f3f3f3f;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int dp[N], w[N], v[N];
int main()
{
    int n, m, i, j, k;
    while (~scanf("%d %d", &n, &m))
    {
        for (i = 1; i <= n; i++)
            scanf("%d", &w[i]);
        for (i = 1; i <= n; i++)
            scanf("%d", &v[i]);
        for (i = 1; i <= m; i++)
        {
            dp[i] = dp[i - 1];
            //一个也不拿,i空间的最大价值就是i-1空间的最大价值
            for (j = 1; j <= n; j++)
            {
                if (i >= v[j])//能拿就比较一下试试
                    dp[i] = max(dp[i], dp[i - v[j]] + w[j]);
            }
        }
        printf("%d\n", dp[m]);
    }
    return 0;
}

这样写有一个好处就是多组输入少了一个memset,因为求dp[1]的时候会从dp[0]那里把0自动继承过来,从而减去了初始化的麻烦和时间

 3)多重背包

 

 传送门:https://www.acwing.com/problem/content/4/

多重背包:第i个物品有cnt[i]个

同理,先有暴力版本:

暴力版:

#include <iostream>
#include <algorithm>
#include <cmath>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e2 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int w[N], v[N], cnt[N], dp[N][N];
int main()
{
    int i, j, n, m, k;
    cin >> n >> m;
    for (i = 1; i <= n; i++)
    {
        scanf("%d %d %d", &v[i], &w[i], &cnt[i]);
    }
    for (i = 1; i <= n; i++)
    {
        for (j = 0; j <= m; j++)
        {
            for (k = 0; k <= cnt[i] && k * v[i] <= j; k++)
                dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);
        }
    }
    cout << dp[n][m] << endl;
    return 0;
}

dp[i][j]=max({dp[i-1][j],dp[i-1][j-v[i]]+w[i],dp[i-1][j-2*v[i]+2*w[i],.....dp[i-1][j-k*v[i]]+kw[i]});

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

                                          dp[i-1][j-(k+1)*v[i]]+k*w[i]);

就不能像完全背包一样优化了(完全背包因为是无穷个,有限的是空间,所以后面的可以消掉,但多重背包空间大了的话,就会多出后面一项)

① 二进制优化 O(n*m*log(cnt))

任何一个数,比如63       63=1+2+4+8+16+32

所以1~65中的任意数都可以用 1 2 4 8 16 32这五个数选取来表示

所以多重背包比如选14个最优,就可以看成是 拿了(2+4+8)个

同理 选k个,也可以从这五个数字选取

就可以把这个 个数限制为63背包改为 {v[i],w[i]},{2v[i],2w[i]},{4v[i],4w[i]},{8v[i],8w[i]},{16v[i],16w[i]},{32v[i],32w[i]}这样五个01背包

又比如个数是65 65=1+2+4+8+16+32+2

拆完后还有多的,就再加一个{2v[i],2w[i]}的背包

1,2,4,8,16,32可以表示1~63,所有组合都加2,可以表示出3~65,所以这6个数一定可以表示出1~65任何数 。

#include <iostream>
#include <algorithm>
#include <cmath>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int w[N], v[N], cnt, dp[2500];
int main()
{
    int i, j, n, m, k, z = 0;
    int vv, ww;
    cin >> n >> m;
    for (i = 1; i <= n; i++)
    {
        scanf("%d %d %d", &vv, &ww, &cnt);
        k = 1;//先转成01背包
        while (cnt >= k)
        {
            v[++z] = vv * k;
            w[z] = ww * k;
            cnt -= k;
            k <<= 1;
        }
        if (cnt)//是否有多出来的
        {
            v[++z] = vv * cnt;
            w[z] = ww * cnt;
        }
    }
    for (i = 1; i <= z; i++)
    {
        for (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*m)‘

该组有k个

dp[i][j]=max({dp[i-1][j],dp[i-1][j-v[i]]+w[i],dp[i-1][j-2*v[i]+2*w[i],.....dp[i-1][j-k*v[i]]+kw[i]});

假设当前空间为j,物品体积为v,拿一个是和体积为j-v的比,拿两个是和体积为j-2v的比(我们先假设物品足够多,能拿到背包装不下东西却还有剩余),然后拿三个是和体积为j-3v的比,一直拿拿拿,背包拿不下了,这时候我们和 比如说 j-kv的空间比,所以j-kv<v因为拿不下了,所以空间为
j-kv
j-(k-1)v
j-(k-2)v
...
j-3v
j-2v
j-v

这些是相关的,可以看出是以v跳跃的,尽头j-kv是一个小于v的数,所以每一个小于v的数都可以扯出这么一条链,比如(假设v>2,k>4)
j-kv==1
就是 1   1+v   1+2v   1+3v   1+4v....
j-kv==2
就是2   2+v   2+2v   2+3v   2+4v....

这些是相关联的,然后就根据余数,把每条链看成一组。通过枚举余数,把每组考虑出来。所以空间为j的状态 拿一个与j-v[i]空间状态有关,拿两个与j-2*v[i]的状态有关,假设r为j%v[i],那我们最多可拿min( (j-r)/v[i] , k )个

 首先在背包空间够大的时候,在空间j的时候我们是从(绿色框的状态+w[i]*拿的个数)来更新,在空间( j - v[i] )的时候我们是从(红色框的状态+w[i]*拿的个数)来更新,由此可以看出“滑动窗口”那题的模型,也就是单调队列,但是每个点更新 j 时还要加上w[i]*拿的个数,所以每个点对于 j 更新时贡献不同,在维护单调队列时要考虑到这一点。所以在维护的过程中,比较的时候,我们可以给每个点减掉 (当前点空间-r)/v[i]*w[i]   大小,类似于放到同一起跑线来比较,因为(j- k*v[i])的状态要加 k*w[i],(j-(k-1)*v[i])的状态要加(k-1)*w[i]......,

对于他们,连续的来看是要加上一个等差数列,所以在维护的时候减掉一个等差数列就ok了。 

背包空间拿不满个的时候,也没关系,此时队列的队首存的就是最好的。

同时也可看出,每个点的状态只和差值为 v[i] 的整数倍有关的点有关,所以最后我们通过枚举v[i]的余数来实现对 r+x*v[i] 的点的状态更新。

#include <iostream>
#include <algorithm>
#include <cmath>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7, M = 2e4 + 10;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int dp[2][M], q[M];
int v[N], w[N], cnt[N];
int main()
{
    int n, m, i, j, k, hh, tt;
    cin >> n >> m;
    for (i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i] >> cnt[i];
    }
    for (i = 1; i <= n; i++)
    {
        for (k = 0; k < v[i]; k++)//枚举余数
        {
            hh = 0;
            tt = -1;
            for (j = k; j <= m; j += v[i])
            {
                if (hh <= tt && (j - q[hh]) / v[i] > cnt[i])
                    hh++;
                while (hh <= tt && dp[(i + 1) % 2][q[tt]] - (q[tt] - k) / v[i] * w[i] <= dp[(i + 1) % 2][j] - (j - k) / v[i] * w[i])
                    tt--;
                q[++tt] = j;
                dp[i % 2][j] = dp[(i + 1) % 2][q[hh]] + (j - q[hh]) / v[i] * w[i];
            }
        }
    }
    cout << dp[n % 2][m] << endl;
    return 0;
}

hh = 0;
tt = -1;
for (j = k; j <= m; j += v[i])
{
    if (hh <= tt && (j - q[hh]) / v[i] > cnt[i])
        hh++;
    while (hh <= tt && dp[(i + 1) % 2][q[tt]] - (q[tt] - k) / v[i] * w[i] <= dp[(i + 1) % 2][j] - (j - k) / v[i] * w[i])
        tt--;
    q[++tt] = j;
    dp[i % 2][j] = dp[(i + 1) % 2][q[hh]] + (j - q[hh]) / v[i] * w[i];
}

首先建造优先队列,hh=0,tt=-1;

判断当前队首是否已经过失,也就是(j - q[hh]) / v[i] > cnt[i],说明他和 j 的距离已经超过了cnt[i]的限制,那这个点就没用了,hh++就是队首出队操作。(j+=v[i],所以从队首看每次最多只会有一个超过限制,但是队尾看过来可能不止一个,所以后面那个用的是while)

再从队尾开始看  dp[(i + 1) % 2][q[tt]] - (q[tt] - k) / v[i] * w[i] <= dp[(i + 1) % 2][j] - (j - k) / v[i] * w[i],说明当前j的状态比队尾的更好,tt--就是队尾出队,最后把 j 放进队列中。

最后用队首的值来更新dp[i][j],也就是拿完i-1个,用了q[hh]空间的状态,再拿(j-q[hh])/v[i]个第 i 件物品

4)分组背包

 传送阵:https://www.acwing.com/problem/content/9/

每组中的物品只能选一个且一次

dp[i][j]为j的空间拿完第i组的物品能取得的最大价值

对j的空间枚举一遍第i组的所有物品,如果能拿,就试着更新一下,找到最好的情况

与01背包基本相同,只是因为第i组物品有很多种,所以我们要找到最优的物品放入j的空间内

#include <iostream>
#include <algorithm>
#include <cmath>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int dp[2][105], cnt[105], w[105][105], v[105][105];
int main()
{
    int n, m, i, j, k;
    cin >> n >> m;
    for (i = 1; i <= n; i++)
    {
        cin >> cnt[i];
        for (j = 1; j <= cnt[i]; j++)
        {
            cin >> v[i][j] >> w[i][j];
        }
    }
    for (i = 1; i <= n; i++)
    {
        for (j = 0; j <= m; j++)
        {
            dp[i % 2][j] = dp[(i + 1) % 2][j];
            for (k = 1; k <= cnt[i]; k++)
            {
                if (j >= v[i][k])
                    dp[i % 2][j] = max(dp[i % 2][j], dp[(i + 1) % 2][j - v[i][k]] + w[i][k]);
            }
        }
    }
    cout << dp[n % 2][m] << endl;
}

可以看出核心转变成了01背包,那么自然可以用背包的一维方法来替换一下核心部分实现空间的降维
先枚举分组 再空间 再决策 很重要不要乱了

#include <iostream>
#include <algorithm>
#include <cmath>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int dp[105], cnt[105], w[105][105], v[105][105];
int main()
{
    int n, m, i, j, k;
    cin >> n >> m;
    for (i = 1; i <= n; i++)
    {
        cin >> cnt[i];
        for (j = 1; j <= cnt[i]; j++)
        {
            cin >> v[i][j] >> w[i][j];
        }
    }
    for (i = 1; i <= n; i++)
    {
        for (j = m; j >= 0; j--)
        //01背包只要j>=v[j]就可以了,但是分组背包由于不知道最小的背包多大,所以改成了j>=0;
        {
            for (k = 1; k <= cnt[i]; k++)
            {
                if (j >= v[i][k])
                    dp[j] = max(dp[j], dp[j - v[i][k]] + w[i][k]);
            }
        }
    }
    cout << dp[m] << endl;
}

5)二维费用的背包问题

 飞雷神:https://www.acwing.com/problem/content/8/

顾名思义:二维费用,那么dp[i][j][k]表示j体积k重量拿完前i件物品能取得的最大价值

#include <iostream>
#include <algorithm>
#include <cmath>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7, M = 1e2 + 10;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int dp[2][M][M], v[N], worth[N], weight[N];
int main()
{
    int i, j, k, n, maxv, maxw;
    cin >> n >> maxv >> maxw;
    for (i = 1; i <= n; i++)
    {
        cin >> v[i] >> weight[i] >> worth[i];
    }
    for (i = 1; i <= n; i++)
    {
        for (j = 0; j <= maxv; j++)
        {
            for (k = 0; k <= maxw; k++)
            {
                dp[i % 2][j][k] = dp[(i + 1) % 2][j][k];
                if (j >= v[i] && k >= weight[i])
                    dp[i % 2][j][k] = max(dp[i % 2][j][k], dp[(i + 1) % 2][j - v[i]][k - weight[i]] + worth[i]);
            }
        }
    }
    cout << dp[n % 2][maxv][maxw] << endl;
    return 0;
}

核心部分依然是01背包的拿一次,也可借用01背包的优化方法,实现空间

复杂度的降维

#include <iostream>
#include <algorithm>
#include <cmath>
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7, M = 1e2 + 10;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int dp[M][M], v[N], worth[N], weight[N];
int main()
{
    int i, j, k, n, maxv, maxw;
    cin >> n >> maxv >> maxw;
    for (i = 1; i <= n; i++)
    {
        cin >> v[i] >> weight[i] >> worth[i];
    }
    for (i = 1; i <= n; i++)
    {
        for (j = maxv; j >=v[i]; j--)
        {
            for (k = maxw; k >=weight[i]; k--)
            {
                
                    dp[j][k] = max(dp[j][k], dp[j - v[i]][k - weight[i]] + worth[i]);
            }

        }
    }
    cout << dp[maxv][maxw] << endl;
    return 0;
}

6)有依赖的背包问题

传:https://www.acwing.com/problem/content/10/

此乃树形dp,望周知,哦耶

 

假设有这么个图 ,那么我们在拿完3的时候,就要考虑拿 4 ,5 的情况了。拿4 5 的时候,又要考虑拿完4,拿6,1的情况了,拿完5 ,又要考虑拿7 8的情况了。于是就递归下去。

然后对于每个节点,拿完以后,如果孩子节点已经最优了,我们再考虑孩子节点的选取,就可以知道当前节点的最优状态了。所以我们先对孩子节点dfs一遍,然后孩子节点最优了以后,再更新当前节点,在子节点都最优了时候后 来完成当前节点的最优化更新,然后又可以把当前节点作为 上层递归的节点的孩子节点,来向上更新(树形DP)。详见代码(感觉讲不清楚)

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <vector>
#include <queue>
#include <map>
//#include <bits/stdc++.h>
using namespace std;
const int N = 100 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7;
typedef long long ll;
#define sacnf scanf
#define xx first
#define yy second
#define PII pair<int, int>
#define mian main
const double PI = acos(-1);
vector<int> q[N];
int v[N], w[N], dp[N][N];
int n, m;
void dfs(int p)
{
    int i, j, k;
    int nn = q[p].size();
    for (i = 0; i < nn; i++)//不同子树
    {
        dfs(q[p][i]);//先处理好子树的情况
        for (j = m - v[p]; j >= 0; j--)//拿完第p件物品,最大还有j-v[p]空间
        {
            for (k = 0; k <= j; k++)//用k的空间来拿子树,把子树的各种情况通过体积分组
            {
                dp[p][j] = max(dp[p][j], dp[p][j - k] + dp[q[p][i]][k]);
            }
        }
    }
    //把当前拿了
    //放在后面是因为当前这个是必拿的,如果两部分换序先拿了再枚举决策,可能附属的更好,取完max把当前主件变成了不拿,这是不行的
    for (j = m; j >= v[p]; j--)
        dp[p][j] = dp[p][j - v[p]] + w[p];
    for (j = 0; j < v[p]; j++)//小于v[p]空间是主件拿不了,所以不能拿附件,价值为0
        dp[p][j] = 0;
    return;
}
int main()
{
    int i, j, k, start, p;
    cin >> n >> m;
    for (i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i] >> p;
        if (p == -1)
            start = i;
        else
            q[p].push_back(i);
    }
    dfs(start);
    cout << dp[start][m];
    return 0;
}

7)背包问题求方案 

 状态转移方程有dp[i][j]=max( dp[i][j] , dp[i-1][j - v[i]]+w[i] );

所以如果有dp[i][j]-dp[i-1][j-v[i]]==w[i],就可以说明在最优决策中我们拿了第j件物品

举个栗子:4件物品,背包体积为5 :v[i]  w[i]

                                                          1     2
                                                          2     4
                                                          3     4
                                                          4     6

用下面这一段代码是这样的:

cin >> n >> m;
for (i = 1; i <= n; i++)
    cin >> v[i] >> w[i];
for (i = 1; i <= n; i++)
{
    for (j = m; j >= 0; 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];
    }
}

for (i = 1; i <= n; i++)
{
    for (j = 0; j <= m; j++)
    {
        printf("%d ", dp[i][j]);
    }
    printf("\n");
}

 看这两个数据,是花费了2体积取得了4价值,对应,物品3

但其实我们取的是1 4这两件物品,并没有取3。

从结果看取了4号物品,所以留给前三件物品就只剩1空间,明显拿不了3了,所以在检查每层 i 取没取并非把0~m扫一遍,而是0~还剩多少空间。

然而最开始时m的空间,因为我们最后拿的第n件物品,所以先检查第n件物品拿没拿,从n→1寻找同时更新剩余背包空间

这题要求字典序,所以我们先确定前面的拿不拿,也就是要后拿前面的物品,我们拿的时候从n开始即可

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
//#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7;
typedef long long ll;
#define sacnf scanf
#define asn ans
const double PI = acos(-1);
int dp[N][N];
int v[N], w[N];
int main()
{
    int n, i, j, k, m;
    cin >> n >> m;
    for (i = 1; i <= n; i++)
        cin >> v[i] >> w[i];
    for (i = n; i; i--)
    {
        for (j = m; j >= 0; 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];
        }
    }
    k = m;
    for (i = 1; i <= n; i++)
    {
        if (k >= v[i] && dp[i + 1][k - v[i]] + w[i] == dp[i][k])
        {
            cout << i << ' ';
            k -= v[i];
        }
    }
    return 0;
}

链接:https://www.acwing.com/problem/content/11/ 

 上面那题,我们有 dp[i][j]-dp[i-1][j-v[i]]==w[i],就可以说明在最优决策中我们拿了第j件物品,

在这题这里就说明我们可以由(i-1,j-v)的状态到达(i,j) (就像是拓扑图中求最短路径个数,a点可以从b点c点到达,那么到达a点的路径个数就是 到达b点路径+到达c点路径之和)。

如果是从(i-1,j-v)的状态到达(i,j) ,用 f 数组存方案数就是

f[i][j] = f[i - 1][j - v];

如果是从(i-1,j)的状态到达(i,j) ,用 f 数组存方案数就是

  f[i][j] = f[i - 1][j];

如果两种同样优,就是

 f[i][j] = f[i - 1][j - v]+f[i - 1][j];

首先我们假设 dp[i][j] 为刚好 j 的空间拿完前 i 件能取得的最大价值,f[i][j] 为刚好 j 的空间拿完前 i 件能取得的最大价值的方案数,那么根据上面学到的,注意好初始化问题,又因为什么都不拿也是一种方案,所以 f[0][0] = 1; 在最后的时候扫一遍找一下最大值,再根据此找出所有的背包大小需要空间,然后加起来(详见代码1 代码2)

或者也可以考虑dp[i][j] 为至多 j 的空间拿完前 i 件能取得的最大价值,f[i][j] 为至多 j 的空间拿完前 i 件能取得的最大价值的方案数。这个初始化时要注意,空间为j(0~m)都是啥也不拿也是方案,所以有

 for (j = 0; j <= m; j++)
        f[j] = 1;

具体详见代码3

//代码 1
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <vector>
#include <queue>
#include <map>
//#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7;
typedef long long ll;
#define sacnf scanf
#define xx first
#define yy second
#define PII pair<int, int>
#define mian main
const double PI = acos(-1);
ll dp[N][N], f[N][N];
int main()
{
    memset(dp, 0x8f, sizeof dp);
    dp[0][0] = 0;
    f[0][0] = 1;
    ll n, m, i, j, k, v, w, maxx;
    cin >> n >> m;
    for (i = 1; i <= n; i++)
    {
        cin >> v >> w;
        for (j = 0; j <= m; j++)
        {
            if (j < v)
            {
                dp[i][j] = dp[i - 1][j];
                f[i][j] = f[i - 1][j];
            }
            else
            {
                maxx = dp[i - 1][j];
                maxx = max(maxx, dp[i - 1][j - v] + w);
                if (maxx == dp[i - 1][j])
                {
                    dp[i][j] = maxx;
                    f[i][j] += f[i - 1][j];
                    f[i][j] %= mod;
                }
                if (maxx == dp[i - 1][j - v] + w)
                {
                    dp[i][j] = maxx;
                    f[i][j] += f[i - 1][j - v];
                    f[i][j] %= mod;
                }
            }
        }
    }
    maxx = 0;
    ll anss = 0;
    for (j = 0; j <= m; j++)
        maxx = max(maxx, dp[n][j]);
    for (j = 0; j <= m; j++)
    {
        if (dp[n][j] == maxx)
        {
            anss = (anss + f[n][j]) % mod;
        }
    }
    cout << anss << endl;
    return 0;
}
//代码 2
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <vector>
#include <queue>
#include <map>
//#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7;
typedef long long ll;
#define sacnf scanf
#define xx first
#define yy second
#define PII pair<int, int>
#define mian main
const double PI = acos(-1);
ll dp[N], f[N];
int main()
{
    memset(dp, 0x8f, sizeof dp);
    dp[0] = 0;
    f[0] = 1;
    ll n, m, i, j, k, v, w, maxx;
    cin >> n >> m;
    for (i = 1; i <= n; i++)
    {
        cin >> v >> w;
        for (j = m; j >= v; j--)
        {
            maxx = dp[j];
            maxx = max(maxx, dp[j - v] + w);
            if (dp[j] == dp[j - v] + w)//拿与不拿都是最优
            {
                dp[j] = maxx;
                f[j] += f[j - v];//加上拿的个数
                f[j] %= mod;
            }
            else
            {
                if (maxx > dp[j])//拿了更优
                {
                    dp[j] = maxx;
                    f[j] = f[j - v];//个数更新
                    f[j] %= mod;
                }
            }
            //else 不拿更优就不用变了
        }
    }
    maxx = 0;
    ll anss = 0;
    for (j = 0; j <= m; j++)
        maxx = max(maxx, dp[j]);
    for (j = 0; j <= m; j++)
    {
        if (dp[j] == maxx)
        {
            anss = (anss + f[j]) % mod;
        }
    }
    cout << anss << endl;
    return 0;
}
//代码 3
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <vector>
#include <queue>
#include <map>
//#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7;
typedef long long ll;
#define sacnf scanf
#define xx first
#define yy second
#define PII pair<int, int>
#define mian main
const double PI = acos(-1);
ll dp[N], f[N];
int main()
{

    ll n, m, i, j, k, v, w, maxx;
    cin >> n >> m;
    for (j = 0; j <= m; j++)
        f[j] = 1;
    for (i = 1; i <= n; i++)
    {
        cin >> v >> w;
        for (j = m; j >= v; j--)
        {
            maxx = dp[j];
            maxx = max(maxx, dp[j - v] + w);
            if (dp[j] == dp[j - v] + w) //拿与不拿都是最优
            {
                dp[j] = maxx;
                f[j] += f[j - v]; //加上拿的个数
                f[j] %= mod;
            }
            else
            {
                if (maxx > dp[j]) //拿了更优
                {
                    dp[j] = maxx;
                    f[j] = f[j - v]; //个数更新
                    f[j] %= mod;
                }
            }
            // else 不拿更优就不用变了
        }
    }
    cout << f[m] << endl;
    return 0;
}

8)完结撒花

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值