1 条题解
-
0
星辰瞬移路径规划问题题解
一、题目分析
1. 问题核心
- 目标:从起点星辰
s出发,通过n-1次瞬移遍历所有n颗星辰,选择每次瞬移的方向(向左/向右),使总能量消耗最小,并输出具体路径。 - 关键约束:第
i次瞬移的成本由方向决定,向左为l[i],向右为r[i];路径需包含所有星辰,且起点固定为s。 - 数据规模:
n最大可达5e5,要求算法时间复杂度为O(n),否则会超时。
2. 问题本质
该问题可转化为“带方向成本的线性遍历路径构造”:星辰沿直线排列,每次瞬移的方向选择直接影响成本,需在保证遍历所有星辰的前提下,通过贪心策略选择最小成本方向,并高效构造路径。
二、算法思路
1. 核心策略:贪心选择方向
贪心的核心逻辑是“每次瞬移优先选成本更低的方向”,但需处理一个特殊约束:若起点
s不是最右侧星辰,前s次瞬移中至少要有一次向左(否则无法遍历左侧星辰)。具体步骤如下:- 初始方向统一:若第一次瞬移的向右成本更低(
l[1] > r[1]),通过反转节点编号(x = n - x + 1),将问题转化为“第一次优先向左”的统一场景,简化代码逻辑。 - 标记方向选择:对第
i次瞬移,用c[i]标记方向——c[i] = 1表示选向右(r[i] < l[i]),c[i] = 0表示选向左(l[i] ≤ r[i])。 - 处理特殊约束:若
s ≠ n且前s次瞬移均未选向左(c[1..s]全为0),需在这s次中选“成本差最小”的一次强制改为向左(c[d] = 1),确保能遍历左侧星辰。
2. 路径构造:双端队列高效维护
路径构造的关键是利用“连续方向批量处理”,用双端队列(
deque)维护未访问节点,根据连续的方向批量弹出节点,避免逐个处理的低效:- 初始化队列:将除起点
s外的所有星辰加入双端队列p,顺序为1,2,...,s-1,s+1,...,n。 - 批量处理连续方向:
- 找到连续相同方向的区间(如
i到j的c值均相同),一次性处理该区间的所有瞬移。 - 若方向为向左(
c[i] = 0):从队列头部弹出节点(左侧未访问节点),加入路径。 - 若方向为向右(
c[i] = 1):从队列尾部弹出节点(右侧未访问节点),加入路径。
- 找到连续相同方向的区间(如
- 恢复反转:若之前进行了节点编号反转,将路径中的所有节点编号反转回原编号,得到最终路径。
3. 总成本计算
遍历所有
n-1次瞬移,根据c[i]累加成本:c[i] = 1时加r[i],c[i] = 0时加l[i]。三、代码逐段解析
1. 输入处理与初始方向统一
cin >> n >> s; for (int i = 1; i < n; i++) cin >> l[i] >> r[i]; // 若第一次瞬移向右成本更低,反转节点编号,统一处理逻辑 if (l[1] > r[1]) { rev = 1; s = n - s + 1; // 起点反转 for (int i = 1; i < n; i++) swap(l[i], r[i]); // 成本反转(原向右变向左,反之亦然) }- 作用:将“第一次优先向右”的场景转化为“第一次优先向左”,避免后续处理分情况讨论,简化代码。
2. 方向选择与特殊约束处理
// 标记每次瞬移的最优方向 for (int i = 1; i < n; i++) c[i] = r[i] < l[i]; // 处理s≠n时,前s次需至少一次向左的约束 if (s != n) { int ok = 0; // 检查前s次是否已有向左的选择 for (int i = 1; i <= s; i++) if (c[i]) ok = 1; if (!ok) { // 选成本差最小的一次强制向左 int mn = 1e18, d = -1; for (int i = 1; i <= s; i++) { if (r[i] - l[i] < mn) { mn = r[i] - l[i]; d = i; } } c[d] = 1; } }- 关键逻辑:
c[i] = r[i] < l[i]直接体现贪心选择——成本低的方向优先;特殊约束处理确保路径能覆盖左侧星辰,避免“卡死”。
3. 双端队列构造路径
vector<int> ans; ans.push_back(s); // 路径起点 deque<int> p; // 初始化未访问节点队列 for (int i = 1; i <= n; i++) if (i != s) p.push_back(i); // 批量处理连续方向的瞬移 for (int i = 1, j; i <= n - 1; i = j + 1) { j = i; // 找到连续相同方向的区间[i, j] while (j + 1 <= n - 1 && c[j + 1] == c[j]) j++; if (!c[i]) { // 方向向左:从队列头部取节点(左侧未访问) for (int k = j - i; k >= 0; k--) ans.push_back(p[k]); for (int k = j - i; k >= 0; k--) p.pop_front(); } else { // 方向向右:从队列尾部取节点(右侧未访问) for (int k = p.size() - (j - i + 1); k < p.size(); k++) ans.push_back(p[k]); for (int k = j - i; k >= 0; k--) p.pop_back(); } } // 恢复反转的节点编号 if (rev) for (auto &x : ans) x = n - x + 1;- 效率优势:双端队列的
push_back/pop_front/pop_back操作均为O(1),批量处理连续方向使整体路径构造时间为O(n),适合大规模数据。
4. 输出总成本与路径
// 计算总成本 int w = 0; for (int i = 1; i < n; i++) w += c[i] ? r[i] : l[i]; // 输出结果 cout << w << "\n"; for (int i = 0; i < n; i++) cout << ans[i] << " \n"[i + 1 == n];四、关键细节与易错点
- 节点编号反转的正确性:反转后,“向左”和“向右”的含义也随之反转,因此需同时交换
l[i]和r[i],确保成本对应正确。 - 特殊约束的必要性:若
s ≠ n且前s次均向右,会导致左侧星辰无法遍历(起点s左侧有s-1颗星辰,需至少一次向左才能到达),必须强制调整一次方向。 - 双端队列的批量处理:需准确计算连续方向区间的长度(
j - i + 1),避免多取或少取节点,确保路径包含所有星辰。
五、样例验证
以样例输入为例,逐步验证算法逻辑:
- 输入:
n=4, s=2;l=[5,4,2],r=[3,6,2] - 初始方向统一:
l[1]=5 > r[1]=3,需反转:- 反转后
s = 4 - 2 + 1 = 3;l与r交换,变为l=[3,6,2],r=[5,4,2]。
- 反转后
- 标记方向:
c[1]:r[1]=5 > l[1]=3→c[1]=0(向左)。c[2]:r[2]=4 < l[2]=6→c[2]=1(向右)。c[3]:r[3]=2 == l[3]=2→c[3]=0(向左)。
- 特殊约束处理:
s=3 ≠ 4,检查前3次c值(0,1,0),已有向左,无需调整。 - 路径构造:
- 未访问节点队列
p = [1,2,4];起点ans = [3]。 - 处理
i=1(c[1]=0,连续区间[1,1]):从头部取1个节点1,ans = [3,1],p = [2,4]。 - 处理
i=2(c[2]=1,连续区间[2,2]):从尾部取1个节点4,ans = [3,1,4],p = [2]。 - 处理
i=3(c[3]=0,连续区间[3,3]):从头部取1个节点2,ans = [3,1,4,2]。
- 未访问节点队列
- 恢复反转:将节点编号反转(
x=4-x+1),ans变为[2,4,1,3],与样例输出一致。 - 总成本:
c[1]=0(加l[1]=3)、c[2]=1(加r[2]=4)、c[3]=0(加l[3]=2),总3+4+2=9,符合样例。
六、学习建议
- 贪心策略理解:重点掌握“为何贪心选择方向可行”——由于每次瞬移的成本仅与次数和方向有关,与当前位置无关,因此局部最优(每次选低成本方向)可导向全局最优。
- 数据结构应用:学习双端队列在“两端取元素”场景中的高效性,对比数组或普通队列的劣势(如数组头部删除为
O(n))。 - 边界处理:关注特殊情况(如
s=1、s=n、l[i]=r[i]),理解代码中针对这些情况的处理逻辑,避免边界错误。
- 目标:从起点星辰
- 1
信息
- ID
- 4369
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者