问题描述
有 nn 个传送阵,编号为 1∼n1∼n,每个传送阵使用若干块 "星石" 作为能源。"星石" 是一种很神奇的物质,一块 "星石" 的 能量值等于它的价值。同时,星石放在一起会激发更强大的能量,例如:有 33 块 "星石" 的能量值分别为 2,3,42,3,4,则它们放在一起可以提供的能量为 2×3×4=242×3×4=24,需要消耗的价值为 2+3+4=92+3+4=9。
现在每个传送阵都有一个能量值 xx, f(x)f(x) 是构成 xx 的星石最小价值之和对 nn 取模加一,换句话说便是 f(x)=(summodn)+1f(x)=(summodn)+1,sumsum 是构成 xx 的星石最小价值之和。
例如:
x=9,n=997,f(x)=(3+3)mod997+1=7x=9,n=997,f(x)=(3+3)mod997+1=7。
而两个传送阵可以相互传送的规则如下:
- 如果两个传送阵的 f(x)f(x) 相等,则两个传送阵可以相互传送。例如 n>6,x=8,x=9n>6,x=8,x=9 便可以相互传送。
- 能量值为 xx 的传送阵可以和 编号 为 f(x)f(x) 的传送阵相互传送。例如 n>6,a4=9n>6,a4=9(a4a4 是假设 44 号传送阵能量为 99),所以 44 号传送阵可以和 77 号传送阵能相互传送。因为 44 号传送阵的能量值为 99,f(x)=(3+3modn)+1=7f(x)=(3+3modn)+1=7。
给定每个传送阵都有的能量值 xx,你需要求出从编号 aa 到编号 bb 的最少传送次数,若不能到达,请输出 -1-1。
输入格式
第 11 行输入 33 个正整数,表示 n,a,bn,a,b,含义为传送阵的数量,起点编号与终点编号。
第 22 行输入 nn 个正整数 xx,表示每个传送阵的能量值。
输出格式
输出 11 行,表示 aa 到 bb 的最少传送次数,若不能从 aa 传送到 bb 则输出 -1-1。
样例输入
4 1 4
9 8 7 6
样例输出
2
说明
形成的图如下所示:

红色线为 f(x)f(x) 相同互相连接,绿色的线为编号为 f(x)f(x) 互相连接。
11 号点到 44 号点可以 1→3→4,1→2→41→3→4,1→2→4,有二种方式可以到达 44 号点,均是最优路径走法。
评测数据规模
2≤n≤105,2≤x≤108,1≤a,b≤n2≤n≤105,2≤x≤108,1≤a,b≤n。



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=4*100010;
static int n,m;
static int id=1,idx=1;
static int g[]=new int[N+1];
static int dist[]=new int[N+1];
static boolean isSure[]=new boolean[N];
static int prime[]=new int[N];
static boolean std[]=new boolean[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 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());
int a=Integer.parseInt(st.nextToken()),b=Integer.parseInt(st.nextToken());
st=new StringTokenizer(br.readLine());
for (int i = 1; i <= n; i++) {
g[i]=Integer.parseInt(st.nextToken());
}
get_Prime();
for (int i = 1; i <= n; i++) {
//如果两个传送阵的 f(x) 相等,则两个传送阵可以相互传送
int u=f(g[i]);
add(i,u,1);
add(u,i,1);
//能量值为 x 的传送阵可以和编号为 f(x) 的传送阵相互传送
//n+u 相当于是一个中转站 节点和中转站相连是1 中转站和节点相连是0
add(i,n+u,1);
add(n+u,i,0);
}
//再用迪杰斯特拉计算最短距离
PriorityQueue<Node> priorityQueue=new PriorityQueue<>();
priorityQueue.add(new Node(a,0));
Arrays.fill(dist, Integer.MAX_VALUE);
dist[a]=0;
while(!priorityQueue.isEmpty()){
Node no=priorityQueue.poll();
int u=no.x;
if(!isSure[u]){
if(u==b){
bw.write(dist[u]+"");
bw.flush();
return;
}
isSure[u]=true;
for (int i = h[u]; i > 0; i=ne[i]) {
int son=e[i];
if(!isSure[son]){
if(dist[son]>dist[u]+w[i]){
dist[son]=dist[u]+w[i];
priorityQueue.add(new Node(son,dist[son]));
}
}
}
}
}
bw.write("-1");
br.close();
bw.flush();
bw.close();
}
static void get_Prime(){
for (int i = 2; i < N; i++) {
if(!std[i]){
prime[idx++]=i;
}
for (int j = 1; j < idx && i*prime[j] < N; j++) {
std[i*prime[j]]=true;
if(i%prime[j]==0)break;
}
}
}
static int f(int u){
int sum=0;
for (int i = 1; i < idx && prime[i]*prime[i]<=u; i++) {
int primenum=prime[i];
while(u>0 && u%primenum==0){
sum+=primenum;
u/=primenum;
}
}
if(u>1)sum+=u;
return sum%n+1;
}
static class Node implements Comparable<Node>{
int x;
int dis;
public Node() {
// TODO Auto-generated constructor stub
}
public Node(int x, int dis) {
this.x = x;
this.dis = dis;
}
@Override
public int compareTo(Node o) {
// TODO Auto-generated method stub
return this.dis-o.dis;
}
}
static void add(int a,int b,int c){
e[id]=b;
ne[id]=h[a];
w[id]=c;
h[a]=id++;
}
}
强连通分量
题目描述
给定一张 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。

