Tarjan算法-强连通分量、路径、出差

强连通分量

题目描述

给定一张 n 个点 m 条边的有向图,求出其所有的强连通分量。

注意,本题可能存在重边和自环。

输入格式

第一行两个正整数 n,m,表示图的点数和边数。

接下来 m 行,每行两个正整数 u 和 v 表示一条边。

输出格式

第一行一个整数表示这张图的强连通分量数目。

接下来每行输出一个强连通分量。第一行输出 1 号点所在强连通分量,第二行输出 2 号点所在强连通分量,若已被输出,则改为输出 3 号点所在强连通分量,以此类推。每个强连通分量按节点编号大小输出。

输入输出样例

输入 #1复制

复制代码
6 8
1 2
1 5
2 6
5 6
6 1
5 3
6 4
3 4

输出 #1复制

复制代码
3
1 2 5 6
3
4

说明/提示

对于所有数据,1≤n≤10000,1≤m≤100000。

Tarjan 算法求有向图强连通分量

1. 算法简介

Tarjan 算法 是基于一次 DFS 的高效强连通分量(SCC)算法,时间复杂度 O(n + m)

核心思想是为每个节点 u 维护两个时间戳:

  • dfn[u]dfs_num :节点 u 在 DFS 过程中被访问的顺序。

  • low[u] :从节点 u 出发,通过自身或子孙的边,能够追溯到的最早的仍在栈中 的节点的 dfn 值。

当一个节点 u 满足 dfn[u] == low[u] 时,说明它无法回到比自己更早的节点,因此 u 以及当前栈中位于 u 之上的所有节点正好构成一个强连通分量。


2. 关键数据结构与变量

变量 含义
n, m 图的点数、边数
h, e, ne 邻接表(链式前向星),h[u] 是头指针
dfs_num dfn 数组,时间戳
low 回溯最小值数组
stack 当前 DFS 栈,存放尚未确定 SCC 的节点
f 栈标记数组,f[x]=true 表示 x 在栈中
cnt 已找到的强连通分量个数
accrod accrod[x] 表示节点 x 所属 SCC 的编号(从 1 开始)
connet connet[i] 是一个 ArrayList,存储编号为 i 的 SCC 内的所有节点
printConsole 标记第 i 个 SCC 是否已经输出,用于按题目要求顺序输出

3. 算法流程

(1) 初始化

对每个未访问的节点调用 Tarjian(i)

(2) 进入节点 u

java

复制代码
dfs_num[u] = low[u] = ++time;
stack.push(u);
f[u] = true;

分配时间戳,压栈并标记。

(3) 遍历所有出边 (u, v)

java

复制代码
for (int i = h[u]; i > 0; i = ne[i]) {
    int son = e[i];
    if (dfs_num[son] == 0) Tarjian(son);    // 未访问,递归
    if (f[son])                              // son 还在栈内(是回边或横叉边)
        low[u] = Math.min(low[u], low[son]); // 用 son 的 low 更新 u
}
  • 如果 son 未被访问,递归进入 son,返回后 son 可能在栈中也可能已出栈(若已形成 SCC)。

  • 只有当 f[son] 为真(son 还在栈内)时,才用 low[son] 更新 low[u]。这保证了更新只来自同一 SCC 内的节点,避免跨分量更新。

注意 :这里用 low[son] 更新,而一些实现用 dfn[son]。两种写法在 Tarjan 算法中都是正确的变体,不影响最终结果。

(4) 识别 SCC 根并弹出分量

java

复制代码
if (dfs_num[u] == low[u]) {
    connet[++cnt] = new ArrayList<>();   // 新建分量列表

    int v = stack.pop();                 // 弹出栈顶第一个节点
    f[v] = false;
    accrod[v] = cnt;
    connet[cnt].add(v);

    while (u != v) {                     // 继续弹出直到弹出根 u
        v = stack.pop();
        f[v] = false;
        accrod[v] = cnt;
        connet[cnt].add(v);
    }
}
先弹出栈顶节点,处理(清标记、记录分量、加入列表)。
  • 若该节点不是 u,则进入 while 循环,反复弹出、处理,直到弹出 u 后循环终止

  • 当分量只有一个节点时:第一次弹出就是 uwhile 条件不成立,节点被正确处理一次。

  • 当分量有多个节点时:从栈顶到 u 的所有节点恰好各被处理一次,无遗漏、无重复

