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\)。
-
分别对 \(x\) 和 \(y\) 执行
findu,得到根 \(rx\) 和 \(ry\),此时:- \(P[x] = P[rx] + pot[x]\)
- \(P[y] = P[ry] + pot[y]\)
-
若 \(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]; -
若 \(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)\):
- 分别执行
findu(X)和findu(Y),得到根 \(rx, ry\),并更新pot。 - 若 \(rx \neq ry\),输出
nan。 - 若 \(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;
}

浙公网安备 33010602011771号