当前位置: 首页 > news >正文

【题解】P1156 垃圾陷阱

题目

洛谷:P1156 垃圾陷阱
在这里插入图片描述

分析

关于本题,状态有三个影响因素:物品,时间(血量),高度。
物品的使用顺序并不是随意的,必须按它们下落的时间顺序来先后处理。所以时间就确定了,将物品排序后依次使用即可。现在就只剩下血量,高度。选哪个作为 j 呢?
只能挨着尝试,看哪个更好些,动态规划有的时候选错了状态定义是做不出来题目的,但是本题两种都可以。这里就选择枚举高度作为 j 来解题。

于是有:f[i][j]表示处理完第 i 个垃圾之后,高度为 j 时的最大血量。

重点

这道题与常规的背包问题解法是不同的

举个例子:

  • 常规背包问题(如0/1背包):
    状态定义:f[i][j]表示在前 i 个物品中选,背包容量不超过 j 时的最大价值
    转移方程:
    f[i][j] = f[i-1][j];  // 不选第i个物品
    if (j >= v[i]) {f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]); // 选第i个物品
    }
    
  • 本题垃圾陷阱问题:
    状态定义:f[i][j]表示处理完第 i 个垃圾后,高度为 j 时的最大血量

关键区别:
本题的状态表示是“处理完第 i 个垃圾后”!!!不是简单地考虑"选"或"不选",而是考虑每个垃圾怎么用

每个垃圾有两种使用方式(吃或堆放):

  1. 堆放垃圾会增加高度(j增加)
  2. 吃垃圾会增加血量(f[i][j]的值增加)

我们可以发现:在常规的背包问题中,状态表示都是状态转移方程研究的都是对于(i,j)这个状态的来源,它是由哪些状态转移而来的,研究是什么导致了f[i][j]。例如在经典01背包中,研究(i,j)这个状态,可以分两种情况,选择或者不选。于是我们都是在对当前的f[i][j]做递推。

但是在本题中,根据题意,我们的状态表示是 “处理完第 i 个垃圾后”,这个 “处理完” 就代表着垃圾已经处理了,我们要研究的是垃圾处理后导致了什么。会导致两种可能:要么加血,要么增加高度。于是我们要同时写出这两种状态转移方程。通过堆放垃圾增加高度,或者吃垃圾维持生命,两种操作都会影响不同的状态维度。

代码

dfs

数据范围问题:一般题目限制128MB的时候,开bool类型数组大概能开1e9左右。如果需要开更大的空间就做不到了,这时候可以使用map来储存数据。只不过map的O(logn)相对数组的O(1)就慢一些了。

为什么数据量大时用map?

  • 空间效率:实际问题中,DP状态往往是稀疏的,可能只有少量状态会被实际访问,数组方式会浪费大量空间存储未访问的状态。

  • 灵活性:可以处理状态值范围很大的情况,不受固定数组大小的限制。

  • 避免MLE:当状态空间理论上很大时,例如状态维度是1e5×100×100,数组方式无法存储。

#include<iostream>
#include<algorithm>
#include<map>using namespace std;//bool dp[10010][110][110];
map<int,map<int,map<int,bool>>> dp; //如果题目数据量太大就用mapstruct node
{int t,f,h;
}a[110];int d, g;
int ans1 = 1e9, ans2 = 0;bool cmp(node& a, node& b)
{return a.t < b.t;
}void dfs(int f,int h,int pos)
{if(h >= d) {ans1 = min(ans1, a[pos-1].t);return;}if(f < a[pos].t || pos > g){ans2 = max(ans2, f);return;}if(dp[f][h][pos]) return;dp[f][h][pos] = true;dfs(f + a[pos].f, h, pos + 1);dfs(f, h + a[pos].h, pos + 1);return;
}int main()
{cin >> d >> g;for(int i=1;i<=g;i++){cin >> a[i].t >> a[i].f >> a[i].h;}sort(a + 1,a + 1 + g,cmp);dfs(10,0,1);if(ans1 == 1e9) cout << ans2;else cout << ans1;return 0; 
}

