背包

一般代码只是例子,具体使用依据题目来, DP是一种思想,代码都以属性为最大值等等为例子

01背包

基本的背包
简单说就是有n个物品和容量为m的包,求其max/min/方案数等等即属性
一般转移方程为f[i][j]意思为在前i个里容量为j的情况下的要求的属性
(可忽略)一般这里的转移是在f[i][j],第i个数取与不取
时间复杂度O(n*m)

一般代码

for (int i = 1; i <= n; i ++ ) // 枚举当前几个物品
    {
        int v, w;
        cin >> v >> w;
        for (int j = 1; j <= m; j ++ )
        {
            f[i][j] = f[i - 1][j];
            if (j >= v) f[i][j] = max(f[i][j], f[i - 1][j - v] + w);
        }
    }
一维优化
for (int i = 1; i <= n; i ++ ) // 枚举当前几个物品
    {
        int v, w;
        cin >> v >> w;
        for (int j = m; j >= v; j -- ) // 体积
            f[j] = max(f[j], f[j - v] + w);
    }


必须倒序枚举体积,我们的f[j - v]用的实际是f[i - 1][j - v], 而正序枚举我们可能会先更新了
f[j - v]使得后面用到f[j - v]时实际用的是f[i][j - v]从而错误, 只要倒序就可以解决此问题

完全背包

即有n种物品,每种无限个
一般的转移方程为 f[i][j] = max(f[i - 1][j], f[i][j - v]);

优化代码

O(n*m)

for (int i = 1; i <= n; i ++ )
{
	int w, v;
	cin >> v >> w;
	for (int j = 0; j <= m; j ++ )
	{
		f[i][j] = f[i - 1][j];   
		if (j >= v) f[i][j] = max(f[i][j], f[i][j - v] + w);
	}
}
朴素版

O(n*m*k)

for (int i = 1; i <= n; i ++ ) 
    {
        int v, w;
        cin >> v >> w;
        for (int j = 1; j <= m; j ++ )
            for (int k = 0; k <= j / v; k ++ )
                f[i][j] = max(f[i][j], f[i - 1][j - v * k] + w * k);
    }
方程推导

正常的思路:
f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w, f[i - 1][j - 2v] + 2w,...,f[i][j - kv] + kw)
k∈(0, j/v);
这种复杂度过高,一般接受不了

一般的方程f[i][j] = max(f[i - 1][j], f[i][j - v]);
f[i][j - v]如何得来? (注意max中的对应)
f[i][j] = max(f[i - 1][j], f[i - 1][j - v], f[i - 1][j - 2v],...,f[i][j - kv])
########f[i][j - v] = max(f[i - 1][j - v], f[i - 1][j - 2v],...,f[i][j - kv])
k是相同
可得f[i][j] = max(f[i - 1][j], f[i][j - v]);
加上价值就是f[i][j] = max(f[i - 1][j], f[i][j - v] + w);

一维优化
for (int i = 1; i <= n; i ++ ) 
{
	int v, w;
	cin >> v >> w;
	for (int j = v; j <= m; j ++ )
		f[j] = max(f[j], f[j - v] + w);
}

这里只能正序枚举,和01背包相反,因为我们转移的实际上是f[i][j - v],正序可以提前更新f[j - v]使他实际上成为f[i][j - v]符合要求倒序则为f[i - 1][j - v]不合要求

转移的注意

对于完全背包,一定要转移完全,即在从k = 0的时候也要转移进去,不能终断,否则会出现错误
P1941 [NOIP2014 提高组] 飞扬的小鸟 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)如果直接做即完全背包的上升和01背包的下降同时转移,那么祝贺你,会得到70分的代码.

多重背包

有n种物品但是每种物品有数量限制

朴素代码
for (int i = 1; i <= n; i ++ )
{
	int w, v, s;
	cin >> v >> w >> s;
	for (int j = 0; j <= m; j ++ )
		for (int k = 0; k <= min(s, j / v); k ++ )
			f[i][j] = max(f[i][j], f[i - 1][j - k * v] + k * w);
}
二进制优化

把物品的个数S拆成一堆数,形成一个单独的物品,最后采用01背包去做
一般采用二进制的方法
因为一堆二进制数加起来可以加成任何数
如0001,0010,0100,1000这四个二进制数相加,可以组成任何小于等于1111的数(正数)

