题解:洛谷 P3083 Luxury River Cruise

【题目来源】

洛谷:P3083 [USACO13OPEN] Luxury River Cruise S - 洛谷

【题目描述】

农民约翰正带着贝西和其他奶牛们进行一次巡航!他们航行在一个有 \(N\) 个港口(\(1 \le N\le10^3\))的河网中,港口编号为 \(1,2,3\dots N\),贝西从港口 \(1\) 出发。每个港口恰好有两条流出的河道,直接通向其他港口,并且这些河道只能单向航行。

在每个港口,导游会选择走左边的河道或右边的河道继续向下游航行,但他们会一遍又一遍地重复相同的选择顺序。更具体地说,导游事先选定了一个长度为 \(M\) 的方向序列(\(1\le M\le500\)),每个方向要么是左,要么是右,然后将这个序列重复 \(K\) 次(\(1\le k\le10^9\)),请帮她计算出她最终会停在哪个港口。

【输入】

\(1\) 行:三个用空格隔开的整数 \(N,M,K\)

\(2\) 行到第 \(N+1\) 行中,第 \(i+1\) 行包含两个用空格隔开的整数,分别表示港口 \(i\) 的左边河道和右边河道通向的港口编号。

\(N+2\) 行:M 个用空格隔开的字符,每个字符是 L 或 R。L 表示选择左边的河道,R 表示选择右边的河道。

【输出】

输出一个整数,表示贝西的巡航最终停靠的港口编号。

【输入样例】

4 3 3
2 4
3 1
4 2
1 3
L L R

【输出样例】

4

【核心思想】

  1. 问题分析:给定 \(N\) 个港口,每个港口有两条出河(左/右),导游按长度为 \(M\) 的方向序列重复 \(K\) 次选择。求从港口 \(1\) 出发,经过 \(K\) 轮重复后最终到达的港口。这是一个倍增(Binary Lifting)问题,关键在于将"一轮 \(M\) 个操作"视为一个复合操作,用倍增预处理 \(2^j\) 轮后的位置,然后二进制分解 \(K\) 实现 \(O(\log K)\) 查询。

  2. 算法选择

    • 复合操作预处理:先计算每个港口执行一轮(\(M\) 个方向)后到达的港口,记为 \(f[i][0]\)
    • 倍增表\(f[i][j]\) 表示从港口 \(i\) 出发执行 \(2^j\) 轮后到达的港口,递推式 \(f[i][j] = f[f[i][j-1]][j-1]\)
    • 二进制分解:将 \(K\) 拆分为 \(2\) 的幂次之和,通过查表 \(O(\log K)\) 完成 \(K\) 轮跳跃
  3. 关键步骤

    • 读取输入\(N, M, K\),每个港口的左右出河 \(a[i][0/1]\),方向序列 \(flag[1..M]\)
    • 一轮预处理\(i\)\(1\)\(N\)):
      • 从港口 \(i\) 出发,按 \(flag[1..M]\) 依次选择左右,得到一轮后的位置 \(f[i][0]\)
    • 倍增预处理\(j\)\(1\)\(29\)):
      • \(f[i][j] = f[f[i][j-1]][j-1]\):执行 \(2^j\) 轮 = 先 \(2^{j-1}\) 轮,再 \(2^{j-1}\)
    • 模拟 \(K\)
      • \(pos = 1\)(起始港口)
      • 从高位到低位遍历 \(i\)\(29\)\(0\)):
        • \(K\) 的第 \(i\) 位为 \(1\)\(pos = f[pos][i]\)
    • 输出 \(pos\)
  4. 时间/空间复杂度

    • 时间复杂度:预处理 \(O(N \cdot (M + \log K))\),查询 \(O(\log K)\)
    • 空间复杂度:\(O(N \log K)\),倍增表
  5. 倍增的核心思想

    • 复合操作抽象:将 \(M\) 个连续操作封装为一个"轮"操作,把问题从"执行 \(K \times M\) 次选择"转化为"执行 \(K\) 轮复合操作"
    • 二进制分解降维:任何整数 \(K\) 可唯一表示为若干 \(2^i\) 之和,将线性遍历转化为对数级跳跃
    • 倍增递推\(f[i][j]\)\(f[i][j-1]\)\(f[f[i][j-1]][j-1]\) 组合,体现"大事化小、小事化了"的分治思想
    • 适用于"重复序列 + 大规模跳跃"类问题,核心是将周期操作抽象为复合操作后用倍增加速

【算法标签】

普及 #倍增

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 1005;
int n, m, k;  // n: 位置数, m: 操作序列长度, k: 操作轮数
int a[N][2];  // a[i][0/1]表示在位置i,选择左/右后到达的位置
bool flag[N]; // flag[i]表示第i个操作是L(0)还是R(1)
int f[N][30]; // f[i][j]表示从位置i开始,执行2^j轮操作后到达的位置

int main()
{
    // 输入
    cin >> n >> m >> k;

    // 读入位置转移表
    for (int i = 1; i <= n; i++)
        cin >> a[i][0] >> a[i][1];  // a[i][0]是左选择,a[i][1]是右选择

    // 读入操作序列
    for (int i = 1; i <= m; i++)
    {
        char opt;
        cin >> opt;
        if (opt == 'R')  // 右操作
            flag[i] = 1;  // 标记为1
        else
            flag[i] = 0;  // 标记为0(左操作)
    }

    // 预处理:计算每个位置执行一轮(m个操作)后到达的位置
    for (int i = 1; i <= n; i++)
    {
        int pos = i;  // 从位置i开始
        for (int j = 1; j <= m; j++)  // 执行一轮m个操作
        {
            // 根据操作类型选择左或右
            pos = a[pos][flag[j]];
        }
        f[i][0] = pos;  // 记录执行一轮后的位置
    }

    // 倍增预处理:计算执行2^j轮后的位置
    for (int j = 1; j < 30; j++)  // 2^30 > 1e9,足够大
    {
        for (int i = 1; i <= n; i++)
        {
            // 执行2^j轮 = 先执行2^(j-1)轮,再执行2^(j-1)轮
            f[i][j] = f[f[i][j-1]][j-1];
        }
    }

    // 模拟执行k轮操作
    int pos = 1;  // 起始位置是1
    for (int i = 29; i >= 0; i--)  // 从高位到低位处理k的二进制
    {
        if (k & (1 << i))  // 如果k的第i位是1
        {
            // 从当前位置执行2^i轮
            pos = f[pos][i];
        }
    }

    cout << pos << endl;  // 输出最终位置
    return 0;
}

【运行结果】

4 3 3
2 4
3 1
4 2
1 3
L L R
4
posted @ 2026-08-28 15:57  团爸讲算法  阅读(5)  评论(0)    收藏  举报