Solution -「APIO 2018」「洛谷 P4630」鐵人兩項
\(\mathcal{Description}\)
Link.
給定一個 \(n\) 個點 \(m\) 條邊的無向圖(不保證聯通),求有序三元點對 \((s,c,f)\) 的個數,滿足 \(s,c,f\) 互不相同,且存在一條從 \(s\) 到 \(c\) 再到 \(f\) 的簡單路徑。
\(n\le10^5\),\(m\le2\times10^5\)。
\(\mathcal{Solution}\)
首先考慮這樣一個問題,若 \(s,c,f\) 在同一點雙中,是否一定滿足條件。
答案是肯定的,這裡介紹一種 CF 某題解上提到的證明。
性質證明
構造網路,將點雙中的每個點拆點。對於點 \(u\)
,連線 \((u_i,u_o,1)\)。對於原圖中的邊 \((u,v)\),連線 \((u_o,v_i,1)\)。對於選定的 \(s,c,f\),連線 \((S,c_i,2),(s_o,T,1),(f_o,T,1)\),接下來只需要證明該網路的最大流為 \(2\)。那麼只需要考慮最小割 \(C\)。顯然 \(C\le2\),則只需證 \(C>1\)。
首先,割掉 \((S,c_i,2)\) 或同時割掉 \((s_o,T,1),(f_o,T,1)\) 都不能使 \(C\le1\)。接下來考慮其它型別的邊。
割掉 \((u_i,u_o,1)\),相當於刪除 \(u\) 點。因為這是一個點雙,所以 \(c\)
與 \(s,f\) 仍然連通,不滿足。割掉 \((u_o,v_i,1)\),相當於刪除 \((u,v)\) 邊。顯然其對於連通性的影響不大於刪除 \(u\) 點或 \(v\) 點,由上種情況,亦不滿足。
到此,有 \(C>1\)。由因為 \(C\le2\) 且 \(C\in\mathbb N\),所以 \(C=2\)。那麼這樣的路徑一定存在,證畢。
那麼如果固定 \(s,f\),合法的 \(c\) 就可以在圓方樹 \(s\) 到 \(f\) 路徑上的所有圓點和所有方點所代表的圓點(除去 \(s,f\))。這是因為 \(c\) 取在任意點雙內部,由我們的結論,都一定可以從某點進入點雙,經過 \(c\)
但簡單的計算會導致重複——一個圓點對多個方點有貢獻。
舉個例子,對於 \(u-w-v\),\(w\) 是圓點,\(u,v\) 是方點,如果我們單純地用點雙大小作為方點的權值,\(w\) 就會在 \(u\) 和 \(v\) 中分別計算一次。
解決辦法很巧妙:將圓點的權值設為 \(-1\)。考慮 \(s\) 到 \(f\) 的路徑必然是”圓-方-圓-……-圓-方-圓“,兩個端點的 \(-1\),去除了 \(s\) 和 \(f\) 的貢獻,中間的 \(-1\) 去除了在左右的”方“中重複的貢獻。那麼,合法的 \(c\) 的數量就是 \(s\) 到 \(f\) 的樹上路徑權值之和。
於是,相當於求樹上點對的路徑權值和。反過來,固定 \(c\),維護子樹資訊求出 \((s,f)\) 的方案數,計算 \(c\) 的貢獻即可。
\(\mathcal{Code}\)
#include <cstdio>
const int MAXN = 1e5, MAXM = 2e5;
int n, m, q, snode;
int dfc, top, dfn[MAXN + 5], low[MAXN + 5], stk[MAXN + 5];
int siz[MAXN * 2 + 5], val[MAXN * 2 + 5];
long long ans;
struct Graph {
int ecnt, head[MAXN * 2 + 5], to[MAXM * 2 + 5], nxt[MAXM * 2 + 5];
inline void link ( const int s, const int t ) {
to[++ ecnt] = t, nxt[ecnt] = head[s];
head[s] = ecnt;
}
inline void add ( const int u, const int v ) {
link ( u, v ), link ( v, u );
}
} src, tre;
inline bool chkmin ( int& a, const int b ) { return b < a ? a = b, true : false; }
inline void Tarjan ( const int u, const int f ) {
dfn[u] = low[u] = ++ dfc, val[stk[++ top] = u] = -1;
for ( int i = src.head[u], v; i; i = src.nxt[i] ) {
if ( ( v = src.to[i] ) == f ) continue;
if ( ! dfn[v] ) {
Tarjan ( v, u ), chkmin ( low[u], low[v] );
if ( low[v] >= dfn[u] ) {
tre.add ( u, ++ snode ), val[snode] = 1;
do tre.add ( snode, stk[top] ), ++ val[snode]; while ( stk[top --] ^ v );
}
} else chkmin ( low[u], dfn[v] );
}
}
inline void calc ( const int u, const int f ) {
siz[u] = u <= n;
for ( int i = tre.head[u], v; i; i = tre.nxt[i] ) {
if ( ( v = tre.to[i] ) ^ f ) {
calc ( v, u );
ans += 1ll * val[u] * siz[u] * siz[v];
siz[u] += siz[v];
}
}
ans += 1ll * val[u] * siz[u] * ( dfc - siz[u] );
}
int main () {
scanf ( "%d %d", &n, &m ), snode = n;
for ( int i = 1, u, v; i <= m; ++ i ) {
scanf ( "%d %d", &u, &v );
src.add ( u, v );
}
for ( int i = 1; i <= n; ++ i ) {
if ( ! dfn[i] ) {
dfc = top = 0;
Tarjan ( i, 0 );
calc ( i, 0 );
}
}
printf ( "%lld\n", ans << 1 );
return 0;
}