for (int i = 1; i <= n; i ++ ) 
{
	int v, w, s, k = 1;
	cin >> v >> w >> s;
	while (k <= s)
	{
		V[ ++ cnt] = v * k;
		W[cnt] = w * k;
		s -= k; // 这样拆是logn个比较高效
		k <<= 1; 
	}
	if (s > 0) // 我们减到最后可能会有剩下的,也加上
	{
		V[ ++ cnt] = v * s;
		W[cnt] = w * s;
	}
}

for (int i = 1; i <= cnt; i ++ ) 
{
	int v = V[i], w = W[i];
	for (int j = m; j >= v; j -- )
        f[j] = max(f[j], f[j - v] + w);
}

一般够用

单调队列优化

口诀:比你小还比你强,你就出列了~~~
时间复杂度为O(nm)
因为一般m都比较大所以常用[[DP的通用优化#滚动数组]]

代码
for (int i = 1; i <= n; i ++ )
{
	int w, s, v;
	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] = max(f[k], g[q[hh]] + (k - q[hh]) / v * w);
		}
	}
}
思路

首先你要知道在s个物品限制下转移方程为(一般情况s >= m / v)
这里推荐看我以前的笔记

	f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w, f[i - 1][j - 2v] + 2w, ... , f[i - 1][j - sv] + sw);
    f[i][j - v] =          max(f[i - 1][j - v],     f[i - 1][j - 2v] + 1w, ... , f[i - 1][j - sv] + (s - 1)w, f[i - 1][j - (s + 1)v] + sw);
    f[i][j - 2v] = ...
    f[i][j - 3v] = ...
    ...
    r = j % v;
    f[i][r + v] = max(f[i - 1][r + v], f[i - 1][r] + w)
    f[i][r] = f[i - 1][r]
    
    经典的图啊
    根据完全背包优化的思路我们可以得到上面的图(也可以看我过去笔记的图片)
    我们能发现一个事情,因为有s的限制,在一般情况下,我们没法直接用f[i][j - v]来转移
    but我们可以从它的余数出发, f[i][r], 这个是值固定的,往上f[i][r + v]看图
    越往上我们会发现,在他们的max内的第一个都在增加v(r + v), 且当s > j / v时它就开始滑动(像f[i][j], f[i][j - v], max的数量一样,但是f[i][j]
    向前进了1个v, 且w的值都加了1w, 除了新进来的)
    这就想到了单调队列,可以靠它维护这个区间最大值,优化的点,保持队列队头最大,利用这个最大,优化计算量;并且可以计算滑动窗口内的最大值
    看看代码,多想想
    
    因为过于复杂还是看y的课比较好。

以前的笔记
如果你还有记忆的话应该会懂得
一般比二进制优化快

二维费用背包

很简单就是有两个费用,多开一维,记录费用即可非常简单
状态一般为 f[i][j][k]在前i个数内,在两个费用都不超过j和k的情况下的属性

可以和上面三种背包结合,相当于前缀

一般代码(二维费用01背包)
for (int i = 1; i <= n; i ++ )
{
	int w, v, t;
	cin >> v >> t >> w;
	for (int j = m; j >= v; j -- )
		for (int k = p; k >= t; k -- )
		{
			f[j][k] = max(f[j][k], f[j - v][k - t] + w);
		}
}

不少于问题

之前没整理到这个
简单说就是对于费用不少于的问题,这也可以和上面三种背包连用,相当于前缀

对于这个直接DP分析即可,关于不少于,直接设f[i][j]为在前i个里面,费用不少于j的情况
那么第i个可选可不选,不选就是f[i - 1][j],选的话是f[i - 1][j - v] 这时候可能会有j < v的情况,这时候也要转移,因为它符合,费用不少于j(大于了当然可以),写成这个就行f[i - 1][max(0, j - v)]。从相当于从0直接到j。

这时候回头想想,好像之前的当j < v的时候就不转移了(可看看上面代码),这是因为之前状态是费用最大为j,这就是本质的差距。

恰好问题

这是一类小问题,状态设计要改变为,恰好,转移没什么变化,但初始化有变动,一定要先设置为不可能情况,因为对于一种状态
像有许多石头,价值和重量无关,求恰好重量为j时的最大价值,石头有(前面是价值,后面是重量){1, 1},{2, 1},{2, 2}

当f[2][3]转移的时候,f[1][2]这种情况是不可能的,所以f[2][3] = max(f[1][2] + 2)
整个转移是转移不了的,但是价值不大于j时是可以的。这就是它的本质

