【题解】[COCI 2024/2025 #2] 流明 / Blistavost

P11432 COCI 2024/2025 #2 流明 / Blistavost - 洛谷 (luogu.com.cn)


这题名字很好听哦。璀璨流明 / 流明水晶像是小马宝莉里哪匹小马的名字。

注意到数据范围,时间复杂度不可能带 log,初步判断是 做法。

考虑最优情况:

第一,能回头吗?

当然是能的,在保证 A 区间 < B 区间 的限制,当且仅当:

cpp 复制代码
如果 t_A > t_B + (R_B - R_A),就回头!

(这只是举个能回头的例子,实际情况要复杂得多,无法保证两个区间不相交)

第二,在已走过区间里的未熄灭区间,一定是连续的吗?

答案是不一定,但我们可以强行让它连续。

如果已走过区间 亮------暗------亮,中间那块暗的,还不如等到最后一次走过这块区域的时候灭。

这样会变得好处理很多。

第三,所有回头操作一定要在处理区间端点执行吗?

当然啦,毫无疑问的。

不然你多走一段是何意味(#`O′)

现在我们可以只关注区间端点,将它们离散化。

设计区间 dp 状态为:

cpp 复制代码
dp[l][r][0]:守卫在 l,只剩 [l, r] 没有被熄灭 的最小时间
dp[l][r][1]:守卫在 r,只剩 [l, r] 没有被熄灭 的最小时间

// 为什么是闭区间?因为守卫可以选择不熄灭那个位置上的灯,这样方便计算
// 隐含规则:必须在合法的时间才能走到 l 或者 r(后面代码会讲)

详见代码注释:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N = 5010;

struct node {
	LL x, t;
} a[N * 2];

LL dp[2 * N][2], p[2 * N][2];
// 两倍 N 就会炸空间,使用滚动数组 
// dp[l][r][0]:守卫在 l,只剩 [l, r] 没有被熄灭 的最小时间
// dp[l][r][1]:守卫在 r,只剩 [l, r] 没有被熄灭 的最小时间

// 为什么是闭区间?因为守卫可以选择不熄灭那个位置上的灯,这样方便计算
// 隐含规则:必须在合法的时间才能走到 l 或者 r(后面代码会讲)

bool cmp(node na, node nb) {
	if (na.x != nb.x) {
		return na.x < nb.x;
		// 保证 dp 处理从左到右 
	}
	return na.t < nb.t;
	// 按时间顺序排(一般情况不影响答案) 
}

int main () {
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int n;
	cin >> n;
	for (int i = 1; i <= n; i ++) {
		LL l, r, t;
		cin >> l >> r >> t;
		a[i * 2 - 1] = {l, t};
		a[i * 2] = {r, t};
	}
	
	n *= 2;
	sort (a + 1, a + n + 1, cmp);
	memset(dp, 0x7f, sizeof(dp));
	LL inf = dp[0][0];
	memset(p, 0, sizeof(p));
	// p 数组代表的是上一个 len 的 dp 数组
	// 第一次转移时范围是 [1, n],不存在什么 len = n + 1
	// 所以不会用到,不初始化也行 
	
	dp[1][0] = max(a[1].x, a[1].t);
	// dp[1][n][0] 
	dp[1][1] = max(a[n].x, a[n].t);
	// dp[1][n][1]
	
	LL ans = inf;
	for (int len = n; len >= 1; len --) {
		for (int i = 1; i + len - 1 <= n; i ++) {
			int j = i + len - 1;
			// 下面二维数组,想象中间维数插了个 [j] 
			if (i >= 2) {
				// 守卫从 i - 1 走到 i
				dp[i][0] = min(dp[i][0], p[i - 1][0] + a[i].x - a[i - 1].x);
				// 守卫从 i - 1 走到 j  
				dp[i][1] = min(dp[i][1], p[i - 1][0] + a[j].x - a[i - 1].x);
			}
			if (j <= n - 1) {
				// 守卫从 j + 1 走到 i 
				dp[i][0] = min(dp[i][0], p[i][1] + a[j + 1].x - a[i].x);
				// 守卫从 j + 1 走到 j 
				dp[i][1] = min(dp[i][1], p[i][1] + a[j + 1].x - a[j].x);
			}
			dp[i][0] = max(dp[i][0], a[i].t);
			dp[i][1] = max(dp[i][1], a[j].t);
			// 这里就是隐含规则,当前状态 i 或 j 是没有熄灭的
			// 但你必须在 a[i].t 或 a[j].t 及之后时刻到这里
			
			if (len == 1) {
				// 当 len = 1 时,代表 i = j,只有 [i, i] 没被熄灭
				// 手动操作一下就熄灭了,直接统计答案  
				ans = min(ans, min(dp[i][0], dp[i][1]));
			}
		}
		for (int i = 1; i <= n; i ++) {
			p[i][0] = dp[i][0];
			p[i][1] = dp[i][1];
			dp[i][0] = inf;
			dp[i][1] = inf;
			// 更新 p 数组,并初始化 dp数组 
		} 
	} 
	
	cout << ans << "\n";
	
	return 0;
}
相关推荐
程序喵大人1 小时前
【C++进阶】STL算法与函数对象 - 01 让算法去处理一段迭代器范围
开发语言·c++·算法·函数对象
Keven_111 小时前
算法札记:二叉树前序、中序、后序遍历及对应序列重要性质
数据结构·算法·深度优先
Delite8021 小时前
微库仑氧化法硫氯检测:油品分析中测量误差的控制要点
算法
宵时待雨1 小时前
优选算法专题12:队列
算法·leetcode·职场和发展
tianyu2341 小时前
双指针循环匹配金额——打款记录与账单的自动对账算法
java·算法·双指针循环·匹配金额
旖旎夜光2 小时前
LeetCode 738:单调递增的数字(贪心算法) —— 题解
数据结构·c++·算法·leetcode·贪心算法
青山木2 小时前
Hot 100 --- 搜索二维矩阵
java·算法·leetcode·矩阵
旖旎夜光2 小时前
LeetCode 1262:可被三整除的最大和 (贪心算法)—— 题解
数据结构·c++·算法·leetcode·贪心算法
心运软件2 小时前
基于机器学习的智能邮件分类系统:从数据采集到模型部署全流程实战
人工智能·python·算法·随机森林·机器学习·分类·scikit-learn