最近学习了动态规划,网课上大佬们讲得我怎么都听不太懂???一听不懂就想摸鱼了...所以干脆不听课了,在这里整理一下经典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
优点:空间复杂度小
缺点:不能很好的记录选择的路径,中间状态有缺失。
//滚动数组处理01背包问题
#include
using namespace std;
long long t[1005], v[105], f[105];//f代表在时间<=i时可以得到的最大价值
int main(void)
{
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 = T; j >= t[i]; j--)//从后往前枚举时间
{
f[j] = max(f[j], f[j - t[i]] + v[i]);
}
}
cout <<"采集药草的最大价值:"<< f[T] << endl;
return 0;
}
题目: P1616 疯狂的采药 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
与01背包问题不同之处,完全背包问题中待取物品每个都有无限件。所以状态转移方程也不一样。
上面解释滚动数组原理时提到过,如果j从前往后遍历每个背包将会被计算多次,利用这一点可以解决完全背包问题。
假设背包容量是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[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
using namespace std;
long long t[10000005], v[10005], f[10005];//f代表在时间<=i时可以得到的最大价值
int main(void)
{
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 = t[i]; j <= T; j++)//从前往后枚举时间
{
f[j] = max(f[j - t[i]] + v[i], f[j]);
}
}
cout << f[T] << endl;;
return 0;
}
由此可见,01背包与完全背包问题都可以用一维数组解决,不同的是:
01背包问题要从后往前枚举背包容量
完全背包问题要从前往后枚举背包容量
完全背包问题用二维数组解决需要用到三重循环
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]);
}
}
}
采用01背包打印方案的方法也可以打印方案
题目:HDU 2191
与完全背包不同之处在于,每个物品的数量不是无限,而是有限个。
我们可以将多重背包问题转化为01背包问题。例如:一个物品的重量是2,价值是3,一共有5个。我们可以把它看成5个重量是2,价值是3的“不一样”的物品。
#include
using namespace std;
int P[2005], H[2005], f[105];//p价格,h重量
int main(void)
{
int C;
cin >> C;
while (C--)
{
int n,m;
cin >> n >> m;//n经费金额m大米种类
int cnt = 1;
for (int i = 1; i <= m; i++)
{
int p, h, c;
cin >> p >> h >> c;
while (c--)//转化成01背包问题
{
P[cnt] = p;
H[cnt] = h;
cnt++;
}
}
for (int i = 1; i < cnt; i++)
{
for (int j = n; j >= P[i]; j--)
{
f[j] = max(f[j], f[j - P[i]] + H[i]);
}
}
cout << f[n] << endl;
}
return 0;
}
题目:9. 分组背包问题 - AcWing题库
思路:将每个组视为01背包中的物品种类,最内层对每个组内的物品进行遍历。
//分组背包
#include
using namespace std;
int num[105],w[105][105], v[105][105],f[105];//num[i]:第i组有多少物品, w[i][j]第i组第j个物品的价值,f[i]背包容量<=i时能装的最大价值
int main(void)
{
int N, V;
cin >> N >> V;//N物品组数V背包容量
for (int i = 1; i <= N; i++)
{
cin >> num[i];
for (int j = 1; j <= num[i]; j++)
{
cin >> v[i][j] >> w[i][j];
}
}
for (int i = 1; i <= N; i++)//枚举组数
{
for (int j = V; j >= 1; j--)//从大到小枚举容量
{
for (int z = 1; z <= num[i]; z++)//枚举每一组内的物品
{
if(j >= v[i][z])
f[j] = max(f[j], f[j - v[i][z]] + w[i][z]);
}
}
}
cout << f[V];
return 0;
}
补充一道分组背包的好题:
P5322 [BJOI2019]排兵布阵 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路:将第i个城堡看做物品i,城堡i中有s个玩家布置的兵力,即物品i中有s个不同的物品样式。
背包容量是m求最大值。洛谷上的题解说的也挺明白的。
#include
using namespace std;
int solder[105][105],a[105][105],f[20005];//a数组表示第i个城堡中玩家1-j士兵的情况
int main(void)
{
int s, n, m;//分别表示玩家人数、城堡数和每名玩家拥有的士兵数。
cin >> s >> n >> m;
for (int i = 1; i <= s; i++)
{
for (int j = 1; j <= n; j++)
{
cin >> solder[i][j];
}
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= s; j++)
{
a[i][j] = solder[j][i];
}
}
for (int i = 1; i <= n; i++)
{
sort(a[i] + 1, a[i] + 1 + s);//每个城堡的兵力人数排序
}
for (int i = 1; i <= n; i++)
{
for (int j = m; j >= 0; j--)
{
for (int z = 1; z <= s; z++)
{
if (j > a[i][z] * 2)
{
f[j] = max(f[j], f[j - 2 * a[i][z] - 1] + i * z);
}
}
}
}
cout << f[m];
return 0;
}
目录
一. 01背包问题
二维数组解决方法:
一维数组解决方法:
二.完全背包问题
三.多重背包问题
四.分组背包问题
五.超大背包问题
优秀博文:(89条消息) hdu 5887 Herbs Gathering_HopeForBetter的博客-CSDN博客
#define _CRT_SECURE_NO_WARNINGS
#include
using namespace std;
int N, ind;
long long V, ans;
struct Node {
long long weight, value;
double per;
};
Node thing[100];
bool cmp(const Node p1, const Node p2) { return p1.per > p2.per; }//按背包单位价值从高到低排序
bool check(int i, long long now_v, long long now_w)//计算从背包i算起,理论的最大价值(剩下的物品已经按单位价值提前排序,所以只要背包容量允许“见一个拿一个”,补上最后的背包剩余重量,所得价值一定是理论背包最大价值)
{
for (int j = i; j < ind; j++)
{
if (thing[j].weight + now_w <= V)
{
now_v += thing[j].value;
now_w += thing[j].weight;
}
else
{
now_v += int((V - now_w) * thing[j].per);
}
}
return now_v > ans;
}
void dfs(int i, long long now_v,long long now_w)//i:当前考虑到第i个物品,now_v:前i个物品价值,now_w:前i个物品重量
{
ans = max(ans, now_v);//ans记录最大价值
if (i >= ind || !check(i, now_v, now_w))//搜索的背包已经到头或者从这个背包开始理论最大价值已经小于ans,则退出
return;
if (now_w + thing[i].weight <= V)
dfs(i + 1, now_v + thing[i].value, now_w + thing[i].weight);//选第i个物品
dfs(i + 1, now_v, now_w);//不选第i个物品
return;
}
int main(void)
{
scanf("%d", &N);
scanf("%lld", &V);
for (int i = 1; i <= N; i++)
{
long long a, b;
scanf("%lld%lld", &a, &b);//体积、价值
if (a <= V)
{
thing[ind].weight = a;
thing[ind].value = b;
thing[ind].per = b * 1.0 / a;
ind++;
}
}
sort(thing, thing + ind, cmp);
dfs(0, 0, 0);
printf("%lld", ans);
return 0;
}