题目
给定一个 n n n 个点 m m m 条边的无向带权联通图,没有重边、自环,且每两个点之间都有至多两条路径,它们的边集不相交。求:
∑ 1 ≤ s < e ≤ n s ⊕ e ⊕ m a x F l o w ( s , e ) \sum_{1 \le s \lt e \le n} s \oplus e \oplus maxFlow(s, e) 1≤s<e≤n∑s⊕e⊕maxFlow(s,e)
其中 ⊕ \oplus ⊕ 表示按位异或。 m a x F l o w ( s , e ) maxFlow(s, e) maxFlow(s,e) 表示 s s s 点到 e e e 点的最大流。
T T T 组数据, T ≤ 100 , n ≤ 1 0 5 , ∑ n ≤ 1 0 6 , w i ≤ 1 0 9 T \le 100, n \le 10^5, \sum n \le 10^6, w_i \le 10^9 T≤100,n≤105,∑n≤106,wi≤109。
分析
仙人掌图
定义
仙人掌是不包含自环的,一条边最多属于一个简单环的无向连通图。
图
1
1
1 是仙人掌:
图
2
2
2 不是仙人掌,因为边
(
B
,
G
)
(\text{B}, \text{G})
(B,G) 属于两个简单环:
特别地,树也是仙人掌:
另外,每个联通块都是仙人掌的无向图被称为沙漠。
性质
- 结点数为 n n n 的仙人掌边数最少为 n − 1 n - 1 n−1(树),最多为 3 2 ( n − 1 ) \frac{3}{2}(n - 1) 23(n−1)。读者可以自行构造。
- 仙人掌的任意两个点之间都只有有至多两条路径,它们的边集不相交。你可以把仙人掌看作是一堆环和一堆散边组合起来,使得两个环最多只有一个公共点。我们称这些散边为树边。
- 仙人掌两个点的最短路为路径上的每条树边的边权与每个环左半边和右半边的边权和的最小值的和。类似地,仙人掌两个点的最小割为路径上的每条树边的权值与每个环左半边的最小权值加上它右半边的最小权值的和的最小值。
本题分析
问题转化
根据 最大流 - 最小割定理 \text{最大流 - 最小割定理} 最大流 - 最小割定理,题目等价于求:
∑ 1 ≤ s < e ≤ n s ⊕ e ⊕ m i n C u t ( s , e ) \sum_{1 \le s \lt e \le n} s \oplus e \oplus minCut(s, e) 1≤s<e≤n∑s⊕e⊕minCut(s,e)
其中 m i n C u t ( s , e ) minCut(s, e) minCut(s,e) 表示 s s s 点到 e e e 点的最小割。
对于仙人掌上的最小割在环上的情况,我们考虑如何把它转化为在链上的情况。发现如果把一个环切成两半,那么两半中的边权最小值中至少有一个等于整个环上的边权最小值。那么,我们把整个环上的边权最小的那条边去掉,并把其他边的边权加上这个最小值。这样,环就变成了一个链,两个点之间的最小割就转化成了链上的最小值,于是我们就可以把它和树边的情况一样考虑。
树上问题
我们已经把仙人掌变成了树,现在要计算的就是:
∑ 1 ≤ s < e ≤ n s ⊕ e ⊕ m i n V a l u e ( s , e ) \sum_{1 \le s \lt e \le n} s \oplus e \oplus minValue(s, e) 1≤s<e≤n∑s⊕e⊕minValue(s,e)
其中 m i n V a l u e ( s , e ) minValue(s, e) minValue(s,e) 表示 s s s 点到 e e e 点路径上的最小边权。
现将所有边储存起来,从大到小排序,然后对于每条边,合并两个端点所在的联通块,并在同时计算贡献即可。由于异或的性质,我们只需对于每一位分别考虑,对于每个联通块维护它包含的所有点的编号中的每一个 bit \text{bit} bit 有几个 0 0 0,几个 1 1 1 就可以了。时间复杂度 Θ ( n log 2 2 n ) \Theta(n \log_2^2 n) Θ(nlog22n)
实现
找环
如何找到仙人掌上的所有树边和环呢?我们先跑一遍 tarjan \text{tarjan} tarjan 求出原图中的点双联通分量,于是对于每条树边,它一定是图的一个桥。对于每个环的最高点 u u u,一定存在一个相邻的点 v v v,使得 dfn [ v ] > dfn [ u ] \text{dfn}[v] > \text{dfn}[u] dfn[v]>dfn[u],并且 u u u 没有直接 dfs \text{dfs} dfs 到 v v v。
细节
注意本题的最大答案为
1
0
10
2
×
2
⋅
1
0
9
=
1
0
19
\frac{10^{10}}{2} \times 2 \cdot 10^9 = 10^{19}
21010×2⋅109=1019,而 long long
类型能够储存的最大整数只有大约
9.2233
×
1
0
18
9.2233 \times 10^{18}
9.2233×1018,所以我们应该使用 unsigned long long
类型储存答案。代码采用启发式合并的并查集来维护每个联通块的信息,静态仙人掌图问题的通用处理方法包含在代码中。
代码
#include <cstdio>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
const int maxn = 1e5 + 5, maxm = 3e5 + 5, logn = 30 + 5;
int T, n, m, fa[maxn], sz[maxn], cnt[maxn][logn][2];
int tot, ter[maxm], wei[maxm], nxt[maxm], lnk[maxn];
int cur, dfn[maxn], low[maxn], par[maxn], ret[maxn], temp[maxn];
struct edge {
int u, v, w;
edge(int u = 0, int v = 0, int w = 0): u(u), v(v), w(w) {}
friend bool operator<(edge a, edge b) {
return a.w > b.w;
}
} a[maxn];
void addEdge(int u, int v, int w) {
nxt[++tot] = lnk[u];
lnk[u] = tot;
ter[tot] = v;
wei[tot] = w;
}
int another(int e) {
return ((e - 1) ^ 1) + 1;
}
void circle(int u, int v, int i) {
int cnt = 0, x = v;
temp[++cnt] = i;
while (x != u) {
temp[++cnt] = ret[x];
x = par[x];
}
int id = 1, mn = wei[temp[1]];
for (int i = 1; i <= cnt; i++) {
if (wei[temp[i]] < mn) {
mn = wei[temp[i]];
id = i;
}
}
for (int i = 1; i <= cnt; i++) {
if (i != id) {
a[++m] = edge(ter[temp[i]], ter[another(temp[i])], wei[temp[i]] + wei[temp[id]]);
}
}
}
void tarjan(int u, int p) {
dfn[u] = low[u] = ++cur;
for (int i = lnk[u], v; i; i = nxt[i]) {
v = ter[i];
if (v == p) continue;
if (dfn[v] == 0) {
par[v] = u;
ret[v] = i;
tarjan(v, u);
low[u] = min(low[u], low[v]);
} else {
low[u] = min(low[u], dfn[v]);
}
}
for (int i = lnk[u], v, w; i; i = nxt[i]) {
v = ter[i], w = wei[i];
if (low[v] > dfn[u]) {
a[++m] = edge(u, v, w);
} else if (par[v] != u && dfn[v] > dfn[u]) {
circle(u, v, i);
}
}
}
int find(int x) {
return fa[x] == x ? x : find(fa[x]);
}
ull calc(int x, int y, int w) {
ull res = 0;
for (int i = 0; i <= 30; i++) {
int b = ~w >> i & 1;
for (int j = 0; j < 2; j++) {
res += (1ull << i) * cnt[x][i][j] * cnt[y][i][j ^ b];
}
}
return res;
}
void merge(int x, int y) {
if (sz[x] < sz[y]) swap(x, y);
for (int i = 0; i <= 30; i++) {
for (int j = 0; j < 2; j++) {
cnt[x][i][j] += cnt[y][i][j];
cnt[y][i][j] = 0;
}
}
sz[x] += sz[y], sz[y] = 0;
fa[y] = x;
}
int main() {
scanf("%d", &T);
while (T--) {
scanf("%d %d", &n, &m);
tot = 0;
for (int i = 1; i <= n; i++) {
lnk[i] = 0;
}
for (int i = 1, u, v, w; i <= m; i++) {
scanf("%d %d %d", &u, &v, &w);
addEdge(u, v, w), addEdge(v, u, w);
}
m = cur = 0;
for (int i = 1; i <= n; i++) {
dfn[i] = low[i] = par[i] = ret[i] = 0;
}
tarjan(1, 0);
sort(a + 1, a + m + 1);
for (int i = 1; i <= n; i++) {
fa[i] = i, sz[i] = 1;
for (int j = 0; j <= 30; j++) {
cnt[i][j][i >> j & 1] = 1;
cnt[i][j][~i >> j & 1] = 0;
}
}
ull ans = 0;
for (int i = 1; i <= m; i++) {
int x = find(a[i].u), y = find(a[i].v);
ans += calc(x, y, a[i].w);
merge(x, y);
}
printf("%llu\n", ans);
}
return 0;
}