这实际上等价于 do-while 的效果,但分成了"先弹出第一个,再循环弹出剩余"的结构,只要逻辑严密,同样正确。

(5) 输出答案

java

复制代码
bw.write(cnt + "\n");                      // 第一行:分量总数
for (int i = 1; i <= n; i++) {
    if (!printConsole[accrod[i]]) {         // 该分量未输出过
        printConsole[accrod[i]] = true;
        Collections.sort(connet[accrod[i]]); // 分量内排序
        // 输出分量内所有节点(空格分隔)
    }
}

按照题目要求:从 1 号点开始,输出其所在分量;若该分量已输出,则跳过继续往后找,最终每个分量被输出一次,且内部节点按编号升序。


4. 复杂度分析

  • 时间:每个节点和每条边均被访问常数次,Tarjan 部分 O(n + m),排序部分总和 O(n log n) 以内,整体可高效通过。

  • 空间:邻接表、数组等占用 O(n + m)。

    import java.io.BufferedReader;
    import java.io.BufferedWriter;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.OutputStreamWriter;
    import java.util.*;

    public class Main {

    复制代码
      static int N=100010;     
         
      static int n,m,cnt;
      static int id=1,time;
      
      static int accrod[]=new int[10010];//属于哪一连通分量
      static int dfs_num[]=new int[10010];//DFS访问顺序
      static int low[]=new int[10010];
      //low[u] 通过 u 的子孙节点或者 u 本身,能够回溯到的最早的 dfn(时间戳)值。
      
      static int h[]=new int[N];
      static int e[]=new int[N];
      static int ne[]=new int[N];
      static int w[]=new int[N];
      static boolean f[]=new boolean[10010];//是否访问过
      
      static Stack<Integer> stack=new Stack<>();
       
      static boolean printConsole[]=new boolean[10010];//第i个连通分量是否输出了
      static ArrayList<Integer> connet[]=new ArrayList[N];//存第i个连通分量
      
      
      static BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 
      static BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(System.out));
      
      public static void main(String[] args) throws IOException { 
      	
          StringTokenizer st=new StringTokenizer(br.readLine());
          n=Integer.parseInt(st.nextToken());  
          m=Integer.parseInt(st.nextToken());
          
          for (int i = 1; i <= m; i++) {
          	st=new StringTokenizer(br.readLine());
      		int u=Integer.parseInt(st.nextToken()),v=Integer.parseInt(st.nextToken());
      		add(u,v,1);		
      	}
          
          for (int i = 1; i <= n; i++) {
      		if(dfs_num[i]==0)Tarjian(i);
      	}
          
          bw.write(cnt+"\n");
          for (int i = 1; i <= n; i++) {
      		if(!printConsole[accrod[i]]){
      			Collections.sort(connet[accrod[i]]);
      			StringBuilder stringBuilder=new StringBuilder();
      			for (int p:connet[accrod[i]]) {
      				stringBuilder.append(p).append(" ");
      			}
      			bw.write(stringBuilder.toString().trim()+"\n");
      			printConsole[accrod[i]]=true;
      		}
      	}
          
          
          
          br.close();
          bw.flush();
          bw.close();
      }
      
      
    
      static void Tarjian(int u){
      	dfs_num[u]=low[u]=++time;
      	stack.add(u);
      	f[u]=true;//标记在栈中
      	
      	for (int i = h[u]; i > 0; i=ne[i]) {
      		int son=e[i];
      		if(dfs_num[son]==0)Tarjian(son);
      		if(f[son]){
      			low[u]=Math.min(low[son], low[u]);
      		}
      	}
      	
      	if(dfs_num[u]==low[u]){
      		connet[++cnt]=new ArrayList<>();
      		
      		int v=stack.pop();
      		f[v]=false;
      		accrod[v]=cnt;
      		connet[cnt].add(v);
     		    
      		while(u!=v){
      			v=stack.pop();
      			f[v]=false;
      			accrod[v]=cnt;
      			connet[cnt].add(v);
      			
      		}   		
      	}

    // if(dfs_num[u] == low[u]){
    // cnt++;
    // connet[cnt] = new ArrayList<>();
    // int v;
    // do {
    // v = stack.pop();
    // f[v] = false; // 一出栈立即清除标记
    // accrod[v] = cnt; // 记录所属分量
    // connet[cnt].add(v); // 加入分量
    // } while (v != u);
    // }
    }

    复制代码
      static void add(int a,int b,int c){
      	e[id]=b;    	
      	ne[id]=h[a];  
      	w[id]=c;
      	h[a]=id++;
      }

    }

