牛牛取快递——从dfs到dijkstra以及堆优化的dijkstra
由于上一篇博客的第三题“牛牛取快递”上次还未ac,因此今天特意再来尝试一下,上次使用暴力dfs搜索最短路径超时的我,在大佬们解题思路的“熏陶”之下,终于在我的记忆深处找回了那被封印依旧的dijkstra算法。
dijkstra算法主要是选择一个初始点s,每次选择一个离s距离最小并未被选过的点t,并通过t作为跳板,更新其余点到s的距离(如果比原来还长就不更新啦)。然后不断重复这个过程即可。(ps:不要和prime搞混啦,虽然他们主要思路很相似)。
然后,题目没审仔细的我用邻接矩阵写出了以下代码。
- 邻接矩阵(堆空间溢出)
import java.util.Scanner; public class Main { static int [][] road; static int min; public static void main(String[] args) { Scanner in = new Scanner(System.in); while (in.hasNextInt()) {//注意while处理多个case int n = in.nextInt(); int M = in.nextInt(); int S = in.nextInt(); int T = in.nextInt(); road = new int[n+1][n+1]; for(int i = 0;i<M;i++){ int s = in.nextInt(); int t = in.nextInt(); int D = in.nextInt(); if(road[s][t]==0||road[s][t]>D) road[s][t] = D; } min = Integer.MAX_VALUE; int res = dijkstra(S, T) + dijkstra(T, S); System.out.println(res); } } static int dijkstra(int s,int t){ // 接受一个有向图的权重矩阵,和一个起点编号start(从0编号,顶点存在数组中) // 返回一个int[] 数组,表示从start到它的最短路径长度 int n = road.length; // 顶点个数 int[] shortPath = new int[n]; // 保存start到其他各点的最短路径 int[] visited = new int[n]; // 标记当前该顶点的最短路径是否已经求出,1表示已求出 // 初始化,第一个顶点已经求出 for(int i = 1;i<n;i++){ shortPath[i] = (road[s][i]==0?Integer.MAX_VALUE:road[s][i]); } shortPath[s] = 0; visited[s] = 1; for (int count = 2; count < n; count++) { // 要加入n-1个顶点 int k = -1; // 选出一个距离初始顶点start最近的未标记顶点 int dmin = Integer.MAX_VALUE; for (int i = 1; i < n; i++) { if (visited[i] == 0 && shortPath[i] < dmin) { dmin = shortPath[i]; k = i; } } if(k==-1){ return shortPath[t]; } // 将新选出的顶点标记为已求出最短路径,且到start的最短路径就是dmin shortPath[k] = dmin; visited[k] = 1; // 以k为中间点,修正从start到未访问各点的距离 for (int i = 1; i < n; i++) { if (visited[i] == 0 && road[k][i]!=0 &&shortPath[k] + road[k][i] < shortPath[i]){//使用k作为跳板,更新未被访问过并且通过k之后离start更近的点的距离 shortPath[i] = shortPath[k] + road[k][i]; } } if(visited[t] == 1){ return shortPath[t]; } } return shortPath[t]; } }
因为题目中这个条件,用邻接矩阵来做,测试数据跑到80%报出了堆溢出,那么我的第一反应自然是把邻接矩阵改成邻接表就成了。
- 邻接表(ac 时间复杂度o(n^2))
既然有了思路,改起来也没有太大的问题,需要注意的是每次更新时应该遍历“跳板点”的边,脑回路奇特的我一开始每次都在邻接表中查找从跳板点到每个点是否有边,如果有则再更新,这样每次查找的耗时极大。代码如下,看到ac还是挺开心的嘿嘿。
static LinkedList<Integer[]>[] road; static int min; public static void main(String[] args) { Scanner in = new Scanner(System.in); while (in.hasNextInt()) {//注意while处理多个case int n = in.nextInt(); int M = in.nextInt(); int S = in.nextInt(); int T = in.nextInt(); road = new LinkedList[n+1]; for(int i = 1;i<n+1;i++){ road[i] = new LinkedList<>(); } for(int i = 0;i<M;i++){ int s = in.nextInt(); int t = in.nextInt(); int D = in.nextInt(); road[s].add(new Integer[]{t,D}); } min = Integer.MAX_VALUE; int res = dijkstra(S, T) ; System.out.println(res); System.out.println("-------------"); res += dijkstra(T, S); System.out.println(res); } } static int dijkstra(int s,int t){ // 接受一个有向图的权重矩阵,和一个起点编号start(从0编号,顶点存在数组中) // 返回一个int[] 数组,表示从start到它的最短路径长度 int n = road.length; // 顶点个数 int[] shortPath = new int[n]; // 保存start到其他各点的最短路径 int[] visited = new int[n]; // 标记当前该顶点的最短路径是否已经求出,1表示已求出 // 初始化,第一个顶点已经求出 for(int i = 1;i<n;i++){ shortPath[i] = Integer.MAX_VALUE; } for(Integer[] kv:road[s]){ if(shortPath[kv[0]]>kv[1]){ shortPath[kv[0]] = kv[1]; } } visited[s] = 1; for (int count = 2; count < n; count++) { // 要加入n-1个顶点 int k = -1; // 选出一个距离初始顶点start最近的未标记顶点 int dmin = Integer.MAX_VALUE/2; for (int i = 1; i < n; i++) { //堆优化对这个寻找过程进行优化 if (visited[i] == 0 && shortPath[i] < dmin) { dmin = shortPath[i]; k = i; } } if(k==-1){ return shortPath[t]; } // 将新选出的顶点标记为已求出最短路径,且到start的最短路径就是dmin shortPath[k] = dmin; visited[k] = 1; int fromToDis; // 以k为中间点,修正从start到未访问各点的距离(一开始在这边用了很蠢的遍历方法) for (Integer[] td : road[to]) { if(visited[td[0]] == 0&&dis[td[0]]>dis[to]+td[1]){ dis[td[0]] = dis[to]+td[1]; } } if(visited[t] == 1){ return shortPath[t]; } } return shortPath[t]; }
- 堆优化(ac,o(nlogn))
ac过后的我,在参考别人的dijstra写法时,发现大多是用一种队列(优先队列)来实现dijstra的,在查阅资料后,使用优先队列,插入[点,距离]是o(logn),取队列头是o(1),对比上述寻找最近节点时遍历n个节点的o(n)有所优化,代码如下。
static int dijkstra(int s, int t) { // 堆优化 int n = road.length; int[] visited = new int[n]; int[] dis = new int[n]; int to, d; Integer[] toDis; PriorityQueue<Integer[]> queue = new PriorityQueue<>((Integer[] o1,Integer[] o2)->o1[1]-o2[1]); for (int i = 1; i < n; i++) { dis[i] = Integer.MAX_VALUE / 2; visited[i] = 0; } dis[s] = 0;// 到自己的距离为0 int newdis; queue.offer(new Integer[] { s, 0 }); while (!queue.isEmpty()) { toDis = queue.element(); queue.poll(); to = toDis[0]; d = toDis[1]; if (visited[to] == 1) { continue; } visited[to] = 1; for (int i = 1; i < n; i++) { newdis = dis[to] + find(to, i); if (visited[i] == 0 && dis[i] > newdis) {// i没去过,并且dis[to]+find(to,i)直接到i的距离大于到to再到i的距离 dis[i] = newdis; queue.offer(new Integer[] { i, newdis }); } } if (visited[t] == 1) { return dis[t]; } } return dis[t]; }
对于最短路径,dijstra基本够用啦,虽然dijstra不适用于有负边图,但是很少能遇到边为负值的图,如果今后能遇到的话,再好好研究一下spfa好惹。
那么对dijstra的研究就到这里啦!