题解:洛谷 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
【核心思想】
-
问题分析:给定 \(N\) 个港口,每个港口有两条出河(左/右),导游按长度为 \(M\) 的方向序列重复 \(K\) 次选择。求从港口 \(1\) 出发,经过 \(K\) 轮重复后最终到达的港口。这是一个倍增(Binary Lifting)问题,关键在于将"一轮 \(M\) 个操作"视为一个复合操作,用倍增预处理 \(2^j\) 轮后的位置,然后二进制分解 \(K\) 实现 \(O(\log K)\) 查询。
-
算法选择:
- 复合操作预处理:先计算每个港口执行一轮(\(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\) 轮跳跃
-
关键步骤:
- 读取输入:\(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\)
-
时间/空间复杂度:
- 时间复杂度:预处理 \(O(N \cdot (M + \log K))\),查询 \(O(\log K)\)
- 空间复杂度:\(O(N \log K)\),倍增表
-
倍增的核心思想:
- 复合操作抽象:将 \(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
浙公网安备 33010602011771号