2022CUST程序设计天梯赛校赛 L2-2power
算法思路
完全背包问题而来动态规划之完全背包问题
但是与之不同是,有限制条件**“每次玩过的项目不能比上一次玩过的项目便宜”**
这里先讲一下我从别的大佬学习到的思路
1. 可选择性 <! --这是我通过这道题对dp的更一层次理解–>
-
我们普通的**
dp[i][j]
表示的是:从前i-1个物品中选取总重不超过W的物品的最大价值**这里我们需要着重注意前i-1个物品,我们普通的背包是在前i-1个物品都可选的情况下的最优解
-
这里的背包显然前i-1个物品不一定都可选,只能选择前i-1个物品中物品价格不下降的k个物品,
而且要看不下降的价格序列中最大的那个判断当前物品是否可选
2.如何实现不下降序列
仔细想想就知道,其实dp会自然实现不下降序列,因为我们每次判断每个最优解的时候,都是基于”价格不下降序列“的前提下完成的,每次只是在前一个“价格不下降序列”的基础上增加或不变”价格不下降序列"的长度。
只不过有个地方需要知道,能否选择当前物品(i),取决于你所选的”价格不下降序列”最后的那个价格
若v[i]>=v[k]那么我就可以选择当前物品(i),换句话说就是可以选择在k前面的“价格不下降序列”中添加物品i的价格,否则没有选择的余地
3.来源问题<!-来源问题是本人自己对dp的理解,在我的动态规划笔记中有体现,就是从哪来的意思->
- 先看看完全背包的来源<! --里面的样例不是本题的,我只是为了解释一下完全背包的来源–>
d p [ i + 1 ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i + 1 ] [ j − v [ i ] ] + w [ i ] ) dp[i+1][j]=max(dp[i][j],dp[i+1][j-v[i]]+w[i]) dp[i+1][j]=max(dp[i][j],dp[i+1][j−v[i]]+w[i])
-
在来看看这里的背包来源
假设实现了v[i]>=v[k1和k2]
于普通完全背包不同的是,它是跨区间跳跃来的
我们还需注意k不止一个,满足v[i]>=v[k]的k都要考虑进去,n的数据量比较小,就只需枚举所有的v[i]>=v[k]的k就行了
这也导致于普通完全背包不同的是,普通完全背包只需比较一次(不用考虑本身这个点,不用将它加入比较),而这里要比较多次, 所以每次都可能更新本身这个点,要把本身这个点加入比较
因此我们可以枚举这些满足条件的k,把它们都用一次完全背包的状态转移方程来更新
代码
这里先写下代码,然后在解释一下上面没提到的点以及优化
#include <iostream>
using namespace std;
int n, m;
const int maxn = 55, maxm = 3005;
int dp[maxn][maxm];
int v[maxn], w[maxm];
void DP()
{
for (int i = 1; i <= n; i++)
{
for (int k = 0; k < i; k++)//注释1
{
if (v[i] >= v[k])
{
for (int j = 0; j <= m; j++)
{
if (j < v[i])
{
dp[i][j] = max(dp[i][j], dp[k][j]);
}
else
dp[i][j] = max(dp[i][j], max(dp[i][j - v[i]] + w[i], dp[k][j]));
}
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++)//注释2
{
ans = max(ans, dp[i][m]);
}
cout << ans;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
DP();
}
注释1:为什么k从0开始,到i-1结束?<!-这个点我理解别人想法是的不同之处,大佬的写法是[1,i]->
首先要知道我们在这里的dp[i][j],v[i],w[i]
的i都是从1开始的,所以没有第0个物品<! --不要和我笔记里i从0开始混淆–>
我们要知道,i=1时是从哪里来更新dp数组的?
是从前i-1=0个物品来更新的,前0个没有物品(没有第0个物品),所以自然dp[0][0~m]=0
,建立全局数组时主动初始化了<! --有些问题得我们自己初始化,要重视此初始化–>
所以我们在更新dp[i][j]
时,找到的v[i]>=v[k]中的k必须包含0(即包含前0个物品这个情况,因为这也算一个价格不下降序列嘛)
再者从前i-1中选价格不下降序列,k自然到i-1结束
那k从1开始,到i结束行不行?
答案是行,但是“碰巧行”,因为当k=i时,第k行在最开始本身就是0行,只不过把上面对0行的操作提到后面来了。这真的算是考虑不周到,因为想想在其他题目中遇到初始化时dp[0][0~m]!=0
呢,那这个就是错误的了所以,认识初始化的重要性
注释2:为什么要取dp[i][m]
的最大值?
想想我们在上面的考虑过程中漏掉了什么
你想到了吗?肯定没有😁
我们漏掉了第i个物品不可选的情况
因为我们条件v[i]>=v[k]只是为了保证第i个物品我们可以选择
那其它的k呢,也同样是表示价格不下降序列,这是的v[i]<[k]使得我们没有选择第i种物品的余地,但是可能直接继承它会是最优解, 这里是我想的优化之一但是我们没有选择继承,所有最优解不一定出现在dp[n][m]
,虽然可以但总有点不符合我们对DP的常规理解(dp[n][m]
是最优解)
优化1
算法思路
加上第i个物品不可选的情况的判断
代码
#include <iostream>
using namespace std;
int n, m;
const int maxn = 55, maxm = 3005;
int dp[maxn][maxm];
int v[maxn], w[maxm];
void DP()
{
for (int i = 1; i <= n; i++)
{
for (int k = 0; k < i; k++)
{
if (v[i] >= v[k])
{
for (int j = 0; j <= m; j++)
{
if (j < v[i])
{
dp[i][j] = max(dp[i][j], dp[k][j]);
}
else
dp[i][j] = max(dp[i][j], max(dp[i][j - v[i]] + w[i], dp[k][j]));
}
}
else dp[i][m]=max(dp[i][m],dp[k][m]);//优化处
}
}
cout<<dp[n][m];
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
DP();
}
注意
优化错误示范:else for(int j=0;j<=m;j++)dp[i][j]=max(dp[i][j],dp[k][j]);
答案会错一个点
也就是说上面所说的但是可能直接继承它会是最优解是指只考虑是否继承j=m时的情形
想想为什么?
因为全部去考虑继承会出现破坏条件(所选价格不能比上一次小的)情况
如样例:
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 6 | 6 | 6 |
2 | 0 | 0 | 5 | 5如果全部继承这里会是6 | 10 | 10 |
当我们后面需要用[2,3]来更新时,就会出现错误的更新
那为啥j=m时没事呢?
其实那也会破坏条件,但是我们就是要这个破坏来使d[i][m]
始终是前i-1个物品所选的最优解
而且此破坏不会影响其他解,因为利用它更新其他答案时只会更它的正下方,不会更新右方,那你对正下方乱更新乱破坏就破坏咯,正合我意,我就要最大值
优化2
算法思路
减少重复比较
看看,对于每个k,我们都把它和爱你处比较,但是其实只用最后比较一次即可,先把前面的满足v[i]>=v[k]时的最大的dp[k][j]
找出来然后最后和dp[i][j-v[i]]+w[i]
比较
与前面代码不同是
我们要改变一下遍历顺序,先遍历j在遍历k,因为我们要找每个dp[i][j]
正上方的满足v[i]>=v[k]时的最大的dp[k][j]
,如果先遍历k在遍历j就无法实现了
代码
#include <iostream>
using namespace std;
int n, m;
const int maxn = 55, maxm = 3005;
int dp[maxn][maxm];
int v[maxn], w[maxm];
void DP()
{
for (int i = 1; i <= n; i++)
{
for (int j = 0; j <= m; j++)//先遍历j
{
for (int k = 0; k < i; k++)//在遍历k
{
if (k == i - 1 && j >= v[i] && dp[i][j - v[i]] + w[i] > dp[i][j])
dp[i][j] = dp[i][j - v[i]] + w[i];//k查找到最后,若可以用dp[i][j-v[i]]更新dp[i][j]则更新
if (v[i] >= v[k]) dp[i][j] = max(dp[i][j], dp[k][j]);//找最大的dp[k][j]
else dp[i][m] = max(dp[i][m], dp[k][m]);//此处是上面的优化1
}
}
}
cout << dp[n][m] << endl;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
DP();
}
感触
这道题花了我很久的时间,最开始用了个错误的思路,过了21个点一直在那bug,最后还是借鉴了一下HMY大佬的想法(就是我上面总结的可选择性),看到那个我就有想法了,后面还是有些思路不同滴,虽然AC了但感觉没完全理解,就写下了题解,在不断思考的过程中想到了一点点优化,虽然看起来没用复杂度没什么变化,但是对dp的理解有那么一些深刻了,希望会在以后某个时刻其作用。
拜托,算法真的好美的好不好😍😍🥰🥰(✿◡‿◡)