古典叙事 · 技术札记

序章

山河有卷
人间有声

写代码,也写长风、旧城与未熄的灯。
愿每一篇随笔,都有自己的山水与回声。
阅览随笔

Tarjan算法

连通性相关有:强连通分量,双连通分量,割点和桥,边,点连通分量。

强连通分量

强连通的定义是:有向图 G 强连通是指,G 中任意两个结点连通。
强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图(就是这个子图中的任意两个点可以互相到达)

Tarjan算法

Tarjan 算法基于对图进行 深度优先搜索。我们视每个连通分量为搜索树中的一棵子树,在搜索过程中,维护一个栈,每次把搜索树中尚未处理的节点加入栈中。
用low数组储存的子树中能够回溯到的最早的已经在栈中的结点,dfn储存这个节点的dfs序列
看一下这道题:

模板题

B 国有 n 个城市,这些城市以铁路相连。任意两个城市都可以通过铁路直接或者间接到达,希望炸毁铁路,以达到存在某两个城市无法互相通过铁路到达的效果。
然而,只有一发炮弹(A 国国会不给钱了)。所以,他能轰炸哪一条铁路呢?
所以我们要找出所有的如果删除这条边就会让图中有些点不连通的边
https://www.luogu.com.cn/problem/P1656
我们用tarjan算法完成这道题
用low存这个点能通过它的子孙到达的dfn值最小的点,dfn存深度优先搜索时第几个遍历,所以当我的儿子节点的low都比我大的时候,说明我和我的儿子节点只有一条路径可以到达

void dfs(i64 now , i64 fa) {
/*dfn:
	{
		下标:点编号
			内存的值:深度优先搜索时第几个遍历
	}
low:
	{
		下标:点编号
			内存的值:这个点能通过它的子孙到达的dfn值最小的点的dfn
	}*/
	dfn [ now ] = ++cnt;//记录节点遍历顺序
	low [ now ] = dfn [ now ];//更新节点的low数组
	for(auto it:t [ now ] ) {
		if (dfn [ it ] && it != fa) {//如果子节点已经被更新,那么父节点可以到达所有子节点的位置
			low [ now ] = min(low [ now ] , dfn [ it ]);//更新
		}
		if (!dfn [ it ]) {//没有遍历过
			dfs(it , now);
			if (dfn [ now ] < low [ it ]) {//如果当前节点的遍历顺序,小于子节点子树中的最早的点
				edg.push_back({ min(now,it) , max(it,now)});
				
			}
			low [ now ] = min(low [ now ] , low [ it ]);//更新
		}
	}
}
AC代码
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
using i64 = long long;
vector<pair<i64 , i64>>edg;
vector<i64> t [ 300005 ];
i64 n,m;
i64 cnt=0,low[300005],dfn[300005];//cnt代表时间戳,low代表
bool cmp(pair<i64 , i64> a , pair<i64 , i64> b) {
	if (a.first == b.first)
		return a.second < b.second;
	return a.first < b.first;
}
void dfs(i64 now , i64 fa) {
/*dfn:
	{
		下标:点编号
			内存的值:深度优先搜索时第几个遍历
	}
low:
	{
		下标:点编号
			内存的值:这个点能通过它的子孙到达的dfn值最小的点的dfn
	}*/
	dfn [ now ] = ++cnt;//记录节点遍历顺序
	low [ now ] = dfn [ now ];//更新节点的low数组
	for(auto it:t [ now ] ) {
		if (dfn [ it ] && it != fa) {//如果子节点已经被更新,那么父节点可以到达所有子节点的位置
			low [ now ] = min(low [ now ] , dfn [ it ]);//更新
		}
		if (!dfn [ it ]) {//没有遍历过
			dfs(it , now);
			if (dfn [ now ] < low [ it ]) {//如果当前节点的遍历顺序,小于子节点子树中的最早的点
				edg.push_back({ min(now,it) , max(it,now)});
				
			}
			low [ now ] = min(low [ now ] , low [ it ]);//更新
		}
	}
}
int main() {
	cin >> n>>m;
	for (int i = 1; i <= m; i++) {
		i64 x , y;
		cin >> x >> y;
		t [ x ].push_back(y);
		t [ y ].push_back(x);
	}
	for (int i = 1; i <= n; i++) {
		if (!dfn [ i ])
			dfs(i , i);
	}
	sort(edg.begin() , edg.end() , cmp);
	for (auto it : edg) {
		cout << it.first << " " << it.second << endl;
	}
}
tarjan一般的作用有很多,缩点(把有向图中的环缩成一个点,并且保留信息),割点,割边。
用途 图类型 判断条件
强连通分量 有向图 low[u] == dfn[u]
割边 无向图 low[v] > dfn[u]
割点(非根) 无向图 low[v] >= dfn[u]
割点(根) 无向图 DFS 儿子数 >= 2
缩点代码
#include <bits/stdc++.h>
using namespace std;
using i64=long long;
vector<i64> t[300005];
vector<i64> suo[300005];
i64 n,k;
i64 dfn[300005],low[300005];
i64 cnt=0,suocnt=0,siz[300005],inst[300005];

