1. 程式人生 > 實用技巧 >Solution -「APIO 2018」「洛谷 P4630」鐵人兩項

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;
}