题目
题目链接:https://www.luogu.com.cn/problem/P7323
给定一张 (n) 个点 (2 m) 条边的有向图,图中的每条边上都有一个标记,代表一个左括号或者右括号。共有 (k) 种不同的括号类型,即图中可能有 (2 k) 种不同的标记。点、边、括号种类均从 (1) 开始编号。
图中的每条边都会和另一条边成对出现。更具体地,若图中存在一条标有第 (w) 种括号的左括号的边 ((u, v)),则图中一定存在一条标有第 (w) 种括号的右括号的边 ((v, u))。同样地,图中每条标有右括号的边将对应着一条反方向的标有同类型左括号的边。
现在请你求出,图中共有多少个点对 ((x, y))((1 le x<y le n))满足:图中存在一条从 (x) 出发到达 (y) 的路径,且按经过顺序将路径各条边上的标记拼接得到的字符串是一个合法的括号序列。
思路
显然“存在一条 ((x,y,w)) 的边,那么必然存在一条 ((y,x,-w)) 的边是解题关键”。
我们发现在这样的条件下,如果点对 ((x,y)) 是合法的,那么 ((y,x)) 也是合法的(虽然并不计入答案中)。
再根据合法括号序列的性质,不难发现最终合法点对一定形成若干个团。因为如果 ((x,y),(y,z)) 均合法,((x,z)) 也必然合法。
所以考虑通过不断合并距离为 (2) 的点对。如果 (x o y o z) 是合法括号序列,那么一定存在 (x o y) 和 (z o y) 的颜色相同的边。
所以我们记 (mathrm{num}_{x,w}) 表示点 (x) 出发的一条颜色为 (w) 的边练向的点。然后如果加入一条 ((x,y,w)) 的边,就把 (y) 和 (num_{x,w}) 合并。
但是合并了之后可能引发再一次合并。为了保证合并的复杂度,我们需要并查集启发式合并。而引发的合并直接扔进队列即可。
时间复杂度 (O(mlog ^2malpha(m)+n))。如果使用 hash 或者 unordered_map 可以省区一个 (log)。
代码
#include <bits/stdc++.h>
#define MAPit map<int,int>::iterator
using namespace std;
typedef long long ll;
const int N=300010;
int n,m,k,father[N],siz[N];
ll ans;
map<int,int> num[N];
queue<int> q;
int find(int x)
{
return x==father[x]?x:father[x]=find(father[x]);
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for (int i=1,x,y,z;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&z);
if (num[y].find(z)==num[y].end()) num[y][z]=x;
else q.push(x),q.push(num[y][z]);
}
for (int i=1;i<=n;i++)
father[i]=i,siz[i]=1;
while (q.size())
{
int x=find(q.front()); q.pop();
int y=find(q.front()); q.pop();
if (x==y) continue;
if (siz[x]>siz[y]) swap(x,y);
for (MAPit it=num[x].begin();it!=num[x].end();it++)
{
int w=it->first;
if (num[y].find(w)==num[y].end()) num[y][w]=it->second;
else q.push(num[y][w]),q.push(it->second);
}
siz[y]+=siz[x]; father[x]=y;
num[x].clear();
}
for (int i=1;i<=n;i++)
if (find(i)==i) ans+=1LL*siz[i]*(siz[i]-1)/2;
printf("%lld",ans);
return 0;
}