路径

本题为填空题,只需要算出结果后,在代码中使用输出语句将所填结果输出即可。

小蓝学习了最短路径之后特别高兴,他定义了一个特别的图,希望找到图 中的最短路径。

小蓝的图由 2021 个结点组成,依次编号 1 至 2021。

对于两个不同的结点 a, b,如果 a 和 b 的差的绝对值大于 21,则两个结点 之间没有边相连;如果 a 和 b 的差的绝对值小于等于 21,则两个点之间有一条 长度为 a 和 b 的最小公倍数的无向边相连。

例如:结点 1 和结点 23 之间没有边相连;结点 3 和结点 24 之间有一条无 向边,长度为 24;结点 15 和结点 25 之间有一条无向边,长度为 75。

请计算,结点 1 和结点 2021 之间的最短路径长度是多少。

提示:建议使用计算机编程解决问题。

复制代码
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.*;


public class Main {

    static int N=100010;     
       
    static int n,m;
    static int id=1;
    
   
    static int dist[]=new int[N];
    static int h[]=new int[N];
    static int e[]=new int[N];
    static int ne[]=new int[N];
    static int w[]=new int[N];
    static boolean f[]=new boolean[10010];//是否确定了最短路径
    
    
    
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 
    static BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(System.out));
    
    public static void main(String[] args) throws IOException { 
    	
//        StringTokenizer st=new StringTokenizer(br.readLine());
//        n=Integer.parseInt(st.nextToken());  
//        m=Integer.parseInt(st.nextToken());
    	
    	for (int i = 1; i <2022; i++) {
			for (int j = i+1; j < 2022; j++) {
				if(j-i<=21){
					add(i,j, lcm(j,i));
					add(j,i, lcm(j,i));
				}
			}
		}
    	
    	//堆优化版的迪杰斯特拉算法
    	Arrays.fill(dist,Integer.MAX_VALUE);
    	dist[1]=0;
    	PriorityQueue<int[]> priorityQueue=new PriorityQueue<>((a,b)->a[1]-b[1]);
    	priorityQueue.add(new int[]{1,0});
    	while(!priorityQueue.isEmpty()){
    		int no[]=priorityQueue.poll();
    		int u=no[0];
    		if(!f[u]){
    			f[u]=true;
    			for (int i = h[u]; i > 0; i=ne[i]) {
					int son=e[i];
					if(!f[son]){
						dist[son]=Math.min(dist[son],dist[u]+w[i]);
						priorityQueue.add(new int[]{son,dist[son]});
					}
				}
    		}
    	}
    	bw.write(dist[2021]+"");
        
        br.close();
        bw.flush();
        bw.close();
    }
    
    static int lcm(int a,int b){
   	 return a*b/gcd(a,b);
    }
  
    static int gcd(int a,int b){
   	 return b==0?a:gcd(b,a%b);
    }
    
    static void add(int a,int b,int c){
    	e[id]=b;    	
    	ne[id]=h[a];  
    	w[id]=c;
    	h[a]=id++;
    }
       
}

出差

问题描述

AA 国有 NN 个城市, 编号为 1...N1...N 。小明是编号为 1 的城市中一家公司的员 工, 今天突然接到了上级通知需要去编号为 NN 的城市出差。

由于疫情原因, 很多直达的交通方式暂时关闭, 小明无法乘坐飞机直接从 城市 1 到达城市 NN, 需要通过其他城市进行陆路交通中转。小明通过交通信息 网, 查询到了 MM 条城市之间仍然还开通的路线信息以及每一条路线需要花费的 时间。

同样由于疫情原因, 小明到达一个城市后需要隔离观察一段时间才能离开 该城市前往其他城市。通过网络, 小明也查询到了各个城市的隔离信息。(由于 小明之前在城市 1 , 因此可以直接离开城市 1 , 不需要隔离)

由于上级要求, 小明希望能够尽快赶到城市 NN, 因此他求助于你, 希望你 能帮他规划一条路线, 能够在最短时间内到达城市 NN 。

输入格式

第 1 行: 两个正整数 N,M,NN,M,N 表示 A 国的城市数量, MM 表示末关闭的路 线数量

第 2 行: NN 个正整数, 第 ii 个整数 CiCi​ 表示到达编号为 ii 的城市后需要隔离 的时间

