强连通分量
题目描述
给定一张 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后循环终止。 -
当分量只有一个节点时:第一次弹出就是
u,while条件不成立,节点被正确处理一次。 -
当分量有多个节点时:从栈顶到
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++;
}
}