题解:洛谷 P15804 消息查找
【题目来源】
洛谷:P15804 [GESP202603 八级] 消息查找 - 洛谷
【题目描述】
小 A 的消息记录中有 \(n\) 条消息,依次以 \(1, 2, \dots, n\) 编号。编号小的消息发送时间早于编号大的消息。
一条消息可以引用一条编号小于它的消息,也可以不引用消息。小 A 注意到消息记录里有引用的消息数量不会非常多。消息记录的一个例子是:
- 【消息 1】小 A:有人做了今天的第一题吗?
- 【消息 2】小 A:我第一题 WA 了,可能是什么原因?
- 【消息 3:引用消息 1】小 B:我我我
- 【消息 4:引用消息 2】小 C:我也 WA 了
- 【消息 5:引用消息 2】小 B:是不是没开 long long?
- 【消息 6:引用消息 5】小 A:改了就 AC 了,太厉害了!
对于消息 \(i\) (\(1 \le i \le n\)),小 A 以 \(r_i\) 标记消息 \(i\) 是否有引用,以及所引用的消息编号。如果 \(r_i > 0\),则消息 \(i\) 为引用了消息 \(r_i\);如果 \(r_i = 0\),则消息 \(i\) 没有引用消息。
消息记录里有非常多条消息。为了快速查找所需要的消息,小 A 准备实现一个简单的消息查找工具。消息查找工具任意时刻只能定位恰好一条消息,如果当前位于消息 \(i\) (\(1 < i \le n\)),那么接下来可以选择以下两种操作之一:
- 定位到消息 \(i - 1\);
- 如果消息 \(i\) 引用了消息 \(r_i\),定位到消息 \(r_i\)。
以上操作可以执行任意次(包括零次)。
小 A 有 \(q\) 次询问。在第 \(k\) (\(1 \le k \le q\))次询问中,小 A 给出消息编号 \(x_k, y_k\) (\(y_k < x_k\))。小 A 想知道,如果当前消息查找工具位于 \(x_k\),至少需要多少次操作才能定位到消息 \(y_k\)。
【输入】
第一行,两个正整数 \(n, q\),分别表示消息条数与询问次数。
第二行,\(n\) 个非负整数 \(r_1, r_2, \dots, r_n\),表示消息的引用关系,具体含义见题目描述。
接下来 \(q\) 行中的第 \(k\) 行(\(1 \le k \le q\))包含两个正整数 \(x_k, y_k\),表示一次询问。
保证至多只有 1000 条引用消息。
【输出】
输出 \(q\) 行,每行一个整数,表示将界面从消息 \(x_k\) 切换到消息 \(y_k\) 所需的最少操作次数。
【输入样例】
6 3
0 0 1 2 2 5
4 1
6 2
6 3
【输出样例】
2
2
3
【核心思想】
-
问题分析:给定 \(n\) 条消息,每条消息可以向前移动到 \(i-1\) 或通过引用跳转到 \(r_i\)(\(r_i < i\))。有 \(q\) 次询问,求从 \(x_k\) 到 \(y_k\)(\(y_k < x_k\))的最少操作次数。由于引用消息数量极少(\(\leq 1000\)),这是一个图论建模 + 关键点提取 + 多源最短路问题,核心在于将稀疏的引用关系转化为图上的最短路径问题。
-
算法选择:
- 关键点提取:将所有引用消息的起点和终点作为关键点,普通移动(\(i \to i-1\))在关键点之间可通过距离差计算
- 多源 Dijkstra:对每对关键点之间运行 Dijkstra 预处理最短路,查询时组合普通移动和关键点间最短路
-
关键步骤:
- 关键点收集:遍历所有消息,若 \(r_i > 0\),将 \(i\) 和 \(r_i\) 加入关键点集合 \(a\),并记录引用关系 \(g[r_i] \to i\)
- 排序去重:对关键点排序并去重,建立值到索引的映射
- 建立图模型:
- 引用边:从关键点 \(u\) 可通过引用以代价 \(1\) 到达目标关键点 \(v\)
- 普通边:相邻关键点 \(a_i\) 和 \(a_{i+1}\) 之间以代价 \(a_{i+1} - a_i\) 向右移动
- 多源最短路:对每个关键点 \(i\) 运行 \(dijkstra(i)\),计算 \(d[i][j]\)(关键点 \(i\) 到 \(j\) 的最短距离)
- 查询处理(询问 \(x \to y\),\(y < x\)):
- 找到 \(\geq x\) 的最小关键点 \(a_{ps}\) 和 \(\leq y\) 的最大关键点 \(a_{pe}\)
- 若区间内无关键点:答案为 \(x - y\)(纯普通移动)
- 否则:\(ans = (a_{ps} - x) + d[ps][pe] + (y - a_{pe})\),即"从 \(x\) 到最近关键点 + 关键点间最短路 + 从关键点到 \(y\)"
-
时间/空间复杂度:
- 时间复杂度:\(O(K^2 \log K + K^2 + q \log K)\),其中 \(K \leq 2000\) 为关键点数量,多源 Dijkstra 为 \(O(K^2 \log K)\),查询为 \(O(\log K)\)
- 空间复杂度:\(O(K^2 + n)\),距离矩阵 \(d\) 为 \(O(K^2)\)
-
关键点提取与多源最短路的核心思想:
- 稀疏性利用:题目保证引用消息 \(\leq 1000\),因此关键点数量 \(K \leq 2000\),远小于 \(n\)(可能达到 \(10^5\)),将问题规模从 \(O(n)\) 降到 \(O(K)\)
- 普通移动的线性处理:非关键点之间的移动是纯线性的(每次 \(-1\)),只需计算关键点间的距离差,无需在图中显式建模
- 引用边的图建模:将引用关系作为有向边(代价为 \(1\))加入图,配合普通移动的"相邻关键点边"(代价为坐标差),构建混合图
- 多源预处理:由于查询次数 \(q\) 可能很大,预处理所有关键点对之间的最短路,将每次查询降至 \(O(\log K)\)
- 查询组合策略:将路径拆分为三段——普通移动到关键点、关键点间最短路、关键点到目标普通移动,利用预计算结果快速回答
- 适用于稀疏特殊边 + 密集普通边的混合图最短路问题,通过关键点提取将大规模图压缩为小规模图处理
【解题思路】

