最近学习了动态规划,网课上大佬们讲得我怎么都听不太懂???一听不懂就想摸鱼了...所以干脆不听课了,在这里整理一下经典DP题目。
题目:P1048 [NOIP2005 普及组] 采药 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
01背包问题中,每个物品只有一件,换句话说,每件物品只有取(1)和不取(0)两种状态。
原理:
——引自《算法竞赛——从入门到进阶》
表格中第一维度i:待取背包的种类,第二维度j:待取背包的容量
我们每一次固定i,对j进行遍历。好比说我们对于一个物品i,路过每一个容量为j的背包时,我们既要问一下这个容量为j的背包:你要不要装一下物品i?
如果在背包容量为j时,,那么此时肯定装不下物品i,那么在背包容量为j时,我们就不用考虑背包i了,考虑背包容量为j装不装物品i-1与此时情况相同,。
如果在背包容量为j时,,那么此时可以装的下物品i,我们就要考虑:装还是不装?
我们要取装和不装这两种情况的最大值,
上面的描述过程就是状态转移方程了。
优点:可以很好的记录选择的路径并打印出来
缺点:空间复杂度大。因为要开二维数组,当背包种类和容量较大时,需要开很大的数组。
#include
using namespace std;
long long t[1005], v[105], f[105][1005], pre[105];//f代表在草药种类<=i且时间<=j时可以得到的最大价值
int main(void) //pre数组记录第i种药草选择了没有
{
int T, M;
cin >> T >> M;//T采药时间,M药草种类
for (int i = 1; i <= M; i++)
{
cin >> t[i] >> v[i];
}
for (int i = 1; i <= M; i++)//枚举可供选择的药草种类
{
for (int j = 1; j <= T; j++)//枚举时间
{
if (j < t[i])//此时的时间装不下第i中药草,不选择
f[i][j] = f[i - 1][j];
else //此时的时间装的下第i中药草
{
f[i][j] = max(f[i - 1][j - t[i]] + v[i], f[i - 1][j]);//既然装的下那么就有装和不装两种情况,取两种情况的最大值
}
}
}
cout << "采集药草的最大价值:" << f[M][T] << endl;
cout << "采集药草的种类:";
int i = M, j = T; int num = M;
while (num--)//从后往前查找选择的背包
{
if (f[i][j] == f[i - 1][j - t[i]] + v[i])
{
pre[i] = 1;
j = j - t[i];
i = i - 1;
}
else
{
i = i - 1;
}
}
for (int i = 1; i <= M; i++)
{
if (pre[i] == 1)
{
cout << i << ' ';
}
}
return 0;
}
原理:滚动数组
滚动数组要直接从上面的二维数组的数学公式上理解有些困难。这里我直接举个例子用一维数组一层层展开解释。但不是数学上的严格证明。
假设背包容量是C=10
物品编号:1 2 3
物品重量:5 6 4
物品价值:20 10 12
f[] = 0 0 0 0 0 0 0 0 0 0
i=1:
f[10] = max(f[5]+20, f[10]);
f[9] = max(f[4]+20, f[9]);
f[8] = max(f[3]+20, f[8]);
f[7] = max(f[2]+20, f[7]);
f[6] = max(f[1]+20, f[6]);
f[5] = max(f[0]+20, f[5]);
f[] = 0 0 0 0 20 20 20 20 20 20
i=2:
f[10] = max(f[4]+10, f[10]);
f[9] = max(f[3]+10, f[9]);
f[8] = max(f[2]+10, f[8]);
f[7] = max(f[1]+10, f[7]);
f[6] = max(f[0]+10, f[6]);
f[] = 0 0 0 0 20 20 20 20 20 20
i=3:
f[10] = max(f[6]+12, f[10]);
f[9] = max(f[5]+12, f[9]);
f[8] = max(f[4]+12, f[8]);
f[7] = max(f[3]+12, f[7]);
f[6] = max(f[2]+12, f[6]);
f[5] = max(f[1]+12, f[5]);
f[4] = max(f[0]+12, f[4]);
f[] = 0 0 0 12 20 20 20 20 32 32
每一次遍历背包容量时需要从后往前遍历,这样每个背包信息只被用到一次(即只被拿一次),从前往后遍历时,背包信息会被用到多次(这就是后面要讲的完全背包问题)。
j>=w[i]时停止即可,因为当j 优点:空间复杂度小 缺点:不能很好的记录选择的路径,中间状态有缺失。 题目: P1616 疯狂的采药 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 与01背包问题不同之处,完全背包问题中待取物品每个都有无限件。所以状态转移方程也不一样。 上面解释滚动数组原理时提到过,如果j从前往后遍历每个背包将会被计算多次,利用这一点可以解决完全背包问题。 假设背包容量是C=10 物品编号:1 2 3 物品重量:5 6 4 物品价值:20 10 12 由此可见,01背包与完全背包问题都可以用一维数组解决,不同的是: 01背包问题要从后往前枚举背包容量 完全背包问题要从前往后枚举背包容量 完全背包问题用二维数组解决需要用到三重循环 采用01背包打印方案的方法也可以打印方案 题目:HDU 2191 与完全背包不同之处在于,每个物品的数量不是无限,而是有限个。 我们可以将多重背包问题转化为01背包问题。例如:一个物品的重量是2,价值是3,一共有5个。我们可以把它看成5个重量是2,价值是3的“不一样”的物品。 题目:9. 分组背包问题 - AcWing题库 思路:将每个组视为01背包中的物品种类,最内层对每个组内的物品进行遍历。 补充一道分组背包的好题: P5322 [BJOI2019]排兵布阵 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 思路:将第i个城堡看做物品i,城堡i中有s个玩家布置的兵力,即物品i中有s个不同的物品样式。 背包容量是m求最大值。洛谷上的题解说的也挺明白的。 目录 一. 01背包问题 二维数组解决方法: 一维数组解决方法: 二.完全背包问题 三.多重背包问题 四.分组背包问题 五.超大背包问题 优秀博文:(89条消息) hdu 5887 Herbs Gathering_HopeForBetter的博客-CSDN博客//滚动数组处理01背包问题
#include
二.完全背包问题
f[] = 0 0 0 0 0 0 0 0 0 0
i=1:
f[5] = max(f[0]+20, f[5]);
f[6] = max(f[1]+20, f[6]);
f[7] = max(f[2]+20, f[7]);
f[8] = max(f[3]+20, f[8]);
f[9] = max(f[4]+20, f[9]);
f[10] = max(f[5]+20, f[10]);
f: 0 0 0 0 20 20 20 20 20 40 //在已经拿过背包1后,f[10]又由f[5]决定了,f[5] = f[0] + 20,
f[5]也已经拿过背包1了!f[10]这样计算拿过2次背包1!!!
i=2:
f[6] = max(f[0]+10, f[6]);
f[7] = max(f[1]+10, f[7]);
f[8] = max(f[2]+10, f[8]);
f[9] = max(f[3]+10, f[9]);
f[10] = max(f[4]+10, f[10]);
f[] = 0 0 0 0 20 20 20 20 20 40
i=3:
f[4] = max(f[0]+12, f[4]);
f[5] = max(f[1]+12, f[5]);
f[6] = max(f[2]+12, f[6]);
f[7] = max(f[3]+12, f[7]);
f[8] = max(f[4]+12, f[8]);
f[9] = max(f[5]+12, f[9]);
f[10] = max(f[6]+12, f[10]);
f: 0 0 0 12 20 20 20 24 32 40
//完全背包问题
#include
for (int i = 1; i <= M; i++)//枚举可供选择的药草种类
{
for (int j = t[i]; j <= T; j++)//枚举时间
{
for (int z = 1; z * t[i] <= j; z++)//枚举药草i采取的数量
{
f[i][j] = max(f[i - 1][j - z * t[i]] + z * v[i], f[i - 1][j]);
}
}
}
三.多重背包问题
#include
四.分组背包问题
//分组背包
#include
#include
五.超大背包问题
#define _CRT_SECURE_NO_WARNINGS
#include