双关键字dij


Dijkstra 进阶:双关键字与多维权值最短路

在常规的最短路问题中,我们只需要维护一个 dis 数组。但当题目出现多个维度的代价(如距离、花费、心情、特定物品数量)时,我们需要在优先队列和松弛操作中引入多关键字比较逻辑。

1. 基础多关键字最短路:逃离石楠花大学

来源文件T_594366_逃离石楠花大学.cpp

题面抽象

给定一张有向图,图中的边有距离权值 $w$,部分节点上有特定物品(石楠花)。要求从起点 $s$ 到终点 $t$ 寻找一条最优路径,评判标准如下:

  1. 第一优先级:经过含有该物品的节点数量(cnt)最少。
  2. 第二优先级:在物品数量相同的情况下,路径总距离(w)最短。

核心思路

将单点状态由一维扩展为二维。在标准的 Dijkstra 框架下,维护两个状态数组:val(记录物品数量)和 dis(记录距离)。

  • 状态重载:在优先队列的 Node 结构体中,重载 < 运算符以体现优先级。因为 C++ 的 priority_queue 默认是大根堆,为了实现小根堆的效果,当 cnt != v.cnt 时返回 cnt > v.cnt,否则返回 w > v.w
  • 二维松弛操作
  • 绝对优势:如果新路径的物品数量严格更少(now_cnt < val[v]),则无条件覆盖现有的 val[v]dis[v],并入队。
  • 次级优势:如果物品数量持平(now_cnt == val[v]),则比较距离。若新路径距离更短(now_dist < dis[v]),则更新 dis[v] 并入队。
点击查看代码
int a[N];
vector<pair<int, int>> g[N];

int n, m, k, s, t;
struct Node {
    int x, w, cnt;
    bool operator<(const Node &v) const {
        if (cnt != v.cnt)
            return cnt > v.cnt; // 第一关键字:优先保证花最少
        else
            return w > v.w; // 第二关键字:花一样多的情况下,距离最短
    }
};

void dij() {
    priority_queue<Node> q;
    vector<int> dis(n + 1, 1e18);
    vector<int> val(n + 1, 1e18);
    vector<bool> vis(n + 1, 0);
    q.push({s, 0, a[s]});
    val[s] = a[s];
    dis[s] = 0;
    while (q.size()) {
        auto [u, w, cnt] = q.top();
        q.pop();
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto [v, w] : g[u]) {
            if (vis[v]) continue;
            int now_cnt = cnt + a[v];
            int now_dist = dis[u] + w;
            if (now_cnt < val[v]) {
                dis[v] = now_dist;
                val[v] = now_cnt;
                q.push({v, dis[v], val[v]});
            } else if (now_cnt == val[v]) {
                if (dis[v] > now_dist) {
                    dis[v] = now_dist;
                    val[v] = now_cnt;
                    q.push({v, dis[v], val[v]});
                }
            }
        }
    }
    if (dis[t] == 1e18)
        cout << -1 << endl;
    else
        cout << dis[t] << endl;
}

void solve() {

    cin >> n >> m >> k >> s >> t;
    for (int i = 1; i <= k; i++) {
        int t;
        cin >> t;
        a[t] = 1;
    }
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back({v, w});
    }
    dij();
}

2. 约束截断与最值极化:天梯赛 2025 L3-1

来源文件天梯赛2025L3 - 1.cpp

题面抽象

给定一张无向图,每条道路除了花费 $w$ 之外,还有一个“心情指数” $cnt$。同时给定了预算上限 $b$。多次查询不同的起点 $u$,要求输出:

  1. 在预算 $b$ 范围内,走最便宜的路线能够到达的所有城市。
  2. 在上述可达的城市中,挑选出沿途心情指数总和最高的城市列表。

核心思路

