【面向對象】記一次錯誤的Dijkstra算法優化—動態規划與貪心


沒有學過算法,請各位大佬們輕拍

本文將簡單比較一下圖論中最短路的兩大最短路算法:Floyd(弗洛伊德)算法與Dijkstra(迪傑斯特拉)算法,並闡述一下兩大算法背后的算法原理(動態規划與貪心),並記錄一下由於對算法本質理解不透徹,我是怎么把自己坑了。

Floyd(弗洛伊德)算法

Floyd算法本質上是一種動態規划算法,又稱“插點法”。可以形象的解釋為“如果兩點間的路徑長度,大於這兩點通通過第三點連接的路徑長度,那么就修正這兩點的最短路徑”。

Floyd算法的Java實現如下

public class GraphAlgorithm {
    public static final int INFINITY = Integer.MAX_VALUE >> 4;
    
    public static void floyd(HashMap<Integer, HashMap<Integer, Integer>> graph) {
        for (Integer k : graph.keySet()) {
            for (Integer i : graph.keySet()) {
                if (k.equals(i)) {
                    continue;
                }
                int ik = graph.get(i).get(k);
                if (ik == INFINITY) {
                    continue;
                }
                for (Integer j : graph.keySet()) {
                    int ij = graph.get(i).get(j);
                    int kj = graph.get(k).get(j);
                    if (ik + kj < ij) {
                        graph.get(i).put(j, ik + kj);
                    }
                }
            }
        }
    }
}

非常簡明的三重循環,是一個動態規划算法。

就是一個三重循環權值修正,就完成了所有頂點之間的最短路計算,時間復雜度是O(n^3)

其實Floyd算法很好理解和實現,沒什么好說的,本質就是動態規划

Dijkstra(迪傑斯特拉)算法

Dijkstra算法本質上是一種貪心算法

迪傑斯特拉算法主要特點是以起始點為中心向外層層擴展,從一個頂點到其余各頂點的最短路徑算法,直到擴展到終點為止。

很難受。Dijkstra算法是一種單源最短路算法,在算法的緩存優化中,我忽略了必須是最短路為真的條件必須是“其余n-1個節點均得到最短路徑”

下面是錯誤的堆優化並緩存的dijkstra代碼,然后分析原因

public class GraphAlgorithm {
    public static final int INFINITY = Integer.MAX_VALUE >> 4;
    // 堆中保存的數據節點
    public class HeapNode implements Comparable {
        private int value;
        private int id;
        public HeapNode(int id, int value) {
            this.value = value;
            this.id = id;
        }
        public int getValue() {
            return value;
        }
        public int getId() {
            return id;
        }
        @Override
        public int compareTo(Object o) {
            return Integer.compare(value, ((HeapNode) o).getValue());
        }
    }
    // 堆優化的迪傑斯特拉算法
    public static void dijkstraWithHeap(
            HashMap<Integer, HashMap<Integer, Integer>> graph,
            int fromNodeId, int toNodeId) {
        PriorityQueue<HeapNode> sup = new PriorityQueue<>();
        HashMap<Integer, Integer> dist = new HashMap<>();
        Set<Integer> found = new HashSet<>();
        for (Integer vertex : graph.keySet()) {
            dist.put(vertex, INFINITY);
        }
        dist.put(fromNodeId, 0);
        sup.add(new HeapNode(fromNodeId, 0));
        while (!sup.isEmpty()) {
            HeapNode front = sup.poll();
            int nowShortest = front.getId();
            int minWeight = front.getValue();
            // 此處更新緩存?好像可以?我不知道
            graph.get(fromNodeId).put(nowShortest, minWeight);
            graph.get(nowShortest).put(fromNodeId, minWeight);
            if (nowShortest == toNodeId) { // 致命錯誤,此處不能結束函數
                return;
            }
            found.add(nowShortest);
            for (Integer ver : graph.get(nowShortest).keySet()) {
                int value = graph.get(nowShortest).get(ver);
                if (!found.contains(ver) && minWeight + value < dist.get(ver)) {
                    dist.put(ver, minWeight + value);
                    sup.add(new HeapNode(ver, minWeight + value));
                }
            }
        }
        graph.get(fromNodeId).put(toNodeId, INFINITY);
        graph.get(toNodeId).put(fromNodeId, INFINITY);
    }
}