求方案数

这类其实有点技巧,最稳妥的方法是,把题目改为恰好,这里用一个例题来说明。
求最优方案数
AcWing 11. 背包问题求方案数 - AcWing
题中要求不超过j方案最大价值的方案数,
有两种大方向

  1. 利用不超过直接求解,因此f初始化都为0,对于每种情况f[i][j]最少有1种方案,然后转移时如果可以更新就直接更替cnt,相等就加上cnt,最后直接输出cnt[n][m]即可
  2. 利用恰好,把不超过j,改为恰好为j,转移和上面一样,然后在f[n][j]中记录,最大值,并把所有的最大值相等的方案加上。
    如上代码在上面链接中
    因此对于一个问题,除了特殊记录以外,就可以直接转移或利用恰好凑出方案

而转移时如果可以更新就直接更替cnt,相等就加上cnt,这是所有方案问题的最基本的思想,不限于动态规划

关于方案的保存

问题在保存选的方案,像这个AcWing 1013. 机器分配 - AcWing比较典型,除了求解答案,还要输出每个工厂选择的机器数,可以dfs,可以像我这样原路推回去(实际上更多使用这种),
没有固定的方式,这里只说明有此类问题。

枚举的状态

正常背包的转移为f[i][j] = max(f[i][j], f[i - 1][j - v] + w)
我们依赖于第i - 1个物品进行转移,实际上还可以依赖体积,来转移。
思路为
{不选}
{选择体积最大为k的子集,并加上这个点} k <= j

对于正常的背包,这是没有用的,但对于树形等物件间有特殊关系的,有奇效.
AcWing 10. 有依赖的背包问题 - AcWing这题就需要对于每个子树分配体积,来计算
否则复杂度将爆炸

有依赖的问题

分为很多种,例如可能依赖是树形的,可能是线性的,等等。
中心思想就是转移好子集,不能言传。
AcWing 10. 有依赖的背包问题 - AcWing
487. 金明的预算方案 - AcWing题库
上面是较好的一道,也是较难的一道。
下面的较为简单适合入入门。

求具体方案

这种会问你最后选择的方案是是什么,而不是方案数,可能有限制,如选择字典序最小的最优选择方案是什么。这种有两种思路。

  1. 直接保存方案,利用string,或者vector,在转移过程中保存方案,通过比较方案来选择最小字典序,这种一般较慢,但很稳定。
  2. 先求出最优方案,再利用最优方案,从大枚举,或从小枚举,推出选择方案。这种较快,但边界和枚举设计需要考虑清楚。
    AcWing 12. 背包问题求具体方案 - AcWing
    具体代码如上

关于状态转移的奇技淫巧

  1. 有时候我们不能直接转移状态,而要合成状态,像f[i + 1][j + v] = ...这时候可能会有“卡壳”情况,即我们无法直接判断情况,这时候不妨从0出发把for (int i = 1; i <= n; i ++ )变为for (int i = 0; i < n; i ++ )这样“退位”,在某些时候有奇效如这题P1156 垃圾陷阱而这就是刷表法,[[刷表法和填表法]]

变化的价值/体积(泛化物品的背包)

对于一类题,它的价值和体积会随着你的选择顺序不同而改变,这时候需要考虑优先级,
常用方法

先设x,y为两个相邻物品,然后列出(如价值)的不等式,像如此价值的情况
先x后y w = (p + c[x]) * b[x] + (p + c[x] + c[y]) * b[y] (1)
先y后x w = (p + c[y]) * b[y] + (p + c[y] + c[x]) * b[x] (2)
我们如果要让x在前的话,那么(1) > (2)
可以解得 c[x] * b[y] > b[x] * c[y],然后我们利用这个性质排序,再进行背包处理

做了那么多题,对于背包,正确的物品排序确实很重要。

给出对应的例题试试吧P1417 烹调方案 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

在最值情况下的最值

这类题题意如求在最大价值1的情况下,最小的价值2为多少,即在最大价值1下的最小价值2,
这种算是比较好做,只要在转移出最大价值1的同时,记录最小价值2,更新时替换,相等时对比求最小。注意,前提是最大价值1,所以要对它Dp

例题P1509 找啊找啊找GF - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

posted @ 2024-04-26 11:53  blind5883  阅读(10)  评论(0编辑  收藏  举报