# P17141 [NOI 2026] 传送 ## 题目背景 题面、样例附件来自 [QOJ](h...

생성일: 2026년 7월 28일

답변에 사용된 모델 GPT-5.6 Thinking by Chat01

질문

P17141 [NOI 2026] 传送

题目背景

题面、样例附件来自 QOJ

提交到洛谷上时,无需引用头文件 #include "teleport.h"。直接将

cpp
std::vector<std::pair<long long, int>> teleport(int c, int n, int m, std::vector<int> u, std::vector<int> v, std::vector<int> x, std::vector<int> y);

复制到程序开头,同时选用 C++17 或者更高版本编译器编译。

题目描述

CC 国共有 nn 座城市,编号为 0n10\sim n-1。这 nn 座城市由 n1n-1 条道路连接,形成树形结构。第 ii0i<n10\le i<n-1)条道路连接城市 uiu_iviv_i,从其一端的城市到达另一端需要耗费 11 单位的时间。

为了提升通行效率,CC 国研发了一种新型传送门。每座城市中均有一扇传送门。使用传送门同样耗费 11 单位时间,但由于系统尚不稳定,它会将使用者 等概率 地传送到所有 nn 座城市之一。注意:使用传送门也可能被传送到当前所在的城市。

为了检测传送门的效果,CC 国进行了 mm 次测试。第 ii0i<m0\le i<m)次测试要求测试员从城市 xix_i 出发,去往城市 yiy_i。在从起点前往终点的过程中,测试员可以选择沿道路移动,或是使用传送门。由于可能的通行方式很多,测试员需要计算出期望耗时最短的通行方式。

具体地,定义一种通行方式如下:对于每座非终点的城市,选择一座与其相邻的城市或是使用传送门,每当测试员到达该城市时,均按事先确定的方式移动,即移动至该相邻的城市,或使用传送门。

形式化地,一种通行方式可以用一个长度为 nn 的序列 [a0,,an1][a_0,\ldots,a_{n-1}] 表示,其中 ayi=1a_{y_i}=-1,且对于所有 jyij\ne y_i,均有 aja_jjj 相邻,或 aj=na_j=n。每当测试员到达城市 jjjyij\ne y_i)时,若 aj<na_j<n,则测试员将移动到 aja_j,否则测试员将使用传送门。

称一种通行方式是合理的,当且仅当其期望耗时为有限值。

对于每次测试,请计算在所有合理的通行方式中,期望耗时的最小值。

【实现细节】

选手不需要,也不应该实现 main 函数。

选手需要确保提交的程序源文件包含头文件 teleport.h,即在程序开头加入以下代码:

cpp
#include "teleport.h"

选手需要在提交的程序源文件 teleport.cpp 中实现以下函数:

cpp
std::vector<std::pair<long long, int>> teleport(int c, int n, int m, std::vector<int> u, std::vector<int> v, std::vector<int> x, std::vector<int> y);
  • c,n,mc,n,m 分别表示测试点编号、城市数量和测试的次数。c=0c=0 表示该测试点为样例。
  • u,vu,v 分别表示每条道路连接的两座城市。
  • x,yx,y 分别表示每次测试的起点与终点。
  • 该函数需要返回一个长度 恰好mm二元组 序列 (a0,b0),(a1,b1),,(am1,bm1)(a_0,b_0),(a_1,b_1),\ldots,(a_{m-1},b_{m-1}),其中 ai,bia_i,b_i0i<m0\le i<m)表示第 ii 次测试中,期望耗时的最小值的 最简分数形式aibi\frac{a_i}{b_i}。特别地,若期望耗时的最小值为正整数,则视为 bi=1b_i=1
  • 对于每个测试点,该函数会被评测程序调用恰好一次。

本试题目录下的 template_teleport.cpp 是提供的示例代码,选手可参考并实现自己的代码。

输入格式

【测试程序方式】

选手可以在本题目录下使用如下命令编译得到可执行文件:

bash
g++ grader.cpp teleport.cpp -o teleport -O2 -std=c++14 -static

对于编译得到的可执行文件 teleport

  • 可执行文件将从标准输入读入以下格式的数据:
    • 第一行包含三个非负整数 c,n,mc,n,m
    • i+2i+20i<n10\le i<n-1)行包含两个非负整数 ui,viu_i,v_i
    • i+n+1i+n+10i<m0\le i<m)行包含两个非负整数 xi,yix_i,y_i
  • 可执行文件将输出以下格式的数据至标准输出:
    • i+1i+10i<m0\le i<m)行包含两个正整数 ai,bia_i,b_i