此题在双权值的基础上增加了全局约束截断(剪枝)多目标最值查询。其路径评判标准为:第一优先级是花费 $w$ 最小,第二优先级是心情指数 $cnt$ 最大。

  • 带方向的状态重载:在优先队列中,花费要求越小越好(w > v.w),而心情指数要求越大越好(cnt < v.cnt)。这种一正一反的重载是处理收益与代价混合问题的标准写法。

  • 全局阈值剪枝:在从优先队列取出元素以及进行松弛操作扩展新节点时,如果发现 now_dis > b,则直接 continue 截断,避免扩展无效的超预算状态。

  • 严格的松弛逻辑

  • dis[v] > now_dis 时,说明找到了更便宜的路径,无条件更新 dis[v]mood[v]

  • dis[v] == now_dis 时,必须且仅当 now_cnt > mood[v] 时,才去更新心情指数并重新入队。

  • 后处理统计:跑完单源最短路后,通过遍历所有非起点的节点 $i$,筛选出满足 dis[i] <= b 的节点输出可达集合,同时维护一个 mx = max(mx, mood[i]) 用于第二轮输出心情最高的城市集合。

点击查看代码
const int N = 505;
int a[N];

int n, m, b, k;
struct Node {
    int x, w, cnt;
    bool operator<(const Node &v) const {
        if (w != v.w) return w > v.w;
        return cnt < v.cnt;
    }
};
vector<Node> g[N];
int dis[N];
int mood[N];
bool vis[N];

void dij(int st) {
    priority_queue<Node> q;
    q.push({st, 0, 0});
    // vector<int> dis(n + 1, 1e9);
    // vector<int> mood(n + 1, 0);
    // vector<bool> vis(n + 1);
    for (int i = 1; i <= n; i++) {
        dis[i] = 1e9;
        mood[i] = 0;
        vis[i] = false;
    }
    dis[st] = 0;
    while (q.size()) {
        auto [u, w, cnt] = q.top();
        q.pop();
        if (vis[u]) continue;
        if (w > b) continue;
        vis[u] = 1;

        for (auto [v, w, cc] : g[u]) {
            if (vis[v]) continue;
            int now_dis = dis[u] + w;
            if (now_dis > b) continue;
            int now_cnt = mood[u] + cc;
            if (dis[v] > now_dis) {
                dis[v] = now_dis;
                mood[v] = now_cnt;
                q.push({v, now_dis, now_cnt});
            } else if (dis[v] == now_dis && now_cnt > mood[v]) {
                dis[v] = now_dis;
                mood[v] = now_cnt;
                q.push({v, now_dis, now_cnt});
            }
        }
    }
    bool f = 0;
    int mx = 0;

    for (int i = 1; i <= n; i++) {
        if (i != st && dis[i] <= b) {
            mx = max(mx, mood[i]);
            if (f) cout << ' ';
            cout << i;
            f = 1;
        }
    }

    if (f == 0) {
        cout << "T_T" << endl;
        return;
    }
    cout << endl;
    f = 0;

    for (int i = 1; i <= n; ++i) {

        if (i != st && dis[i] <= b && mood[i] == mx) {
            if (f == 1) cout << ' ';
            cout << i;
            f = 1;
        }
    }
    cout << endl;
}

void solve() {

    cin >> b >> n >> m >> k;
    for (int i = 1; i <= m; i++) {
        int u, v, w, cnt;
        cin >> u >> v >> w >> cnt;
        g[u].push_back({v, w, cnt});
        g[v].push_back({u, w, cnt});
    }
    while (k--) {
        int u;
        cin >> u;
        dij(u);
    }
}

💡 核心总结与避坑指南

  1. 重载运算符的符号一致性:在双关键字 Dijkstra 中,operator< 的书写极易出错。务必明确题目对第二关键字是求最大还是最小。求最小用 >,求最大用 <
  2. 入队条件:只有在发生实质性更新(无论是第一关键字还是第二关键字变优)时,才将新状态 push 进优先队列,否则会导致队列膨胀从而 TLE。
posted @ 2026-05-16 19:18  r_123  阅读(17)  评论(0)    收藏  举报