先叠个甲,网上很少有专门讲求边所在点双的,我无法保证我是第一个发现这种算法的,但是我可以保证我是独立发现它的,巨佬勿喷。
做法
设 $bel_i$ 表示边 $i$ 所属点双,$del_j$ 表示点 $j$ 所属点双编号最大的那个,那么对于任意一条边,设其两端为 $u,v$,那么有:
$$
bel_i=\min(del_u,del_v)
$$
证明
首先要明确以下几点:
- 边一定且只能属于一个点双。
- 边两端点一定属于一个点双。
- 非割点只属于一个点双,对于割点,认为它同时属于多个点双。
- 正常 tarjan 算法先判定出的点双编号小。
分三种情况证明。
情况 $1$
$u,v$ 均不是割点。
那么这两点显然一定属于同一个点双,所以上式显然正确。
情况 $2$
$u$ 为割点,$v$ 为非割点。此时 $bel_i$ 应该等于 $del_v$。
此时 $u$ 可能属于多个点双,我们只需证明一定有 $del_v\leq del_u$ 即可。
那么很显然,更新 $del_v$ 的时候一定会更新 $del_u$,那么如果后面有新的点双包括 $u$,$del_v$ 一定小于 $del_u$。所以上式成立。
情况 $3$
$u,v$ 均为割点。最复杂的一种情况。但是证明起来也很容易。
我们设 $u$ 为 $v$ 的子孙。那么最后一个包含 $u$ 的点双一定包含 $u$ 和 $u$ 的祖先,当然也包括这条边。这是因为在 tarjan 中是从下往上判定点双的。且此时一定会更新 $v$ 所在的点双。那么上式就显然了。
如果有巨佬发现证明过程中有疏漏或者错误,请及时指出,蒟蒻一定会改正。
代码实现
只需要对 tarjan 进行一点小小的改动即可。
//tarjan
int dfn[N], low[N];
int dcnt, scnt;
vector<int>scc[N];//点双不是强连通分量
stack<int>s;
int root;
int bel[N], del[N];
void tarjan(int now, int fa) {
dfn[now] = low[now] = ++dcnt;
s.push(now);
for (int i = h[now]; i; i = lian[i].ne) {//链式前向星
int to = lian[i].to;
if (!dfn[to]) {
tarjan(to, i);
low[now] = min(low[now], low[to]);
if (low[to] >= dfn[now]) {
scnt++;
while (s.top() != to) {
scc[scnt].push_back(s.top());
del[s.top()] = scnt;
s.pop();
}
scc[scnt].push_back(s.top());
del[s.top()] = scnt;
s.pop();
scc[scnt].push_back(now);
del[now] = scnt;//新的一定是更大的,直接赋值即可
}
} else low[now] = min(low[now], dfn[to]);
}
if (now == root)
s.pop();
}
void solve() {
for (int i = 1; i < N; i++) {
for (int j = h[i]; j; j = lian[j].ne)
bel[lian[j].id] = min(del[i], del[lian[j].to]);//取较小值即可
}
}
其他方法
目前我所了解的求边所在点双的方法有以下两种:
- 这篇题解的写法,将边入栈而不是让点入栈。
- 这篇题解的方法,先建出圆方树,然后进行分类讨论。
其实细想我的方法和这几种方法没有本质区别,且可以相互转化。