stack<i64> st;
i64 du[300005];
i64 belong[300005];
i64 dis[300005];
i64 p[300005];
i64 dp[300005];
void tarjan(int u){
	dfn[u]=low[u]=++cnt;
	st.push(u);
	inst[u]=1;
	for(auto it:t[u]){
		auto now=it;
		if(!dfn[now]){
			tarjan(now);
			low[u]=min(low[u],low[now]);
		}
		else if(inst[now]){
			low[u]=min(low[u],dfn[now]);
		}
	}
	if(dfn[u]==low[u]){
		suocnt++;
		while(1){
			int x=st.top();
			st.pop();
			inst[x]=0;
			belong[x]=suocnt;
			siz[suocnt]++;
			dis[suocnt]+=p[x];
			if(x==u){
				break;
			}
		}
	}
	
	
}
int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>p[i];
	}
	for(int i=1;i<=k;i++){
		i64 x,a,b;
		cin>>a>>b;
		t[a].push_back(b);
	}
	for (int i = 1; i <= n; i++) {
        if (!dfn[i]) {
            tarjan(i);
        }
    }
	for(int i=1;i<=n;i++)
	{i64 x=belong[i];
		for(auto it:t[i]){
			i64 y=belong[it];
			if(x==y){
				continue;
			}
			suo[x].push_back({y});
			du[y]++;
		}
	}
	queue<i64> q;
	for(int i=1;i<=suocnt;i++){
		dp[i]=dis[i];
		if(du[i]==0){
			q.push(i);
		}
	}
	i64 res=0;;
	while(!q.empty()){
		i64 now=q.front();
		res=max(res,dp[now]);
		q.pop();
		for(auto v:suo[now]){
			dp[v]=max(dp[v],dp[now]+dis[v]);
			du[v]--;
			if(du[v]==0){
				q.push(v);
			}
		}
	}
	cout<<res<<"\n";
	return 0;
}
割点代码
#include <bits/stdc++.h>
using namespace std;

const int N = 200005;

int n, m;
vector<pair<int, int>> g[N];
// {相邻点,边编号}

int dfn[N], low[N];
int timer_;

bool cut[N];

void tarjan(int u, int inEdge) {
    dfn[u] = low[u] = ++timer_;

    int child = 0; // u 在 DFS 树中的儿子数

    for (auto [v, id] : g[u]) {
        // 只跳过“进入 u 的那一条边”
        // 不能写 if(v == fa),否则有重边会错
        if (id == inEdge) {
            continue;
        }

        if (!dfn[v]) {
            child++;

            tarjan(v, id);

            low[u] = min(low[u], low[v]);

            // u 不是 DFS 根节点
            // v 子树无法绕到 u 的祖先
            if (inEdge != 0 && low[v] >= dfn[u]) {
                cut[u] = true;
            }
        } 
        else {
            // 返祖边
            low[u] = min(low[u], dfn[v]);
        }
    }

    // DFS 根节点:有至少两个 DFS 儿子才是割点
    if (inEdge == 0 && child >= 2) {
        cut[u] = true;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;

    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;

        g[u].push_back({v, i});
        g[v].push_back({u, i});
    }

    // 图可能不连通
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) {
            tarjan(i, 0);
        }
    }

    vector<int> ans;

    for (int i = 1; i <= n; i++) {
        if (cut[i]) {
            ans.push_back(i);
        }
    }

    cout << ans.size() << '\n';

    for (int x : ans) {
        cout << x << ' ';
    }

    cout << '\n';
    return 0;
}
posted @ 2025-08-19 11:33  Morphis‘  阅读(37)  评论(0)    收藏  举报