背包dp

在绝对时间中不进行扣血操作。
举个例子,现在是中午12点,你醒了,你是一个机器人,你身上有2个小时电量,到了下午1点天上掉了一个补给包给你充了2个小时电,现在的电量够支撑到下午4点,但是下次掉落补给包在下午5点,于是你噶掉了。这就是比较绝对时间
同样一个故事,还可以这样叙述:现在是中午12点,你醒了,你是一个机器人,你身上有2个小时电量,过去了一个小时,你的电量只剩下1个小时了(绝对时间的下午1点),此时天上掉了一个补给包给你充了2个小时电,现在的电量增加到3个小时了,但是还有4个小时你才能拿到补给包(绝对时间下午5点),于是你噶掉了。

实际上它们是相同的逻辑,都是正确的。我分别写出他们的代码。

比较绝对时间

绝对时间的逻辑:

  • f[i-1][j]表示处理完前i-1个垃圾后,高度为j时的剩余血量

  • 这个血量是从时间0开始累积的"剩余生存时间"

  • 比较f[i-1][j] >= a[i].t 实际上是判断:从开始到现在,剩余的生命是否足够支撑到当前垃圾掉落的时间

完整代码如下:

#include <iostream>
#include <algorithm>
#include <cstring>using namespace std; const int N = 110, T = 1e5 + 10;int f[N][T]; //f[i][j]表示处理完第i个垃圾之后,高度为j时的最大血量。 处理完第i个垃圾之后!! 
int d,n; //垃圾井深度、垃圾的数量 
struct node
{int t,w,h; //扔下的时间、维持生命的时间、能垫高的高度 
}a[N];bool cmp(node& a, node& b)
{return a.t < b.t;
}int main() 
{cin >> d >> n;for(int i=1;i<=n;i++){cin >> a[i].t >> a[i].w >> a[i].h;	}	sort(a + 1, a + 1 + n, cmp); //排序哪个垃圾最先落下 f[0][0] = 10;for(int i=1;i<=n;i++){	for(int j=0;j<=d;j++){			if(f[i-1][j] >= a[i].t) //能拿到这个垃圾 {if(j + a[i].h >= d) //拿完就出去了,那就直接返回这个物品掉落的时间  {cout << a[i].t << endl;return 0;}//当前物品不能直接垫出去//记住:状态表示是处理完第i个物品!那么处理完会导致两种可能。而不是在1~i个物品中选择,第i个物品的状态是由两种可能性导致的 f[i][j] = max(f[i][j],f[i-1][j] + a[i].w); //吃掉第i个物品导致的状态f[i][j+a[i].h] = max(f[i][j+a[i].h],f[i-1][j]); //堆起来第i个物品导致的状态			 }}}int ret = 10;for(int i=1;i<=n;i++){if(ret < a[i].t) break;else ret += a[i].w;}cout << ret << endl;return 0;
}

问题:为什么不将全部 f 数组初始化为负无穷也是对的?

  • 血量永远不会为负值:因为吃垃圾只会增加血量(+a[i].w),而时间判断f[i-1][j] >= a[i].t已经确保了血量足够
  • 不会选择无效状态:无法到达的状态会保持为0,而有效状态的值都会≥10

比较相对时间

