题解:洛谷 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

【核心思想】

  1. 问题分析:给定 \(n\) 条消息,每条消息可以向前移动到 \(i-1\) 或通过引用跳转到 \(r_i\)\(r_i < i\))。有 \(q\) 次询问,求从 \(x_k\)\(y_k\)\(y_k < x_k\))的最少操作次数。由于引用消息数量极少(\(\leq 1000\)),这是一个图论建模 + 关键点提取 + 多源最短路问题,核心在于将稀疏的引用关系转化为图上的最短路径问题。

  2. 算法选择

    • 关键点提取:将所有引用消息的起点和终点作为关键点,普通移动(\(i \to i-1\))在关键点之间可通过距离差计算
    • 多源 Dijkstra:对每对关键点之间运行 Dijkstra 预处理最短路,查询时组合普通移动和关键点间最短路
  3. 关键步骤

    • 关键点收集:遍历所有消息,若 \(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\)"
  4. 时间/空间复杂度

    • 时间复杂度:\(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)\)
  5. 关键点提取与多源最短路的核心思想

    • 稀疏性利用:题目保证引用消息 \(\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
posted @ 2026-07-09 23:26  团爸讲算法  阅读(6)  评论(0)    收藏  举报