由于上一篇博客的第三题“牛牛取快递”上次还未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了吧,我边祈祷边按下了“提交”按钮。
static LinkedList<Integer[]>[] road;//邻接表 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 (int i = 1; i < n; i++) { if(visited[i] == 0){ fromToDis = find(k, i); if(fromToDis + shortPath[k]< shortPath[i]){ shortPath[i] = fromToDis+shortPath[k]; } } } if(visited[t] == 1){ return shortPath[t]; } } return shortPath[t]; } //从邻接表查找from到to有路的距离 static int find(int from,int to){ int res = Integer.MAX_VALUE/2; for (Integer[] kv: road[from]) { if(kv[0] == to && kv[1]<res){ res = kv[1]; } } return res; }
结果很快就出来了,可是,嗯?还是80%的通过率?不过这次是超时了,邻接矩阵可以一次查出从a节点到b节点是否有边,距离是多少,而邻接表则需要查询一个链表,效率自然是变低了,那么是否有优化的办法呢?
经过查阅资料后,我发现了dijstra堆优化的一种算法,通过一个优先队列记录下一个点以及离原点的距离,每次取队列的头判断该节点是否加入过,若没加入过,就用该节点更新其余点的距离,感觉似乎是比普通的方法快一些,堆的插入以及取头都是o(logn)的,很快,堆优化的dijstra也码出来了。
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]; }
嗯,结果也不算出乎意料,同样只有80%的通过率,最主要的问题应该在于dijstra每次更新距离时需要查找是否存在a到b的边,以及其最短距离,因此每次查询的效率较低,而堆优化的性能在测试用例中效果并不是特别明显吧。
从邻接矩阵到邻接表再到堆优化的dijstra,经过三次编码之后,虽然通过率没有提高,但是能dijstra的思路基本是不会再忘了的(毕竟前后三次从满怀期待的编码到仅有80%通过而产生的失落),尤其是prime和dijstra的不同之处(其实一开始我以为是同一种思路嘿嘿),下次应该会学习spfa算法再尝试本题,因为看到不少c++通过的都是用spfa算法做的。这也算自己日后需要填的一个坑吧。