输出格式

输入输出样例 #1

输入 #1

text
0 4 4 0 1 1 2 2 3 0 3 0 1 0 2 1 2

输出 #1

text
7 3 1 1 2 1 1 1

说明/提示

【样例 11 解释】

对于第 00 次测试:

  • 若通行方式为 [1,2,3,1][1,2,3,-1],则耗时为固定值 33
  • 若通行方式为 [4,2,3,1][4,2,3,-1],则测试员将不断使用传送门直至离开城市 00,因此期望耗时为 73\frac{7}{3}
  • 若通行方式为 [4,4,3,1][4,4,3,-1],则测试员将不断使用传送门直至到达城市 22 或城市 33,因此期望耗时为 33
  • 若通行方式为 [1,0,4,1][1,0,4,-1],则测试员将永远在城市 00 与城市 11 间移动,因此该通行方式不是合理的。

可以证明,期望耗时的最小值为 73\frac{7}{3}

【样例 22

见选手目录下的 teleport/teleport2.inteleport/teleport2.ans

该样例满足测试点 2,32,3 的约束条件。

【样例 33

见选手目录下的 teleport/teleport3.inteleport/teleport3.ans

该样例满足测试点 464\sim6 的约束条件。

【样例 44

见选手目录下的 teleport/teleport4.inteleport/teleport4.ans

该样例满足测试点 787\sim8 的约束条件。

【样例 55

见选手目录下的 teleport/teleport5.inteleport/teleport5.ans

该样例满足测试点 99 的约束条件。

【样例 66

见选手目录下的 teleport/teleport6.inteleport/teleport6.ans

该样例满足测试点 1616 的约束条件。

【样例 77

见选手目录下的 teleport/teleport7.inteleport/teleport7.ans

该样例满足测试点 172017\sim20 的约束条件。

【数据范围】

对于所有测试数据,均有:

  • 2n5×1052\le n\le5\times10^51m1061\le m\le10^6
  • 对于所有 0i<n10\le i<n-1,均有 0ui,vi<n0\le u_i,v_i<n,且所有 (ui,vi)(u_i,v_i) 构成一棵树;
  • 对于所有 0i<m0\le i<m,均有 0xi,yi<n0\le x_i,y_i<nxiyix_i\ne y_i

::cute-table{tuack}

测试点编号nn\lemm\le特殊性质
11442020
2,32,3553030^
464\sim610210^211^
7,87,810310^320002000AA
99^10610^6
10,1110,1110510^5^AA
121512\sim15^^
16165×1055\times10^55×1055\times10^5BB
172017\sim20^10610^6

特殊性质 AA:对于所有 0i<n10\le i<n-1,均有 ui=iu_i=ivi=i+1v_i=i+1

特殊性质 BB:对于所有 0i<m0\le i<m,均有 yi=0y_i=0