Dijkstra是一種貪心算法,所有的最短路都只是基於已知情況做出的判斷,所以在堆不為空(朴素Dijkstra是沒有遍歷完其余n-1個節點)之前不能結束算法,否則得到的答案可能是錯誤的。

此前沒有發現這個問題是因為數據量不夠大,只有1000余條指令,所以這樣的Dijkstra算法沒有出錯。

當數據量增大到5000條,其中384條最短路查詢指令,有13條出錯。仔細排查后才發現是Dijkstra的問題。(然而這時候提交時間已經截至了,C組預定🙃)

而Dijkstra算法不止最短路矩陣使用了,最少換成、最少票價、最小不滿意度矩陣均使用了Dijkstra算法,但這些指令沒有出錯。我認為原因如下:圖的鄰接表在數據量大的情況下,是一個稠密圖,Dijkstra算法提前結束會導致緩存結果並非實際的最短路。

主要還是沒有理解貪心算法的本質,導致了錯誤的修改。

貪心算法是指,在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,貪心算法所做出的是在某種意義上的局部最優解。

以后一定好好學習算法。不知道以后算法課會不會很難……

那么正確的緩存方式應該是這樣:

public class GraphAlgorithm {
    public static final int INFINITY = Integer.MAX_VALUE >> 4;
    // 堆優化的迪傑斯特拉算法
	public static void dijkstraWithHeap(
            HashMap<Integer, HashMap<Integer, Integer>> graph,
            int fromNodeId, int toNodeId) {
        PriorityQueue<HeapNode> sup = new PriorityQueue<>();
        HashMap<Integer, Integer> dist = new HashMap<>(graph.size());
        Set<Integer> found = new HashSet<>();
        for (Integer vertex : graph.keySet()) {
            dist.put(vertex, INFINITY);
        }
        dist.put(fromNodeId, 0);
        sup.add(new HeapNode(fromNodeId, 0));
        while (!sup.isEmpty()) {
            HeapNode front = sup.poll();
            int nowShortest = front.getId();
            int minWeight = front.getValue();
            if (found.contains(nowShortest)) {
                continue;
            }
            found.add(nowShortest);
            for (Integer ver : graph.get(nowShortest).keySet()) {
                int value = graph.get(nowShortest).get(ver);
                if (!found.contains(ver) && minWeight + value < dist.get(ver)) {
                    dist.put(ver, minWeight + value);
                    sup.add(new HeapNode(ver, minWeight + value));
                }
            }
        }
        // 最后緩存數據
        for (Integer ver : dist.keySet()) {
            int minWeight = dist.get(ver);
            graph.get(fromNodeId).put(ver, minWeight);
            graph.get(ver).put(fromNodeId, minWeight);
        }
    }
}

本來可以是開心的A組,開心的滿分,結果……唉😔

聯想:貪心與動態規划——不恰當的貪心導致出錯

關於貪心和動態規划,讓我想起來了一類很經典的題型,最少的錢的張數:

現在有5元、4元、3元以及1元的紙幣,問7元最少要多少張紙幣?

如果按照簡單的貪心策略,就是7 = 5 + 1 + 1,但這顯然是錯的,顯然7 = 4 + 3才是最優解。

如果是動態規划就不存在這個問題。

原題我記不清楚了,只記得大概坑點就是這個。當時看了題解才知道坑點是這個。

(可惜當時太菜了不懂啥事動態規划,現在也菜)

大概就這樣。算法真有趣。

請大佬們多多補充,說的不對或者不好的糾正一下。


2019.5.16

我果然強測涼了🙃果然C組🙃


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM