Pay or Receive

Pay or Receive

带权并查集,启发式算法

问题简述

有 \(N\) 个城镇和 \(M\) 条道路。道路 \(i\) 连接 \(A_i\) 和 \(B_i\),从 \(A_i\) 到 \(B_i\) 分数增加 \(C_i\),从 \(B_i\) 到 \(A_i\) 分数减少 \(C_i\)。分数可以为负。

有 \(Q\) 个询问,每个询问给出 \(X_i, Y_i\),从 \(X_i\) 出发初始分数为 \(0\),求到达 \(Y_i\) 时的最大分数。

  • 若无法到达,输出 nan。
  • 若可以得到任意大的分数,输出 inf。

核心思路

每条无向边可以看作两条有向边:\(A_i \to B_i\) 权值为 \(+C_i\),\(B_i \to A_i\) 权值为 \(-C_i\)。

如果图中所有环的总权值都为 \(0\),那么可以给每个点定义一个势能 \(P[v]\),使得沿任意路径从 \(X\) 到 \(Y\) 的分数变化都等于 \(P[Y] - P[X]\)。此时最大分数就是 \(P[Y] - P[X]\)。

如果某个环的总权值不为 \(0\),那么沿着使分数增加的方向绕这个环走,就可以无限增加分数,因此答案为 inf。

带权并查集可以高效维护这种势能关系,并检测非零环。

带权并查集

定义

每个点 \(v\) 维护:

  • pa[v]:并查集中的父节点。
  • pot[v] = P[v] - P[pa[v]]:节点 \(v\) 相对于其父节点的势能差。
  • status[root]:该连通分量的状态,0 表示正常,1 表示存在非零环(答案为 inf)。

经过路径压缩后,pa[v] 变为根,pot[v] 变为 \(P[v] - P[root]\)。

find 操作

路径压缩时累加势能差:

int findu(int x) {
    if (pa[x] == x) return x;
    int p = pa[x];
    int r = findu(p);
    pot[x] += pot[p];
    pa[x] = r;
    return r;
}

执行后,pot[x] = P[x] - P[root]。

unite 操作

对于边 \(x \to y\),权值为 \(w\),表示 \(P[y] - P[x] = w\)。

  1. 分别对 \(x\) 和 \(y\) 执行 findu,得到根 \(rx\) 和 \(ry\),此时:

    • \(P[x] = P[rx] + pot[x]\)
    • \(P[y] = P[ry] + pot[y]\)
  2. 若 \(rx \neq ry\),合并两个集合。假设让 pa[rx] = ry,则需要计算 pot[rx] = P[rx] - P[ry]。
    由 \(P[y] - P[x] = w\) 展开:

    \[(P[ry] + pot[y]) - (P[rx] + pot[x]) = w \]

    \[P[ry] - P[rx] = w + pot[x] - pot[y] \]

    \[P[rx] - P[ry] = pot[y] - pot[x] - w \]

    所以:

    pa[rx] = ry;
    pot[rx] = pot[y] - pot[x] - w;
    status[ry] |= status[rx];
    
  3. 若 \(rx == ry\),说明 \(x\) 和 \(y\) 已在同一连通分量。此时必须满足 \(P[y] - P[x] = w\),即:

    \[pot[y] - pot[x] = w \]

    若不相等,说明存在非零环,将整个连通分量标记为 inf:

    if (pot[y] - pot[x] != w) status[ry] = 1;
    

查询

对于询问 \((X, Y)\):

  1. 分别执行 findu(X) 和 findu(Y),得到根 \(rx, ry\),并更新 pot。
  2. 若 \(rx \neq ry\),输出 nan。
  3. 若 \(rx == ry\):
    • 若 status[ry] == 1,输出 inf。
    • 否则,从 \(X\) 到 \(Y\) 的最大分数为 \(P[Y] - P[X] = pot[Y] - pot[X]\),输出该值。

正确性说明

  • 无环(或环权值为 \(0\))时,任意两点间的分数变化唯一确定,等于 \(P[Y] - P[X]\)。并查集通过维护到根的势能差,保证查询时 pot[Y] - pot[X] 正确。
  • 若存在非零环,则沿使分数增加的方向绕环可无限增大分数,整个连通分量答案为 inf。并查集在合并或同集合检查时发现矛盾,即标记 inf。
  • 并查集的树结构只是维护势能差的工具,不代表实际路径。所有节点的势能差全局自洽。

复杂度

  • 并查集操作近似 \(O(\alpha(N))\)。
  • 总时间复杂度 \(O((M+Q)\alpha(N))\),空间复杂度 \(O(N)\)。
  • 可以轻松通过 \(N, M, Q \le 10^5\)。

代码实现

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 1e9 + 7;

struct dsu {
    int n;
    vector<int> pa, status;
    vector<ll> pot;
    dsu(int _n) {init(_n);}
    void init(int _n) {
        n = _n;
        pa.assign(n + 1, 0);
        status.assign(n + 1, 0); //0可行,1 inf
        pot.assign(n + 1, 0LL);
        iota(pa.begin(), pa.end(), 0);
    }

    int findu(int x) {
        if(pa[x] == x) return x;
        int tim = findu(pa[x]);
        pot[x] += pot[pa[x]]; pa[x] = tim;
        return pa[x];
    }

    void unite(int x, int y, ll w) {
        int rx = findu(x), ry = findu(y);
        if(rx != ry) {
            pot[rx] = pot[y] - pot[x] + w; pa[rx] = ry;
            status[ry] = (status[ry] | status[rx]);
        }
        else {
            if(pot[x] - pot[y] != w) status[ry] = 1;
        }
    }
};

void solve() {
    int n, m, q; cin >> n >> m >> q;
    dsu dsf(n);
    for(int i = 1; i <= m; i ++) {
        int a, b, c; cin >> a >> b >> c;
        dsf.unite(a, b, c);
    }

    while(q --) {
        int x, y; cin >> x >> y;
        int rx = dsf.findu(x), ry = dsf.findu(y);
        if(rx != ry) cout << "nan\n";
        else {
            if(dsf.status[ry] == 1) cout << "inf\n";
            else cout << dsf.pot[x] - dsf.pot[y] << "\n";
        }
    }
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    int _ = 1; //cin >> _;
    while(_--) solve();
    return 0;
}
posted @ 2026-10-11 21:07  thenrify  阅读(11)  评论(0)    收藏  举报