第 3...M+23...M+2 行: 每行 3 个正整数, u,v,cu,v,c, 表示有一条城市 uu 到城市 vv 的 双向路线仍然开通着, 通过该路线的时间为 cc

输出格式

第 1 行: 1 个正整数, 表示小明从城市 1 出发到达城市 NN 的最短时间(到 达城市 NN, 不需要计算城市 NN 的隔离时间)

样例输入

复制代码
4 4
5 7 3 4
1 2 4
1 3 5
2 4 3
3 4 5

样例输出

复制代码
13

评测用例规模与约定

对于 100%100% 的数据, 1≤N≤1000,1≤M≤10000,1≤Ci≤200,1≤u,v≤1≤N≤1000,1≤M≤10000,1≤Ci​≤200,1≤u,v≤ N,1≤c≤1000N,1≤c≤1000

复制代码
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.*;


public class Main {

    static int N=1010,M=2*10010;     
       
    static int n,m;
    static int id=1;
    
   
    static int dist[]=new int[N];
    static int c[]=new int[N];
    
    static int h[]=new int[M];
    static int e[]=new int[M];
    static int ne[]=new int[M];
    static int w[]=new int[M];
    static boolean f[]=new boolean[10010];//是否确定了最短路径
    
    
    
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 
    static BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(System.out));
    
    public static void main(String[] args) throws IOException { 
    	
        StringTokenizer st=new StringTokenizer(br.readLine());
        n=Integer.parseInt(st.nextToken());  
        m=Integer.parseInt(st.nextToken());
    	
        st=new StringTokenizer(br.readLine());
    	for (int i = 1; i <= n; i++) {
			c[i]=Integer.parseInt(st.nextToken());
		}
    	
    	for (int i = 0; i < m; i++) {
    		st=new StringTokenizer(br.readLine());
    		int u=Integer.parseInt(st.nextToken()),v=Integer.parseInt(st.nextToken());
    		int we=Integer.parseInt(st.nextToken());
    		add(u,v,we);
    		add(v,u,we);
		}
    	
    	Arrays.fill(dist,Integer.MAX_VALUE);
    	dist[1]=0;
    	
    	
    	
    	PriorityQueue<int[]> priorityQueue=new PriorityQueue<>((a,b)->a[1]-b[1]);
    	priorityQueue.add(new int[]{1,0});
    	while(!priorityQueue.isEmpty()){
    		int no[]=priorityQueue.poll();
    		int u=no[0];
    		if(!f[u]){
    			f[u]=true;
    			for (int i = h[u]; i > 0; i=ne[i]) {
					int son=e[i];
					if(!f[son]){
						dist[son]= Math.min(dist[son], dist[u]+w[i]+c[son]);
						priorityQueue.add(new int[]{son,dist[son]});
						
					}
				}
    			
    		}
    	}
    	
    	bw.write(dist[n]-c[n]+"");
        
    	
        br.close();
        bw.flush();
        bw.close();
    }
    
    
    
    static void add(int a,int b,int c){
    	e[id]=b;    	
    	ne[id]=h[a];  
    	w[id]=c;
    	h[a]=id++;
    }
       
}
相关推荐
geovindu1 小时前
CSharp: Iterative Algorithms
开发语言·后端·算法·c#·.net·迭代算法
道影子2 小时前
《黄帝内经》021章|津凝精晶 沉降为患
人工智能·深度学习·神经网络·算法·机器学习
道影子2 小时前
《黄帝内经》022章|调和双旋 伏风自消
人工智能·深度学习·神经网络·算法·机器学习
吞下星星的少年·-·2 小时前
牛客技能树:一道GCD问题(排序,数论)
算法
m沐沐3 小时前
【计算机视觉】人脸识别三大经典算法:LBPH、Eigenfaces、FisherFaces 原理与实战
图像处理·人工智能·深度学习·opencv·算法·机器学习·计算机视觉
Lumos1863 小时前
嵌入式常用滤波算法与控制算法(5)卡尔曼滤波(下)
算法
Lumos1863 小时前
嵌入式常用滤波算法与控制算法(6)互补滤波
算法
Tisfy3 小时前
LeetCode 3536.两个数字的最大乘积:O(1)空间维护max2
数学·算法·leetcode·题解
知识分享小能手3 小时前
统计学学习教程,从入门到精通,一元线性回归 —— 知识点详解(17)
学习·算法·线性回归