1. 程式人生 > 其它 >[loj3462] [luogu7323] [WC2021] 括號路徑 - 圖論 - 等價關係 - 線段樹合併 - 啟發式合併

[loj3462] [luogu7323] [WC2021] 括號路徑 - 圖論 - 等價關係 - 線段樹合併 - 啟發式合併

技術標籤:雜題資料結構圖論

傳送門:https://www.luogu.com.cn/problem/P7324

好久沒寫題解了,最近放假閒得無聊,寫了寫這兩天WC的題,順手來一發題解。

題目大意:給定圖,每條邊上正反方向分別標有某種括號的左括號和右括號,問有多少點對之間存在能組成合法括號序列的路徑。

這道題的關鍵是:這個“存在合法括號序列路徑”到底有什麼性質?

如果我們在V^2上建立關係R<u,v> \in R當且僅當點u到點v存在合法括號序列路徑。方便起見這裡姑且把u<v的限制去掉。

那麼我們驚奇地發現,R居然是個等價關係:

(1)自反性:自己到自己是空串,顯然是合法的;

(2)對稱性:如果uv存在合法路徑P,那麼考慮其翻轉之後的路徑P^'

,其括號序列為P的括號序列翻轉後左右括號顛倒,容易證明也是合法的;

(3)傳遞性:如果uv存在合法路徑P_1vw存在合法路徑P_2,則考慮兩條路徑拼接而成的路徑P_1P_2,容易證明也是合法的。

發現了這一點,我們就可以嘗試求出所有的等價類,然後所求答案就是對於所有等價類S\subseteq V\frac{1}{2}|S|*(|S|-1)的和。

怎麼求出等價類呢?

我們先從最簡單的非空合法序列“()”看起,假設點u到點v有一條“(”的邊,點v到點w有一條“)”的邊。定睛一看,其實就是點v向外有兩條“)”的邊。

因此,我們只保留所有的右括號邊,這樣如果某個點向外有兩條相同種類括號的邊,就可以把它們的終點合併。實際操作中可以看作用一個新點替代了原先的兩個點,並繼承了原先兩個點的所有出入邊。

如果又有點p到點u有“{​

\{​”的邊,點v到點q有“\}”的邊,則在上述合併操作後原先pq的合法路徑“\{()\}”就會變成“\{\}”,相當於消去了最內層括號。

如果某兩點間存在一條合法路徑,我們總能通過不斷消去最內層括號的方式把整個括號序列消空,這個事實保證了上述演算法的正確性。

現在需要做的,就是用資料結構維護這個合併過程。

每個點要維護若干個集合,分別儲存不同型別的邊。然後一旦發現某個集合大小\geq 2,就把這個集合裡所有邊的終點合併。

這當然是老生常談的啟發式合併,考慮到邊的種數很多,可以拿map存每一種邊的索引,或者在外面套一個線段樹合併。

唯一需要注意的程式碼細節是在合併的時候有可能將正在處理的集合又拿去和別的集合合併,或者已經被處理過的集合又因為合併上了新的點需要再次處理。

時間複雜度O(n\log n)

程式碼如下:

#include<bits/stdc++.h>
#define gc getchar()
#define pc putchar
#define li long long
#define pb push_back
#define mp make_pair
#define md int mid = l + r >> 1
#define ln ls[q],l,mid
#define rn rs[q],mid + 1,r
using namespace std;
inline li read(){
	li x = 0;
	int y = 0,c = gc;
	while(c < '0' || c > '9') y = c,c = gc;
	while(c >= '0' && c <= '9') x = x * 10 + c - '0',c = gc;
	return y == '-' ? -x : x;
}
inline void prt(li x){
	if(x >= 10) prt(x / 10);
	pc(x % 10 + '0');
}
inline void print(li x){
	if(x < 0) pc('-'),x = -x;
	prt(x);
}
int n,m,k;
int vis[600010];
int f[300010],sz[300010];
int que[2000010],h,t,cnt,tot;
vector<int> e[600010];
int rt[300010],ls[13000010],rs[13000010],dy[13000010];
inline int getf(int x){
	return f[x] == x ? x : f[x] = getf(f[x]);
}
inline void ins(int &q,int l,int r,int x,int y){
	if(!q) q = ++cnt;
	if(l == r){
		if(!dy[q]) dy[q] = ++tot;
		e[dy[q]].pb(y);
		if(e[dy[q]].size() == 2) que[++t] = dy[q],vis[dy[q]] = 1;
		return;
	}
	md;
	if(mid >= x) ins(ln,x,y);
	else ins(rn,x,y);
}
int merge(int p,int q,int l,int r){
	if(!p || !q) return p + q;
	if(l == r){
		int u = dy[p],v = dy[q];
		if(e[u].size() > e[v].size()) swap(u,v),swap(p,q);
		vis[u] = 2;
		if(!vis[v]) que[++t] = v,vis[v] = 1;
		for(int i = 0;i < e[u].size();++i) e[v].pb(e[u][i]);
		e[u].clear();
		return q;
	}
	md;
	ls[q] = merge(ls[p],ln);
	rs[q] = merge(rs[p],rn);
	return q;
}
inline int mg(int u,int v){
	u = getf(u);v = getf(v);
	if(u == v) return u;
	if(sz[u] > sz[v]) swap(u,v);
	f[u] = v;sz[v] += sz[u];
	rt[v] = merge(rt[u],rt[v],1,k);
	return v;
}
int main(){
	int i,u,v,w;
	n = read();m = read();k = read();
	for(i = 1;i <= n;++i) f[i] = i,sz[i] = 1;
	for(i = 1;i <= m;++i){
		u = read();v = read();w = read();
		ins(rt[v],1,k,w,u);
	}
	while(h < t){
		w = que[++h];
		if(vis[w] == 2) continue;
		u = e[w][0];
		for(i = 1;i < e[w].size();++i) u = mg(u,e[w][i]);
		if(vis[w] == 2) continue;
		e[w].clear();e[w].pb(u);vis[w] = 0;
	}
	li ans = 0;
	for(i = 1;i <= n;++i) if(f[i] == i) ans += 1ll * sz[i] * (sz[i] - 1) / 2;
	print(ans);
	return 0;
}

儘管6*10^5的資料範圍很大,在0.5s內跑出來還是問題不大。