[loj3462] [luogu7323] [WC2021] 括號路徑 - 圖論 - 等價關係 - 線段樹合併 - 啟發式合併
傳送門:https://www.luogu.com.cn/problem/P7324
好久沒寫題解了,最近放假閒得無聊,寫了寫這兩天WC的題,順手來一發題解。
題目大意:給定圖,每條邊上正反方向分別標有某種括號的左括號和右括號,問有多少點對之間存在能組成合法括號序列的路徑。
這道題的關鍵是:這個“存在合法括號序列路徑”到底有什麼性質?
如果我們在上建立關係:當且僅當點到點存在合法括號序列路徑。方便起見這裡姑且把的限制去掉。
那麼我們驚奇地發現,居然是個等價關係:
(1)自反性:自己到自己是空串,顯然是合法的;
(2)對稱性:如果到存在合法路徑,那麼考慮其翻轉之後的路徑
(3)傳遞性:如果到存在合法路徑,到存在合法路徑,則考慮兩條路徑拼接而成的路徑,容易證明也是合法的。
發現了這一點,我們就可以嘗試求出所有的等價類,然後所求答案就是對於所有等價類,的和。
怎麼求出等價類呢?
我們先從最簡單的非空合法序列“”看起,假設點到點有一條“”的邊,點到點有一條“”的邊。定睛一看,其實就是點向外有兩條“”的邊。
因此,我們只保留所有的右括號邊,這樣如果某個點向外有兩條相同種類括號的邊,就可以把它們的終點合併。實際操作中可以看作用一個新點替代了原先的兩個點,並繼承了原先兩個點的所有出入邊。
如果又有點到點有“
如果某兩點間存在一條合法路徑,我們總能通過不斷消去最內層括號的方式把整個括號序列消空,這個事實保證了上述演算法的正確性。
現在需要做的,就是用資料結構維護這個合併過程。
每個點要維護若干個集合,分別儲存不同型別的邊。然後一旦發現某個集合大小,就把這個集合裡所有邊的終點合併。
這當然是老生常談的啟發式合併,考慮到邊的種數很多,可以拿map存每一種邊的索引,或者在外面套一個線段樹合併。
唯一需要注意的程式碼細節是在合併的時候有可能將正在處理的集合又拿去和別的集合合併,或者已經被處理過的集合又因為合併上了新的點需要再次處理。
時間複雜度。
程式碼如下:
#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;
}
儘管的資料範圍很大,在內跑出來還是問題不大。