CF1142B Lynyrd Skynyrd
我来讲一讲我的做法,时间复杂度 \(O(n \log n)\)。
适宜阅读
2024/7/26 upd:都快写成流水账了。
首先,我们定义 \(p\) 数组中一个数字 \(p_i\) 的下一个数字为:
对于 \(1 \le i \le n - 1\),\(p_i\) 的下一位为 \(p_{i + 1}\)。
对于 \(i = n\),\(p_i\) 的下一位为 \(p_1\)。
我们将这个数字记为 \(f(p_i)\)。
比如 \([1, 3, 4, 5, 2]\),
\(1\) 的下一位就是 \(3\),\(f(1) = 3\);
\(3\) 的下一位就是 \(4\),\(f(3) = 4\);
\(4\) 的下一位就是 \(5\),\(f(4) = 5\);
\(5\) 的下一位就是 \(2\),\(f(5) = 2\)
\(2\) 的下一位就是 \(1\),\(f(2) = 1\)。(注意这里)
然后我们回到 \(a\) 数组。
对于每个数字 \(a_i\),我们将 \(i\) 和在 \(i\) 右边第一个 \(f(a_i)\) 所在位置相连。
比如 \(p = [3, 1, 2], a = [3, 2, 1, 3, 1, 2]\)。
首先 \(f(3) = 1, f(1) = 2, f(2) = 3\)。
然后我们开始连边。
我们开始枚举 \(a\) 数组。
\(a_1 = 3\),查询得 \(f(3) = 1\),找到 \(3\) 右边第一个 \(1\),发现在位置 \(3\),我们将点 \(1\) 和点 \(3\) 连接。
\(a_2 = 2\),查询得 \(f(2) = 3\),找到 \(2\) 右边第一个 \(3\),发现在位置 \(4\),我们将点 \(2\) 和点 \(4\) 连接。
\(a_3 = 1\),查询得 \(f(1) = 2\),找到 \(1\) 右边第一个 \(2\),发现在位置 \(6\),我们将点 \(3\) 和点 \(6\) 连接。
\(a_4 = 3\),查询得 \(f(3) = 1\),找到 \(3\) 右边第一个 \(1\),发现在位置 \(5\),我们将点 \(4\) 和点 \(5\) 连接。
\(a_5 = 1\),查询得 \(f(1) = 2\),找到 \(1\) 右边第一个 \(2\),发现在位置 \(6\),我们将点 \(5\) 和点 \(6\) 连接。
对于找不到下一个数字在哪里的直接连到 \(m + 1\) 上面去。
\(a_6 = 2\),查询得 \(f(2) = 3\),发现 \(6\) 号位后没有数字 \(2\),我们就将点 \(6\) 和点 \(m + 1 = 7\) 相连。
然后,我们可以发现,如果从一个点沿着刚刚连的边跳 \(n - 1\) 次还没跳到 \(m + 1\),说明从它开始跳 \(n - 1\) 步所经过的所有点就可以构成一个序列,而不会跑到外面去。
比如从第 \(1\) 个点,我们接下来跳 \(n - 1 = 2\) 步:从位置 \(1\) 跳到位置 \(3\),从位置 \(3\) 跳到位置 \(6\),发现 \(6 < m + 1 = 7\),这说明什么?这说明一定跳出了一个序列!这个序列就是 \(3, 1, 2\),分别在位置(索引)\(1, 3, 6\)。
再比如从第 \(2\) 个点跳 \(n - 1 = 2\) 步:从位置 \(2\) 跳到位置 \(4\),从位置 \(4\) 跳到位置 \(5\),发现 \(5 < m + 1 = 7\),这说明可以构成一个题目要求的序列,即 \(2, 3, 1\),分别在位置(索引)\(2, 4, 5\)。
但是,每个点都跳 \(n - 1\) 步实在是太慢啦!我们套路地选择倍增,参考倍增求 LCA 的做法即可,这里不多说了。
然后我们就得出了每个点 \(i\) 跳 \(n - 1\) 步到达点,我们将它记为 \(g(i)\)。
对于每个询问 \(l, r\),我们可以枚举从 \(l, r\) 的每一个点 \(i\),即 \(l \le i \le r\),如果 \(g(i) \le r\),就说明从点 \(i\) 开始跳,跳 \(n - 1\) 步不会超过 \(r\),那么整个序列也就在 \(l\) 到 \(r\) 这段之间。
对于每个询问,每次枚举 \(l, r\),时间爆炸!\(O(nq)\) 的时间复杂度直接让我们之前全白干了。
所以我们又套路地选择 RMQ(ST 表) 来求解,ST 表的基础用法在这里不再赘述。
用 ST 表求出从 \(l\) 到 \(r\) 间最小的 \(g(i)\),如果它的最小值比 \(r\) 小,说明一定存在序列在 \(l\) 到 \(r\) 间。
时间复杂度:
倍增:\(O(n \log n)\),ST 表初始化:\(O(n \log n)\),询问:\(O(q)\)
总时间复杂度:\(O(n \log n)\)。
代码
注意:第 40 个点 \(m < n\) 坑人,注意初始化时选择 \(n\) 还是选择 \(m\)。
// Problem: Lynyrd Skynyrd
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/CF1142B
// Memory Limit: 250 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
const int N = 200010; // N 个点
int n, m, q; // n, m, q
int a[N], b[N]; // a 数组, b 数组
int lst[N], nxt[N]; // lst 记录一个数字最近的位置。 nxt 记录 a 数组中下一个数字,相当于 f(n)。
int fa[25][N]; // 树上倍增
int jump[N]; // jump[i] 记录从点 i 跳 n - 1 步到达的点
int jump_n_steps(int u, int step) { // 求点 i 跳 n - 1 步到达的点
for (int i = 24; i >= 0; i--) { // 套路,不多说了
if (step >> i & 1) {
u = fa[i][u];
}
}
return u;
}
int st[25][N]; // 存储 ST 表
void getst() { // 初始化 ST 表,不多说
for (int j = 1; (1 << j) <= m; j++) {
for (int i = 1; i + (1 << j) - 1 <= m; i++) {
st[j][i] = min(st[j - 1][i], st[j - 1][i + (1 << j - 1)]);
}
}
}
int query(int l, int r) { // ST 表操作:询问 l, r 的最小的 g(i),不多说
int len = r - l + 1;
if (len <= 0) return 0x3f3f3f3f;
int lg = __lg(len);
return min(st[lg][l], st[lg][r - (1 << lg) + 1]);
}
int main() {
ios::sync_with_stdio(false); // 优化输入
cin.tie(nullptr); // 优化输入
cin >> n >> m >> q; // 输入
for (int i = 1; i <= n; i++) { // 输入
cin >> a[i];
if (i > 1) nxt[a[i - 1]] = a[i]; // 对于 1 ~ n - 1, p[i] 的下一个数字为 p[i + 1]
}
nxt[a[n]] = a[1]; // 对于点 n, p[n] 的下一个数字为 p[1]
for (int i = 1; i <= m + 1; i++) fa[0][i] = m + 1;// 初始化第一个点跳出的位置为 m + 1
for (int i = 1; i <= n; i++) lst[i] = m + 1; // 第 40 个测试数据坑人,这里初始化用 n
for (int i = 1; i <= m; i++) cin >> b[i]; // 输入
for (int i = m; i >= 1; i--) { // 连边
fa[0][i] = lst[nxt[b[i]]];
lst[b[i]] = i;
}
for (int j = 1; j < 25; j++) {
for (int i = m + 1; i >= 1; i--) {
fa[j][i] = fa[j - 1][fa[j - 1][i]]; // 树上倍增,不多说
}
}
for (int i = 1; i <= m; i++) jump[i] = jump_n_steps(i, n - 1); // 跳 n - 1 步
for (int i = 1; i <= m; i++) st[0][i] = jump[i]; // 记录 g(i)
getst(); // 初始化 ST 表
while (q--) {
int l, r;
cin >> l >> r;
cout << (query(l, r) <= r); // 询问
}
return 0;
}