cpp
// Author: lzm0107 #include <bits/stdc++.h> std::vector<std::pair<long long, int>> teleport(int c, int n, int m, std::vector<int> u, std::vector<int> v, std::vector<int> x, std::vector<int> y); using namespace std; constexpr int kMaxN = 500'000, kMaxM = 1'000'000; long long Cgcd(long long a, long long b) { return b == 0 ? a : Cgcd(b, a % b); } vector<int> adj[kMaxN + 10]; int depth[kMaxN + 10], parent[kMaxN + 10]; int st[__lg(kMaxN) + 2][kMaxN + 10]; int dfn[kMaxN + 10], dfo[kMaxN + 10], idx; void PreProcess(int u) { dfn[u] = idx; dfo[idx] = u; idx++; st[0][dfn[u]] = dfn[parent[u]]; for (auto v : adj[u]) { if (v == parent[u]) { continue; } parent[v] = u, depth[v] = depth[u] + 1; PreProcess(v); } } int GetLCA(int u, int v) { if (u == v) { return u; } u = dfn[u], v = dfn[v]; if (u > v) { swap(u, v); } u++; int tmp = __lg(v - u + 1); return dfo[min(st[tmp][u], st[tmp][v - (1 << tmp) + 1])]; } int GetDistance(int u, int v) { return depth[u] + depth[v] - 2 * depth[GetLCA(u, v)]; } vector<pair<long long, int>> teleport(int c, int n, int m, vector<int> u, vector<int> v, vector<int> x, vector<int> y) { vector<int> deg(n); for (int i = 0; i < n - 1; i++) { deg[u[i]]++, deg[v[i]]++; } vector<int> f(n), g(n, 1), h(n); vector<long long> wsc(n); vector<int> sc(n); vector<bool> processed(n); vector<pair<long long, int>> baseline(n); int unprocessed_cnt = n; for (int i = 0; i < n - 1; i++) { adj[u[i]].push_back(v[i]); adj[v[i]].push_back(u[i]); } memset(parent, 0xff, sizeof(parent)); parent[0] = 0; PreProcess(0); for (int i = 1; 1 << i <= n; i++) { for (int j = 0; j + (1 << i) - 1 < n; j++) { st[i][j] = min(st[i - 1][j], st[i - 1][j + (1 << i - 1)]); } } for (int i = 0; i < n; i++) { adj[i].clear(); } vector<int> odeg(deg); for (int i = 0; i < n - 1; i++) { u[i] = dfn[u[i]], v[i] = dfn[v[i]]; adj[u[i]].push_back(v[i]); adj[v[i]].push_back(u[i]); } for (int i = 0; i < n; i++) { deg[dfn[i]] = odeg[i]; } for (int d = 0; unprocessed_cnt > 0; d++) { for (int i = 0; i < n; i++) { if (!processed[i]) { wsc[i] += g[i] * d; sc[i] += g[i]; if ((long long)sc[i] * (d + 1) - wsc[i] >= n) { unprocessed_cnt--; processed[i] = true; long long t0 = n + wsc[i], t1 = sc[i]; long long g = Cgcd(t0, t1); baseline[i].first = t0 / g, baseline[i].second = t1 / g; } } if (d == 1) { h[i] = (long long)-f[i] * deg[i]; } else { h[i] = (long long)-f[i] * (deg[i] - 1); } for (auto j : adj[i]) { h[i] += g[j]; } } swap(f, g); swap(g, h); } vector<pair<long long, int>> new_baseline(n); for (int i = 0; i < n; i++) { new_baseline[i] = baseline[dfn[i]]; } vector<pair<long long, int>> answer(m); for (int i = 0; i < m; i++) { int d = GetDistance(x[i], y[i]); if ((long long)d * new_baseline[y[i]].second >= new_baseline[y[i]].first) { answer[i] = new_baseline[y[i]]; } else { answer[i] = {d, 1}; } } return answer; }

QOJ 通过,洛谷 TLE 75,如何卡常?

不要告诉我改成基于点分树或者长链剖分的 poly log 做法,我只想用我的根号做法通过。

사고

Compiled and benchmarked an optimized teleport solver

Worked for 6m 7s

답변

你这份代码的瓶颈确实能在不改变根号算法的前提下解决。QOJ 时限只有 3.5 秒;题面给出的编译方式也是 -O2 -static,所以这里需要降低内存访问和间接寻址,而不只是加 pragma。(QOJ)

最大的几个常数问题

1. 不要在每一轮遍历 vector<int> adj[n]

根号部分最多约 10001000 轮。当前代码每轮执行:

cpp
for (int i = 0; i < n; i++) for (auto j : adj[i])

也就是反复访问 50 万个 vector 对象及其分散的堆内存。

DFS 序中父亲一定出现在儿子前面,只保留:

cpp
par[i] = 父亲的 DFS 序

即可计算下一层:

cpp
h[i] = g[par[i]] - f[i] * (deg[i] - 1); h[par[i]] += g[i];

因为遍历到 i 时,h[par[i]] 已经初始化,所以初始化和向父亲贡献可以在同一个循环完成。

2. 删除 wscprocessed

设:

Sd=r=0dcr,Qd=r=0d(d+1r)cr.S_d=\sum_{r=0}^d c_r, \qquad Q_d=\sum_{r=0}^d(d+1-r)c_r.

那么:

Sd=Sd1+cd,Qd=Qd1+Sd.S_d=S_{d-1}+c_d,\qquad Q_d=Q_{d-1}+S_d.

只需要:

cpp
cnt[i] += g[i]; lhs[i] += cnt[i];

判定条件就是:

cpp
lhs[i] >= n

而原来的距离和满足:

wsc=Sd(d+1)Qd.\operatorname{wsc}=S_d(d+1)-Q_d.

所以分子为:

cpp
n + cnt[i] * (d + 1) - lhs[i]

并且 lhs[i] >= n 本身就可以充当 processed 标记。

3. 你的 int h 存在有符号溢出 UB