解题思路(Kosaraju 算法 + 迭代栈优化 DFS 防栈溢出)
一、算法整体思想
本题采用Kosaraju 算法求解有向图强连通分量 (SCC),整套算法分为三大阶段:原图遍历记录节点完成次序 → 反向图依照逆序拆分强连通块 → 按照题目指定规则格式化输出结果。 强连通定义:一个分量内部任意两点可以互相抵达;不同强连通分量之间只存在单向通路。Kosaraju 利用「原图后序时间戳」保证在反向图遍历的时候,不会从当前 SCC 跑到别的 SCC 中。
二、数据结构说明
- 链式前向星:两套邻接表,
h/e/ne存原图 ,h1/e1/ne1存转置(反向)图 ;每条输入边u→v,原图加u到v,反向图加v到u。 - 标记数组:
f1原图访问标记、f2反向图访问标记;accrod[]记录每个节点归属第几号 SCC;connet[]存放每个分量的节点集合;printConsole[]标记分量是否已经输出。 list集合:存放原图 DFS 的后序完成序列。- 输入输出使用 BufferedReader/BufferedWriter 高速 IO,适配 1e5 条边的大数据,避免普通 Scanner 超时。
三、分步详细流程 + DFS 逐层拆解
阶段 1:建图
循环读取 n 个点、m 条有向边u v,调用 add1 构建原图u→v,调用 add2 构建反向图v→u。
阶段 2:遍历原图,获取节点后序完成序列(dfs1 方法)
1)原生递归 DFS 逻辑
标准递归后序 DFS 逻辑:
plaintext
void dfs(int u){
标记u已访问
递归遍历u所有邻接子节点
// 所有孩子全部走完之后,当前节点才算"完成"
list.add(u);
}
核心规则:一个节点必须等它所有可达子节点全部遍历完毕,自身才会被存入 list ,存入顺序就是节点的完成时间先后顺序。 Kosaraju 要求:后续需要对这个 list倒序遍历(先访问最晚完成的节点)。第一次DFS在向列表中加入节点的过程中,越后加入的点越可能通往其他的连通分量。这也是我们在第二次DFS中进行逆序遍历的原因。
2)本代码栈式迭代改写思路
当节点数量接近 1e4 时,递归深度过大会触发 Java 递归栈溢出报错,因此用手动栈模拟系统递归栈 ,栈内存数组[节点编号, 状态flag]:
- state = 0:初次入栈,代表刚访问该节点,需要先遍历它所有子节点; 操作:打上访问标记,重新把当前节点以 state=1 压回栈中,再把所有邻接子节点以 state=0 入栈(栈后进先出,保证遍历顺序和递归一致)。
- state = 1:代表该节点的全部子节点都已经遍历处理完成; 操作:将当前节点加入
list列表,记录完成次序。 依靠手动栈,完整复刻了递归后序 DFS 的执行逻辑,同时规避了 JVM 递归栈深度限制。
阶段 3:逆序 list,遍历反向图划分所有强连通分量(dfs22 BFS 实现)
- 遍历顺序:从 list 末尾向前遍历(最晚完成的节点最先访问反向图)。
- 碰到未访问的节点 u,开启 BFS 遍历反向图: 在转置图中所有能被 u 遍历到的节点,共同组成一个独立的强连通分量;给这批节点统一填写
accrod[节点]=当前分量编号cnt,把节点集合存入connet[cnt]。 - 每一次 BFS 成功遍历,SCC 总数 cnt 自增 1。
补充:原本此处可以用递归 DFS,代码为了彻底杜绝栈溢出,改用队列 BFS 实现连通块搜集。
阶段 4:依照题目要求输出结果
- 第一行输出总 SCC 数量 cnt。
- 按节点 1~n 从小到大遍历:
- 若当前节点所属的 SCC 没有输出过:将该分量内节点升序排序,拼接字符串输出;
- 给该分量打上已输出标记,后续重复分量直接跳过。 严格匹配题目:优先输出 1 号点分量、再输出未打印的 2 号点分量、顺延至 3、4...... 的输出规则。
四、整体优化点总结
-
第一次获取后序序列的 DFS:放弃原生递归写法,使用栈模拟递归迭代,解决大数据下递归深度超标、程序栈溢出崩溃的问题;
-
反向图搜连通块:用 BFS 队列替代递归 DFS,双层兜底,全程无递归调用,稳定性拉满;
-
使用 Buffered 高速 IO 处理大量输入数据,不会出现评测超时。
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=5*100010; static int n,m; static int id=1,idx=1; static int accrod[]=new int[10010];//属于哪一连通分量 //正向图 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 f1[]=new boolean[10010];//是否访问过 //反向图 static int h1[]=new int[N]; static int e1[]=new int[N]; static int ne1[]=new int[N]; static int w1[]=new int[N]; static boolean f2[]=new boolean[10010];//是否访问过 static boolean printConsole[]=new boolean[10010];//第i个连通分量是否输出了 static ArrayList<Integer> connet[]=new ArrayList[N];//存第i个连通分量 static List<Integer> list=new ArrayList<Integer>();//存拓扑排序的访问的点的次序 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()); add1(u,v,1); add2(v,u,1); } for (int i = 1; i <= n; i++) { if(!f1[i])dfs1(i); } //第一次DFS 我们得到后续遍历的顺序 //第二次DFS 我们逆序遍历 第一次DFS得到的顺序// //按逆序的顺序依次遍历反向图
// int cnt=0;//第几个连通分量
// for(int i=list.size()-1;i>=0;i--){
// ArrayListli=new ArrayList<>();
// int u=list.get(i);
// if(!f2[u]){
// dfs2(u,++cnt,li);
// connet[cnt]=li;
// }
//
// }
int cnt=0;
for (int i =list.size()-1; i >= 0; i--) {
int u=list.get(i);if(!f2[u]){ ArrayList<Integer> li=dfs22(u,++cnt); connet[cnt]=li; } } 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 k:connet[accrod[i]]){ stringBuilder.append(k+" "); } bw.write(stringBuilder.toString().trim()+"\n"); printConsole[accrod[i]]=true; } } br.close(); bw.flush(); bw.close(); } static void dfs2(int u,int cnt,ArrayList<Integer> li){ accrod[u]=cnt; f2[u]=true; li.add(u); for (int i = h1[u]; i > 0; i=ne1[i]) { int son=e1[i]; if(!f2[son]){ dfs2(son, cnt,li); } } } static ArrayList<Integer> dfs22(int u,int cnt){//dfs2的栈的实现 ArrayList<Integer> li=new ArrayList<>(); Queue<Integer> queue=new LinkedList<Integer>(); queue.add(u); accrod[u]=cnt; f2[u]=true; li.add(u); while(!queue.isEmpty()){ u=queue.poll(); for (int i = h1[u]; i > 0; i=ne1[i]) { int son=e1[i]; if(!f2[son]){ f2[son]=true; accrod[son]=cnt; queue.add(son); li.add(son); } } } return li; }// static void dfs1(int u){//后序遍历
// if(f1[u])return;
//
// f1[u]=true;
// for (int i = h[u]; i > 0; i=ne[i]) {
// int son=e[i];
// if(!f1[son]){
// dfs1(son);
// }
// }
// list.add(u);
// }static void dfs1(int u){//后序遍历栈的实现 Stack<int[]> stack=new Stack<>(); stack.add(new int[]{u,0});//又表示当前节点零表示是否处理完了子节点 //0表示表我刚发现这个节点,准备"深度优先去探索它的子节点" //1代表这个节点的所有子节点都已经探索并处理完毕了 while(!stack.isEmpty()){ int g[]=stack.pop(); int o=g[0],state=g[1]; if(state==0){ if(f1[o])continue; f1[o]=true; stack.add(new int[]{o,1}); for (int i = h[o]; i > 0; i=ne[i]) { int son=e[i]; if(!f1[son]){ stack.add(new int[]{son,0}); } } }else{ list.add(o); } } } static void add1(int a,int b,int c){ e[id]=b; ne[id]=h[a]; w[id]=c; h[a]=id++; } static void add2(int a,int b,int c){ e1[idx]=b; ne1[idx]=h1[a]; w1[idx]=c; h1[a]=idx++; }}