题解:洛谷 P9015 Moo Route
【题目来源】
洛谷:P9015 [USACO23JAN] Moo Route S - 洛谷
【题目描述】
农夫 Nhoj 把 Bessie 丢在了一个荒无人烟的地方!在时间 \(t=0\) 时,Bessie 位于无限数轴的 \(x=0\) 处。她疯狂地寻找出口,每秒向左或向右移动 1 个单位。然而,实际上并没有出口,在 \(T\) 秒后,Bessie 回到了 \(x=0\),疲惫且无奈。
农夫 Nhoj 试图追踪 Bessie,但他只知道 Bessie 穿过 \(x=0.5,1.5,2.5, \cdots ,(N-1).5\) 的次数,由数组 \(A_0,A_1, \cdots ,A_{N-1}\) 给出(\(1 \le N \le 10^5, 1 \le A_i \le 10^6, \sum A_i \le 10^6\))。Bessie 从未到达 \(x>N\) 或 \(x<0\)。
特别地,Bessie 的路线可以用一个长度为 \(T= \sum\limits_{i=0}^{N-1}A_i\) 的 \(L\) 和 \(R\) 字符串表示,其中第 \(i\) 个字符表示 Bessie 在第 \(i\) 秒移动的方向。方向变化的次数定义为 \(LR\) 出现的次数加上 \(RL\) 出现的次数。
请帮助农夫 Nhoj 找到任何一个与 \(A\) 一致且方向变化次数最少的 Bessie 的路线。保证至少存在一条有效路线。
【输入】
第一行包含 \(N\)。第二行包含 \(A_0,A_1,\cdots ,A_{N-1}\)。
【输出】
输出一个长度为 \(T=\sum\limits_{i=0}^{N-1}A_i\) 的字符串 \(S\),其中 \(S_i\) 是 L 或 R,表示 Bessie 在第 \(i\) 秒的移动方向。如果有多条路线能使方向变化次数最少,输出任意一条。
【输入样例】
2
2 4
【输出样例】
RRLRLL
【核心思想】
-
问题分析:Bessie 在数轴 \([0, N]\) 上每秒向左或向右移动 1 单位,起点和终点均在 \(x=0\)。已知她穿过半整数点 \(0.5, 1.5, \dots, (N-1).5\) 的总次数分别为 \(A_0, A_1, \dots, A_{N-1}\)(即单位区间 \([0,1]\) 被穿越 \(A_0\) 次,\([1,2]\) 被穿越 \(A_1\) 次,依此类推),需要构造一个长度 \(T=\sum A_i\) 的 \(L/R\) 移动序列,使得方向变化次数最少。本质是给定链上每条边的遍历次数,构造从端点出发并回到端点的欧拉游走,最小化拐弯次数。
-
算法选择:
- 贪心模拟:将每个单位区间看作一条边,\(A_i\) 表示边 \(i\)(连接点 \(i\) 和 \(i+1\))需要被遍历的次数。核心贪心策略是尽量保持当前方向不变——每次决定下一步方向时,比较当前位置两侧相邻边的剩余次数,优先走向剩余次数更多的方向(避免未来被迫多次掉头),从而最小化总拐弯次数。
-
关键步骤(基于代码实现,下标调整为 \(1\)~\(N\)):
- 建模:边 \(i\) 连接顶点 \(i\) 和 \(i+1\),剩余次数为 \(A_i\)。当前位置 \(cur\)(\(1 \le cur \le N+1\)),\(pre\) 表示上一个顶点。
- 起点处理:从点 \(1\)(即 \(x=0\))出发,第一步必须向右,输出
R,消耗边 1 的一次遍历,移动到点 2,\(pre=1\)。 - 贪心循环(重复 \(T-1\) 次):
- 若 \(cur=1\)(左边界):只能向右,输出
R,消耗边 1,\(cur\)、\(pre\) 均右移。 - 若 \(cur=N+1\)(右边界):只能向左,输出
L,消耗边 \(N\),\(cur\)、\(pre\) 均左移。 - 若从左向右到达(\(cur>pre\)):
- 若右侧边 \(cur\) 的剩余次数 大于 左侧边 \(pre\) 的剩余次数,则继续向右(保持方向),消耗边 \(cur\),\(cur\)、\(pre\) 均右移;
- 否则掉头向左,消耗边 \(pre\),交换 \(cur\) 和 \(pre\) 实现掉头。
- 若从右向左到达(\(cur<pre\)):
- 若左侧边 \(cur-1\) 的剩余次数 大于 右侧边 \(cur\) 的剩余次数,则继续向左(保持方向),消耗边 \(cur-1\),\(cur\)、\(pre\) 均左移;
- 否则掉头向右,消耗边 \(cur\),交换 \(cur\) 和 \(pre\) 实现掉头。
- 若 \(cur=1\)(左边界):只能向右,输出
- 输出:循环顺序输出的 \(L/R\) 字符串即为最少拐弯路线。
-
时间/空间复杂度:
- 时间复杂度:\(O(T)\),\(T=\sum A_i \le 10^6\),循环内为常数操作。
- 空间复杂度:\(O(N)\),存储 \(A\) 数组。
-
贪心模拟的核心思想:
- 边遍历到路线构造的转化:将穿越次数理解为“边容量”,问题变为在链图上从端点出发、遍历完所有边容量后回到端点的路径构造。
- 最小化拐弯的贪心依据:在任意点处,若两侧都有剩余次数,选择继续沿当前方向走总是优于掉头(避免一次方向变化)。仅当继续前进方向的边剩余次数 不大于 反方向时,才需要提前掉头,防止后续某条边剩余次数集中在远处导致被迫多次拐弯。
- 边界与动态平衡:算法通过实时比较两侧剩余次数,动态决定掉头时机,确保每条边的容量被恰好耗尽,拐弯次数达到理论下界。
- 适用场景:适用于已知每条边被访问次数、需要构造端点固定路径并最小化方向切换频率的问题,常见于“走廊行走”、“农场巡逻”等模拟类题目。
【解题思路】


【算法标签】
普及 #模拟
【代码详解】
#include <bits/stdc++.h>
using namespace std;
int n, a[100005], sum=0;
int main()
{
cin >> n; // 输入n
for (int i=1; i<=n; i++) { // 记录n个区间的线段数
cin >> a[i];
sum+=a[i]; // 计算总跳出
}
int cur=1, pre=0; // 预置起始位置和前一个位置
for (int i=1; i<=sum; i++) { // 按总条数循环
if (cur==1) { // 当处在最左边区间时
cout << 'R'; // 输出R
a[cur]--; // 线段数减1
cur++; // 当前和前一个位置均自增1
pre++;
} else if (cur==n+1) { // 当处在最右边区间之外时
cout << 'L'; // 输出L
a[cur-1]--; // 最后一个区间的线段数减1
cur=n; // 修改当前位置和前一个位置
pre=n+1;
} else { // 其他区间时
if (cur>pre) { // 之前是从左向右走
if (a[cur]>a[pre]) { // 判断向右走
cout << 'R';
a[cur]--;
cur++;
pre++;
} else { // 还是向左走
cout << 'L';
a[pre]--;
swap(cur, pre);
}
} else { // 之前是从右向左走
if (a[cur-1]>a[cur]) { // 判断向左走
cout << 'L';
a[cur-1]--;
cur--;
pre--;
} else { // 还是向右走
cout << 'R';
a[cur]--;
swap(cur, pre);
}
}
}
}
cout << endl;
return 0;
}
【运行结果】
2
2 4
RRLRLL
浙公网安备 33010602011771号