例如某个高度数点上:

cpp
-f[i] * (deg[i] - 1)

可能达到 O(n2)O(n^2),虽然之后邻居贡献会把它抵消到 [0,n][0,n],但中间的有符号溢出是未定义行为。

可以有意使用 uint32_t2322^{32} 运算。线性递推在模意义下仍成立,而最终的距离层点数不超过 n<232n<2^{32},所以最终值就是正确值。

4. 不要重建第二份邻接表

第一次建树直接用 CSR 连续数组。得到 DFS 序、父亲和深度后,邻接表即可释放。根号部分只访问连续数组。


可直接提交的版本

仍然是你的:

O(nn+nlogn+m)O(n\sqrt n+n\log n+m)

做法,没有点分树和长链剖分。

cpp
#include <bits/stdc++.h> std::vector<std::pair<long long, int>> teleport( int c, int n, int m, std::vector<int> u, std::vector<int> v, std::vector<int> x, std::vector<int> y ); using namespace std; namespace { constexpr int kMaxN = 500000; constexpr int kLog = 20; alignas(64) int rmq[kLog][kMaxN]; } // namespace vector<pair<long long, int>> teleport( int c, int n, int m, vector<int> u, vector<int> v, vector<int> x, vector<int> y ) { (void)c; using U32 = uint32_t; using U64 = uint64_t; /* * 建立 CSR 邻接表。 */ vector<int> original_degree(n, 0); for (int i = 0; i < n - 1; ++i) { ++original_degree[u[i]]; ++original_degree[v[i]]; } vector<int> offset(n + 1, 0); for (int i = 0; i < n; ++i) { offset[i + 1] = offset[i] + original_degree[i]; } vector<int> cursor = offset; vector<int> to(2 * n - 2); for (int i = 0; i < n - 1; ++i) { to[cursor[u[i]]++] = v[i]; to[cursor[v[i]]++] = u[i]; } vector<int>().swap(cursor); vector<int>().swap(original_degree); vector<int>().swap(u); vector<int>().swap(v); /* * 非递归 DFS,得到 DFS 序。 * * 普通栈 DFS 仍然保证每棵子树在 DFS 序中连续, * 所以原代码的 preorder RMQ LCA 仍然适用。 */ vector<int> original_parent(n, -1); vector<int> original_depth(n, 0); vector<int> dfn(n); vector<int> order(n); vector<int> stack; stack.reserve(n); original_parent[0] = 0; stack.push_back(0); int timer = 0; while (!stack.empty()) { int a = stack.back(); stack.pop_back(); dfn[a] = timer; order[timer++] = a; for (int e = offset[a]; e < offset[a + 1]; ++e) { int b = to[e]; if (b == original_parent[a]) { continue; } original_parent[b] = a; original_depth[b] = original_depth[a] + 1; stack.push_back(b); } } /* * 全部转为 DFS 序编号。 * * parent[i] < i(根除外),这是后面单循环转移的关键。 */ vector<int> parent(n); vector<int> depth(n); vector<U32> degree(n); for (int i = 0; i < n; ++i) { int a = order[i]; parent[i] = dfn[original_parent[a]]; depth[i] = original_depth[a]; degree[i] = static_cast<U32>(offset[a + 1] - offset[a]); rmq[0][i] = parent[i]; } vector<int>().swap(original_parent); vector<int>().swap(original_depth); vector<int>().swap(order); vector<int>().swap(offset); vector<int>().swap(to); /* * f、g、h 分别是相邻三层的点数。 * * 直接从 d = 1 开始: * f = 距离 0 的点数 = 1; * g = 距离 1 的点数 = degree。 */ vector<U32> buffer_a(n, 1); vector<U32> buffer_b = degree; vector<U32> buffer_c(n); U32* f = buffer_a.data(); U32* g = buffer_b.data(); U32* h = buffer_c.data(); /* * count[i] = 当前距离球大小。 * * lhs[i] = * sum_{r <= d} (d + 1 - r) * shell_r[i]。 * * lhs >= n 后,该点已经得到答案,同时 lhs 也充当 processed。 */ vector<U32> count(n, 1); vector<U32> lhs(n, 1); vector<U32> base_numerator(n); vector<U32> base_denominator(n); const U32 N = static_cast<U32>(n); int remaining = n; auto update_answer = [&](int i, U32 distance) { if (lhs[i] >= N) { return; } U32 current_count = count[i] + g[i]; count[i] = current_count; U32 current_lhs = lhs[i] + current_count; lhs[i] = current_lhs; if (current_lhs >= N) { U64 numerator = static_cast<U64>(N) + static_cast<U64>(current_count) * (distance + 1) - current_lhs; U64 divisor = std::gcd( numerator, static_cast<U64>(current_count) ); base_numerator[i] = static_cast<U32>(numerator / divisor); base_denominator[i] = static_cast<U32>(current_count / divisor); --remaining; } }; /* * d = 1 特判。 * * 此时递推式中减去的是 degree,而不是 degree - 1。 * 因为 f[i] 恒为 1,所以直接减 g[i](即原度数)。 */ update_answer(0, 1); h[0] = 0u - g[0]; --degree[0]; for (int i = 1; i < n; ++i) { update_answer(i, 1); int p = parent[i]; /* * 来自父亲的贡献,同时减去回头路径。 */ h[i] = g[p] - g[i]; /* * i 对父亲的贡献。 * parent[i] < i,所以 h[p] 已经初始化。 */ h[p] += g[i]; /* * 此后 degree 数组直接保存原度数 - 1。 */ --degree[i]; } if (remaining != 0) { swap(f, g); swap(g, h); /* * d >= 2 的统一递推: * * shell_{d+1}(i) * = sum_{j adjacent i} shell_d(j) * - (degree(i) - 1) shell_{d-1}(i)。 * * 使用 uint32_t,让中间抵消过程按照模 2^32 * 进行定义良好的运算。 */ for (U32 distance = 2; remaining != 0; ++distance) { update_answer(0, distance); h[0] = 0u - f[0] * degree[0]; for (int i = 1; i < n; ++i) { update_answer(i, distance); int p = parent[i]; h[i] = g[p] - f[i] * degree[i]; h[p] += g[i]; } if (remaining == 0) { break; } swap(f, g); swap(g, h); } } /* * 释放根号预处理中的热数组,再建立 LCA RMQ。 */ vector<U32>().swap(buffer_a); vector<U32>().swap(buffer_b); vector<U32>().swap(buffer_c); vector<U32>().swap(count); vector<U32>().swap(lhs); vector<U32>().swap(degree); /* * 将询问端点直接转为 DFS 序,后面不再需要 dfn。 */ for (int i = 0; i < m; ++i) { x[i] = dfn[x[i]]; y[i] = dfn[y[i]]; } vector<int>().swap(dfn); /* * preorder-parent RMQ。 */ for (int level = 1; (1 << level) <= n; ++level) { int half = 1 << (level - 1); int limit = n - (1 << level) + 1; const int* previous = rmq[level - 1]; int* current = rmq[level]; for (int i = 0; i < limit; ++i) { current[i] = min( previous[i], previous[i + half] ); } } vector<int>().swap(parent); vector<pair<long long, int>> answer; answer.reserve(m); for (int i = 0; i < m; ++i) { int sx = x[i]; int sy = y[i]; int left = min(sx, sy) + 1; int right = max(sx, sy); // 题目保证 x != y,因此 length >= 1。 unsigned length = static_cast<unsigned>(right - left + 1); int level = 31 - __builtin_clz(length); int lca_index = min( rmq[level][left], rmq[level][right - (1 << level) + 1] ); int distance = depth[sx] + depth[sy] - 2 * depth[lca_index]; if ( static_cast<U64>(distance) * base_denominator[sy] >= base_numerator[sy] ) { answer.emplace_back( static_cast<long long>(base_numerator[sy]), static_cast<int>(base_denominator[sy]) ); } else { answer.emplace_back(distance, 1); } } return answer; }

相比原代码的关键差异

根号循环由原来的:

cpp
扫描 n 个点 + 扫描 2(n-1) 个邻接项 + vector<bool> + long long 乘法

变成:

cpp
单次顺序扫描 n 个点 + 每个点一次父亲访问 + 两个 uint32_t 状态更新

另外还删除了:

cpp
vector<int> adj[500000] 第二次邻接表构造 vector<bool> processed vector<long long> wsc new_baseline 递归 DFS

我用随机树对这版与逐个终点 BFS 的暴力进行了小规模对拍;样例输出也是:

text
7 3 1 1 2 1 1 1

在当前运行环境下,合成的 n=5×105n=5\times10^5 链加 m=106m=10^6 询问、-O2 编译,完整计算约为 0.73 秒。不同评测机差别会很大,但这里主要是结构性降常数,不依赖 #pragma 或特定指令集。

이 Q&A 공유하기