【算法标签】
提高+ #Dijkstra
【代码详解】
// 40分版本
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII; // 用于队列存储(当前位置, 步数)
const int N = 100005;
int n, q; // n: 建筑数量, q: 查询次数
int r[N]; // r[i]: 从建筑i可以直达的建筑编号
bool vis[N]; // 访问标记数组
// 广度优先搜索,计算从x到y的最少步数
int bfs(int x, int y)
{
memset(vis, 0, sizeof(vis)); // 重置访问标记
queue<PII> q; // 创建队列
q.push({x, 0}); // 起点入队,步数为0
while (!q.empty())
{
int x = q.front().first; // 当前位置
int step = q.front().second; // 当前已走步数
q.pop();
if (vis[x]) // 如果已访问过,跳过
{
continue;
}
vis[x] = 1; // 标记为已访问
if (x == y) // 如果到达终点
{
return step; // 返回步数
}
// 选择1: 向左走一步
q.push({x - 1, step + 1});
// 选择2: 如果当前位置有特殊通道且能直接到达y或更远
if (r[x] && r[x] >= y)
{
q.push({r[x], step + 1}); // 通过特殊通道跳转
}
}
return -1; // 理论上应该不会执行到这里
}
int main()
{
cin >> n >> q; // 输入建筑数量和查询次数
for (int i = 1; i <= n; i++)
{
cin >> r[i]; // 输入每个建筑的特殊通道
}
while (q--) // 处理每个查询
{
int x, y;
cin >> x >> y; // 输入起点和终点
cout << bfs(x, y) << endl; // 输出最少步数
}
return 0; // 程序正常结束
}
// AC代码
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII; // 用于优先队列,first存储距离,second存储节点编号
const int N = 100005;
int n, q, ans, cnt; // n: 原始位置数量, q: 查询次数, ans: 临时答案, cnt: 关键点数量
int a[N]; // 存储所有关键点坐标
int d[2005][2005]; // 距离矩阵,d[i][j]表示关键点i到关键点j的最短距离
vector<PII> g[N]; // 邻接表,g[x]存储从值x能到达的位置及其在关键点数组中的索引
priority_queue<PII, vector<PII>, greater<PII> > heap; // 小顶堆,用于Dijkstra
bool st[2005]; // Dijkstra中的已确定最短路径标记数组
// Dijkstra算法,计算从关键点idx到其他所有关键点的最短距离
void dijkstra(int idx)
{
memset(st, 0, sizeof(st)); // 重置已确定标记
heap.push({0, idx}); // 起点入队,距离为0
while (!heap.empty())
{
auto t = heap.top();
heap.pop();
int u = t.second; // 当前关键点编号
int distance = t.first; // 当前距离
if (st[u]) // 如果已确定最短路径,跳过
{
continue;
}
st[u] = true; // 标记为已确定
// 情况1:通过传送门移动
// 从关键点u(值为a[u])可以通过传送门到达其他位置
for (int i = 0; i < g[a[u]].size(); i++)
{
int v = g[a[u]][i].second; // 目标关键点编号
if (d[idx][v] > distance + 1) // 如果可以更新最短距离
{
d[idx][v] = distance + 1; // 更新距离
heap.push({d[idx][v], v}); // 入队
}
}
// 情况2:直接在数轴上向右移动到下一个关键点
if (u < cnt && d[idx][u + 1] > distance + (a[u + 1] - a[u]))
{
d[idx][u + 1] = distance + (a[u + 1] - a[u]); // 更新距离
heap.push({d[idx][u + 1], u + 1}); // 入队
}
}
}
int main()
{
cin >> n >> q; // 输入位置数量和查询次数
// 第一阶段:收集所有关键点
for (int i = 1; i <= n; i++)
{
int x;
cin >> x; // 输入第i个位置的值
if (x) // 如果x不为0,表示有传送门
{
g[x].push_back({i, 0}); // 记录从值x能传送到位置i
a[++cnt] = x; // 将传送门的起始值加入关键点数组
a[++cnt] = i; // 将传送门的目标位置加入关键点数组
}
}
// 对关键点排序并去重
sort(a + 1, a + cnt + 1);
cnt = unique(a + 1, a + cnt + 1) - a - 1; // 更新实际关键点数量
// 第二阶段:建立关键点索引映射
for (int i = 1; i <= cnt; i++)
{
int u = a[i]; // 第i个关键点的值
for (int j = 0; j < g[u].size(); j++)
{
int v = g[u][j].first; // 从u能传送到位置v
int p = lower_bound(a + 1, a + cnt + 1, v) - a; // 找到v在关键点数组中的索引
g[u][j].second = p; // 存储关键点编号
}
}
// 第三阶段:计算所有关键点之间的最短距离
memset(d, 0x3f, sizeof(d)); // 初始化为无穷大
for (int i = 1; i <= cnt; i++)
{
d[i][i] = 0; // 自己到自己的距离为0
}
for (int i = 1; i <= cnt; i++)
{
dijkstra(i); // 计算以关键点i为起点的最短路径
}
// 第四阶段:处理查询
while (q--)
{
int x, y; // 查询从x到y
cin >> y >> x; // 注意输入顺序是y,x,但实际上是x到y
// 找到大于等于x的最小关键点
int ps = lower_bound(a + 1, a + cnt + 1, x) - a;
if (ps >= cnt) // 如果x大于所有关键点
{
cout << y - x << endl; // 只能通过普通移动
continue;
}
// 找到小于等于y的最大关键点
int pe = upper_bound(a + 1, a + cnt + 1, y) - a - 1;
if (pe <= 1 || pe < ps) // 如果没有关键点在区间内
{
cout << y - x << endl; // 只能通过普通移动
continue;
}
// 计算最短距离:
// 1. 从x到最近的关键点a[ps]:普通移动
// 2. 在关键点之间的最短距离d[ps][pe]
// 3. 从关键点a[pe]到y:普通移动
ans = (a[ps] - x) + d[ps][pe] + (y - a[pe]);
cout << ans << endl;
}
return 0; // 程序正常结束
}
【运行结果】
6 3
0 0 1 2 2 5
4 1
2
6 2
2
6 3
3
浙公网安备 33010602011771号