前置知识
为啥要自建类
有些平台不给用集合,就很牛
path定义
1 长度为3
2 分别为权重,初始点,连接点(对于有向图而言两者是有区别的)
并查集
并查集思想
并查集是一种集合划分方法,而且还可以找到上一个点的路径
如果两个元素根节点不同,说明不在同一集合,可以创建路径,否则不能
对于像kruskal这种直接选取边的算法来说,每条边选取时都需要判断环,而成环原因就是一个集合中有点再次相连
又比如prim,每次取边时,还要判断这条边是否有点存在于生成树中(因为prim每次取和生成数相连最近的节点),我们使用集合表示生成树就可以轻易解出,由于
并查集优化
1 每次查找这个点是否属于某个集合时,顺便将这个点父节点直接改为根(但是压缩后会导致无法打印路径)
2 两个集合合并后,一个根节点会指向另一个,这样有一个集合相当于其中每个元素都增加遍历一次,所以使用根节点记录节点数,这样每次可以拿大的合并小的,减少合并后遍历平均消耗
单源最短路径
dijkstra
核心思想
维护一个最小数组,每次获取数组最小点,然后拿最小点路径更新数组,直到整个数组都被访问过
实现
class HeadNode{
Node head;
Node end;
}
class Node{
Node next;
int dist;
int spend;
public Node(int dist,int spend){
this.dist = dist;
this.spend = spend;
}
}
class Solution {
public int[] dijkstra(int N,int[][] path){
HeadNode[] nodes = new HeadNode[N];
boolean[] visited = new boolean[N];
int[] spendArr = new int[N];
int len = path.length;
Arrays.fill(spendArr, Integer.MAX_VALUE);
spendArr[0] = 0;
for(int i=0;i<N;i++){
nodes[i] = new HeadNode();
}
//初始化图
for(int i=0;i<len;i++){
int val = path[i][0];
int src = path[i][1];
int dist = path[i][2];
Node newNode = new Node(dist,val);
HeadNode list = nodes[src];
if(list.head==null){
list.head = list.end = newNode;
}else{
list.end.next = newNode;
list.end = list.end.next;
}
}
DFS(0,visited,nodes,spendArr);
return spendArr;
}
/**
每次在数组更新当前节点到其它节点距离,然后选取数组最大值,作为新节点,进入递归
*/
public void DFS(int src,boolean[] visited,HeadNode[] nodes,int[] spendArr){
visited[src] = true;
Node head = nodes[src].head;
while(head!=null){
int dist = head.dist;
int val = spendArr[src]+head.spend;
if(val<spendArr[dist]){
spendArr[dist] = val;
}
head = head.next;
}
int len = visited.length;
int nextDistIndex = -1;
int nextDistSpend = Integer.MAX_VALUE;
for(int i=0;i<len;i++){
if((spendArr[i]<nextDistSpend)&&!visited[i]){
nextDistSpend = spendArr[i];
nextDistIndex = i;
}
}
if(nextDistIndex!=-1){
DFS(nextDistIndex,visited,nodes,spendArr);
}
}
}
多源最短路径
Floyd
思想
首先记录所有连通边,然后拿各顶点作为中间点,让所有两点都尝试经过这个点,来更新两点间最短距离。
首先更新所有到零的边
可能有人会想,中间点按0-k增加是否会导致错漏,如果最短路径为1->2->0->3,看起来就不是顺序的,首先我们知道,每个节点两点间必然存在,也就是1->2,2->0,0->3,然后会获得2->0->3,1->2->0如果1->2->0->3是最短路径,那么2->0->3一定也是2->3的最短路径,不然为什么不是2->4->3呢
如果最短路径是1->2->4->3,这个就不用验证了,1->2->0->3就算存在,那么也会被1->2->4->3直接取代,因为这是顺序的
ee行吧我证不出来我就是在胡说八道
实现
public void Floyd(int N,int[][] path){
int[][] graph = new int[N][N];
int[][] pathGraph = new int[N][N];
//初始化图,并连接边
for(int i =0;i<N;i++){
for(int j=0;j<N;j++){
graph[i][j] = Integer.MAX_VALUE;
pathGraph[i][j] = -1;
}
}
for(int[] p:path){
int val = p[0];
int a = p[1];
int b = p[2];
graph[a][b] = graph[b][a] = val;
}
/**
每次以不同节点作为中间点,更新图
*/
for(int k=0;k<N;k++){
for(int i=0;i<N;i++){
for(int j=0;j<N;j++){
if(graph[i][j]>graph[i][k]+graph[k][j]){
graph[i][j] = graph[i][k]+graph[k][j];
pathGraph[i][j] = k;
}
}
}
}
for(int i=0;i<N;i++){
for(int j=0;j<N;j++){
if(graph[i][j]!=Interger.MAX_VALUE){
print(i,j,pathGraph);
}
}
}
}
/**
如果是-1,说明两点直接连接
如果不是,那就是i,j之间中间点k了,那我们输出i,拿k,j再次代入
*/
public void print(int i,int j,int[][] pathGraph){
System.out.println(i+"->");
if(pathGraph[i][j]==-1){
System.out.println(j);
return;
}else{
print(pathGraph[i][j],j);
}
}
最小生成树
prim
核心思想
每次取最近点加入
简单的并查集思想,纳入生成树时边值设为0,这个点其余连接生成树的边不可能比这个更小了,所以相当于加入集合
实现
class HeadNode{
Node head;
Node end;
}
class Node{
Node next;
int dist;
int spend;
public Node(int dist,int spend){
this.dist = dist;
this.spend = spend;
}
}
class Solution {
public int[] prim(int N,int[][] path){
HeadNode[] nodes = new HeadNode[N];
int[] spendArr = new int[N];
int len = path.length;
Arrays.fill(spendArr, Integer.MAX_VALUE);
//默认由0出发,所以到0点消耗为0
spendArr[0] = 0;
for(int i=0;i<N;i++){
nodes[i] = new HeadNode();
result[i] = new HeadNode();
}
for(int i=0;i<N;i++){
for(int j=0;j<N;j++){
result[i][j] = Integer.MAX_VALUE;
}
}
//初始化图
for(int i=0;i<len;i++){
int val = path[i][0];
int src = path[i][1];
int dist = path[i][2];
Node newNode = new Node(dist,val);
HeadNode list = nodes[src];
if(list.head==null){
list.head = list.end = newNode;
}else{
list.end.next = newNode;
list.end = list.end.next;
}
}
DFS(0,nodes,spendArr,result);
//判断最小生成树是否成立
for(int i=0;i<N;i++){
if(spendArr[i]!=0) return null;
}
return result;
}
public void DFS(int src,HeadNode[] nodes,int[] spendArr){
Node head = nodes[src].head;
//和dijkstra一样,更新数组
while(head!=null){
int dist = head.dist;
int val = spendArr[src]+head.spend;
if(val<spendArr[dist]){
spendArr[dist] = val;
}
head = head.next;
}
int len = nodes.length;
int nextDistIndex = -1;
int nextDistSpend = Integer.MAX_VALUE;
//选择最接近生成树的点
for(int i=0;i<len;i++){
//如果spendArr等于零,代表加入生成树了,直接跳过不管
if((spendArr[i]<nextDistSpend)&&spendArr[i]!=0){
nextDistSpend = spendArr[i];
nextDistIndex = i;
}
}
//将其加入生成树,并写入结果图
if(nextDistIndex!=-1){
spendArr[nextDistIndex] = 0;
result[src][nextDistIndex] = val;
DFS(nextDistIndex,nodes,spendArr);
}
}
}
kruskal
核心思想
每次取最小边,融入边集
实现
初始化一个visited数组
给所有边排序,每次从小到大取
使用负数代表根节点,数值代表这个根下有几个点
细心的小伙伴可能会发现两个优化点
1 传入的path可以用链表承装,这样每次就可以删除用过的路径了
2 如果将set也返回,就可以利用set快速得到result中的路径,而不用慢慢遍历result
这两点就交给你们辣!!
public int[][] prim(int N,int[][] path){
boolean[] visited = new boolean[N];
int[] set = new int[N];
int[][] result = new int[N][N];
int len = path.length;
Arrays.fill(set,-1);
Arrays.sort(path,new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2){
return o1[0]-o2[0];
}
});
int[][] graph = new int[N][N];
for(int i=0;i<N;i++){
for(int j=0;j<N;j++){
graph[i][j]=Integer.MAX_VALUE;
}
}
for(int i=0;i<N;i++){
boolean success = false;
for(int j=0;j<len;j++){
//说明这条边用过了
if(path[j][0]==-1) continue;
int a= path[j][1];
int b = path[j][2];
int aVal = set[a];
int bVal = set[b];
//寻找根节点
while(aVal>0){
a = set[a];
aval = set[a];
}
while(bVal>0){
b = set[b];
bval = set[b];
}
//说明是两个集合
if(a!=b){
result[a][b] = val;
//由于负数表示,越小数越多,拿数多的合并少的
if(aVal<=bVal){
set[a] = aVal + bVal;
set[b] = a;
}else{
set[b] = aVal + bVal;
set[a] = b;
}
path[i][0] = -1;
success = true;
break;
}
}
if(!success){
//每趟纳入一个节点,如果某一趟没有纳入,说明没有生成树
return null;
}
}
return result;
}