#include <iostream>
#include <algorithm>
#include <cstring>using namespace std; const int N = 110, T = 1e5 + 10;int f[N][T]; //f[i][j]表示处理完第i个垃圾之后,高度为j时的最大血量。 处理完第i个垃圾之后!! 
int d,n; //垃圾井深度、垃圾的数量 
struct node
{int t,w,h; //扔下的时间、维持生命的时间、能垫高的高度 
}a[N];bool cmp(node& a, node& b)
{return a.t < b.t;
}int main() 
{cin >> d >> n;for(int i=1;i<=n;i++){cin >> a[i].t >> a[i].w >> a[i].h;	}	sort(a + 1, a + 1 + n, cmp); //排序哪个垃圾最先落下 memset(f, -0x3f, sizeof f); //负无穷表示无法到达的状态	f[0][0] = 10;for(int i=1;i<=n;i++){int delta = a[i].t - a[i-1].t;for(int j=0;j<=d;j++){if(f[i-1][j] < delta) continue; //撑不到垃圾来就死了//能拿到垃圾 if(j + a[i].h >= d) //拿完就出去了,那就直接返回这个物品掉落的时间  {cout << a[i].t << endl;return 0;}//当前物品不能直接垫出去//记住:状态表示是处理完第i个物品!那么处理完会导致两种可能。而不是在1~i个物品中选择,第i个物品的状态是由两种可能性导致的 f[i][j] = max(f[i][j], f[i-1][j] - delta + a[i].w); //吃掉第i个物品导致的状态f[i][j+a[i].h] = max(f[i][j+a[i].h], f[i-1][j] - delta); //堆起来第i个物品导致的状态			 }}int ret = 10;for(int i=1;i<=n;i++){if(ret < a[i].t) break;else ret += a[i].w;}cout << ret << endl;return 0;
}

重点分析:
将代码写在if(f[i-1][j] >= a[i].t)(绝对时间代码演示中)的条件下,与直接if(f[i-1][j] < delta) continue;(相对时间代码演示中)过滤不符合条件的 j 的枚举是等效的。

Q:f(f[i-1][j] < delta) continue; 撑不到垃圾来就死了为什么还要continue ,直接break不行吗?
A:没有理解动态规划的过程。用 continue 是因为我们要枚举所有可能的高度 j(从 0 到 d),每一个 j 都是一个独立的状态。continue 表示跳过当前状态 j 的处理,继续尝试下一个高度。而 break 会直接退出这个 for 循环,不再检查更高的高度,这会导致状态没有被完整地转移,结果就会出错。

初始化分析:
相对时间中 f 数组必须要初始化为负无穷,因为相对时间下,血量可能会出现负数,会影响判断。

小结:对于动态规划的理解,可以从函数的角度来理解动态规划。我们可以把动态规划看作是在构造一个函数,这个函数的输入是某些“状态参数”,输出是该状态下的“最优值”或“可行性判断”等。如果你知道 f[i-1][j] 是准确的;那你可以只基于这个数值去计算 f[i][j],而不用关心“我是怎么走到 f[i-1][j] 这个状态的”。这跟函数的定义是一致的。函数不记录“历史”,它只关注输入和输出。

*无后效性是指:当前状态一旦确定,后续的决策和状态转移就只与当前状态有关,而与之前如何到达这个状态的路径无关。

相关文章:

  • Django的HelloWorld程序
  • 纯血Harmony NETX 5小游戏实践:贪吃蛇(附源文件)
  • Python训练营打卡 Day50
  • FreeRTOS信号量
  • @Configuration原理与实战
  • ​计算机网络原理超详解说​
  • Web后端基础:Maven基础
  • Web 毕设篇-适合小白、初级入门练手的 Spring Boot Web 毕业设计项目:智驿AI系统(前后端源码 + 数据库 sql 脚本)
  • P4 QT项目----串口助手(4.2)
  • 2025 高考:AI 都在哪些地方发挥了作用
  • crackme007
  • 电脑一段时间没用就变成登陆的界面
  • CppCon 2015 学习:Implementing class properties effectively
  • RocketMQ延迟消息机制
  • 【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
  • 第5章 类的基本概念 笔记
  • 不变性(Immutability)模式
  • b2b企业网络营销如何用deepseek、豆包等AI平台获客 上海添力
  • switch选择语句
  • 打造多模态交互新范式|彩讯股份中标2025年中国移动和留言平台AI智能体研发项目
  • 做愛表情网站/腾讯广告代理商加盟
  • wordpress 分享后可见/免费seo快速排名系统
  • 找事做网站/google官网登录入口
  • 后期网站/百度官方电话24小时
  • 做钢化膜网站/百度网址大全旧版安装
  • 网站备案咨询/网络营销的策略有哪些