虚拟节点-星石传送阵(4星)、强连通分量

问题描述

有 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。

而两个传送阵可以相互传送的规则如下:

  1. 如果两个传送阵的 f(x)f(x) 相等,则两个传送阵可以相互传送。例如 n>6,x=8,x=9n>6,x=8,x=9 便可以相互传送。
  2. 能量值为 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 中。

二、数据结构说明

  1. 链式前向星:两套邻接表,h/e/ne原图h1/e1/ne1转置(反向)图 ;每条输入边u→v,原图加u到v,反向图加v到u
  2. 标记数组:f1原图访问标记、f2反向图访问标记;accrod[]记录每个节点归属第几号 SCC;connet[]存放每个分量的节点集合;printConsole[]标记分量是否已经输出。
  3. list集合:存放原图 DFS 的后序完成序列
  4. 输入输出使用 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 实现)

  1. 遍历顺序:从 list 末尾向前遍历(最晚完成的节点最先访问反向图)。
  2. 碰到未访问的节点 u,开启 BFS 遍历反向图: 在转置图中所有能被 u 遍历到的节点,共同组成一个独立的强连通分量;给这批节点统一填写accrod[节点]=当前分量编号cnt,把节点集合存入connet[cnt]
  3. 每一次 BFS 成功遍历,SCC 总数 cnt 自增 1。

补充:原本此处可以用递归 DFS,代码为了彻底杜绝栈溢出,改用队列 BFS 实现连通块搜集。

阶段 4:依照题目要求输出结果

  1. 第一行输出总 SCC 数量 cnt。
  2. 按节点 1~n 从小到大遍历:
    • 若当前节点所属的 SCC 没有输出过:将该分量内节点升序排序,拼接字符串输出;
    • 给该分量打上已输出标记,后续重复分量直接跳过。 严格匹配题目:优先输出 1 号点分量、再输出未打印的 2 号点分量、顺延至 3、4...... 的输出规则。

四、整体优化点总结

  1. 第一次获取后序序列的 DFS:放弃原生递归写法,使用栈模拟递归迭代,解决大数据下递归深度超标、程序栈溢出崩溃的问题;

  2. 反向图搜连通块:用 BFS 队列替代递归 DFS,双层兜底,全程无递归调用,稳定性拉满;

  3. 使用 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--){
    // ArrayList li=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++;
     }

    }

相关推荐
IT方大同1 小时前
C语言分支与循环语句
c语言·开发语言·算法
待磨的钝刨2 小时前
深入理解主成分分析(PCA)
人工智能·线性代数·算法·机器学习
电子云与长程纠缠2 小时前
UE中使用TGuardValue与TInlineComponentArray数据结构
开发语言·数据结构·学习·ue5·游戏引擎
来一碗刘肉面3 小时前
串的定义与基本操作
数据结构
geovindu3 小时前
java: Backtracking Algorithm
java·开发语言·windows·后端·算法·回溯算法
三克的油3 小时前
数据结构-1
数据结构
晚风叙码3 小时前
C++ vector底层模拟实现与迭代器失效深度剖析
c++·算法
To_OC11 小时前
LC 51 N 皇后:我以为难的是回溯,结果栽在了对角线下标
javascript·算法·leetcode