题解:洛谷 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\)LR,表示 Bessie 在第 \(i\) 秒的移动方向。如果有多条路线能使方向变化次数最少,输出任意一条。

【输入样例】

2
2 4

【输出样例】

RRLRLL

【核心思想】

  1. 问题分析: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\) 移动序列,使得方向变化次数最少。本质是给定链上每条边的遍历次数,构造从端点出发并回到端点的欧拉游走,最小化拐弯次数

  2. 算法选择

    • 贪心模拟:将每个单位区间看作一条边,\(A_i\) 表示边 \(i\)(连接点 \(i\)\(i+1\))需要被遍历的次数。核心贪心策略是尽量保持当前方向不变——每次决定下一步方向时,比较当前位置两侧相邻边的剩余次数,优先走向剩余次数更多的方向(避免未来被迫多次掉头),从而最小化总拐弯次数。
  3. 关键步骤(基于代码实现,下标调整为 \(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\) 实现掉头。
    • 输出:循环顺序输出的 \(L/R\) 字符串即为最少拐弯路线。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(T)\)\(T=\sum A_i \le 10^6\),循环内为常数操作。
    • 空间复杂度:\(O(N)\),存储 \(A\) 数组。
  5. 贪心模拟的核心思想

    • 边遍历到路线构造的转化:将穿越次数理解为“边容量”,问题变为在链图上从端点出发、遍历完所有边容量后回到端点的路径构造。
    • 最小化拐弯的贪心依据:在任意点处,若两侧都有剩余次数,选择继续沿当前方向走总是优于掉头(避免一次方向变化)。仅当继续前进方向的边剩余次数 不大于 反方向时,才需要提前掉头,防止后续某条边剩余次数集中在远处导致被迫多次拐弯。
    • 边界与动态平衡:算法通过实时比较两侧剩余次数,动态决定掉头时机,确保每条边的容量被恰好耗尽,拐弯次数达到理论下界。
    • 适用场景:适用于已知每条边被访问次数、需要构造端点固定路径并最小化方向切换频率的问题,常见于“走廊行走”、“农场巡逻”等模拟类题目。

【解题思路】

【算法标签】

普及 #模拟

【代码详解】

#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
posted @ 2026-08-29 09:41  团爸讲算法  阅读(11)  评论(0)    收藏  举报