[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内跑出来还是问题不大。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值