常用10種演算法(二)

MXC肖某某發表於2021-01-12

一、普里姆演算法

  原始碼:普里姆演算法

1,介紹

  普里姆演算法是圖結構中尋找最小生成樹的一種演算法。所謂生成樹,即為連通圖的極小連通子圖,其包含了圖中的n個頂點,和n-1條邊,這n個頂點和n-1條邊所構成的樹即為生成樹。當邊上帶有權值時,使生成樹中的總權值最小的生成樹稱為最小代價生成樹,簡稱最小生成樹。最小生成樹不唯一,且需要滿足一下準則:

  • 只能使用圖中的邊構造最小生成樹
  • 具有n個頂點和n-1條邊
  • 每個頂點僅能連線一次,即不能構成迴路

2,案例

  看一個應用場景和問題:

  • 有勝利鄉有7個村莊(A, B, C, D, E, F, G) ,現在需要修路把7個村莊連通
  • 各個村莊的距離用邊線表示(權) ,比如 A – B 距離 5公里
  • 問:如何修路保證各個村莊都能連通,並且總的修建公路總里程最短?

      常用10種演算法(二)

3,思路

     常用10種演算法(二)

4,實現O(n3)

  採用三層for迴圈,第一層控制生成的邊的條數,第二層表示已經被訪問的頂點,第三層為未被訪問的頂點。獲取滿足這三者的最小權重值

//最終會生成vertexNum-1條邊,故而只需要遍歷vertexNum-1次。當然也可以多遍歷因為後面會有最小值的控制
for (int k = 1; k < graph.vertexNum; k++) {
    min = 10000;
    //表示被訪問的結點
    for (int i = 0; i < graph.vertexNum; i++) {
        //表示未被訪問的結點
        for (int j = 0; j < graph.vertexNum; j++) {
            //如果當前為i結點被訪問,j結點為未被訪問,當前路徑權重小於最小權重
            if (visited[i]==1 && visited[j]==0 && graph.weight[i][j] < min ) {
                //此時記錄最小權重,記錄i結點和j結點
                min = graph.weight[i][j];
                x = i;
                y = j;
            }
        }
    }

    //如果當前結點全部被訪問,則min==10000,否則就有邊未被訪問。
    if (min < 10000) {
        //記錄當前y為被訪問,並輸出此時訪問的邊
        visited[y] = 1;
        System.out.println(getVertex(x) +" -> " +getVertex(y) +"  "+ min);
    }
}

5,優化實現O(n2)

int minIndex:   記錄每輪查詢的最小值對應索引
int[] indexArr:記錄每次訪問的結點對應的索引
int[] visited:  記錄訪問狀態(如果為0表示已訪問)和距離。當查詢到最小值對應的索引minIndex後,如果以minIndex為原點到達各頂點的距離 小於 原始visited的值,就更新visited當前索引值
/**
 * prim演算法(優化)
 *
 * @param data 頂點
 * @param weight 鄰接矩陣
 * @param start 開始頂點
 */
public static void prim(char[] data,int[][] weight,int start) {
    int length = data.length;
    //記錄各最小節點的索引
    int[] indexArr = new int[length];
    //如果當前值為0 表示已經被訪問, 其他情況表示未訪問
    int[] visited = new int[length];

    //獲取start這一行與其他節點的距離
    for (int i = 0; i < length; i++) {
        visited[i] = weight[start][i];
    }
    //標記start行為已訪問
    visited[start] = 0;
    //記錄第一個結點
    int index = 0;
    indexArr[index++] = start;


    int min;
    int minIndex ;
    //記錄權重
    int sum = 0;
    for (int i = 1; i <length; i++) {
        //記錄每輪最小值
        min = 10000;
        //記錄每輪的最小索引
        minIndex = -1;
        //獲取visited中最小值(非零)
        for (int j = 0; j < length; j++) {
            if (visited[j] != 0 && visited[j] < min  ) {
                min = visited[j];
                minIndex = j;
            }
        }

        //證明這一輪沒有最小索引,迴圈結束
        if (minIndex == -1) {
            break;
        }
        //記錄(最小的索引)
        indexArr[index++] = minIndex;
        sum += min;

        //更新visited中的資料
        visited[minIndex] = 0;
        for (int j = 0; j < length; j++) {
            //如果當前visited陣列中j的值  > 鄰接矩陣中從minIndex到j的值  更新visited
            if (visited[j] != 0 && visited[j] > weight[minIndex][j]) {
                visited[j] = weight[minIndex][j];
            }
        }
    }

    System.out.println("總的權重為: " + sum);
    for (int i = 0; i < length; i++) {
        System.out.printf("%s\t",data[indexArr[i]]);
    }
    System.out.println();

}

二、克魯斯卡爾演算法

  原始碼:克魯斯卡爾演算法

1,介紹

  • 克魯斯卡爾(Kruskal)演算法,是用來求加權連通圖的最小生成樹的演算法。
  • 基本思想:按照權值從小到大的順序選擇 n-1 條邊,並保證這 n-1 條邊不構成迴路
  • 具體做法:首先構造一個只含 n 個頂點的森林,然後依權值從小到大從連通網中選擇邊加入到森林中,並使森林中不產生迴路,直至森林變成一棵樹為止

2,案例

  看一個應用場景和問題:

  • 某城市新增7個站點(A, B, C, D, E, F, G) ,現在需要修路把7個站點連通
  • 各個站點的距離用邊線表示(權) ,比如 A – B 距離 12公里
  • 問:如何修路保證各個站點都能連通,並且總的修建公路總里程最短?

      常用10種演算法(二)

3,思路

  • 對所有的邊按照權值大小進行排序
  • 新增權值最小並且不構成迴路的邊

  如果判定新新增的邊是否與原來的邊構成迴路?新新增邊的兩個頂點的終點不相同,下面展示如果求指定索引的終點:

/**
 * 查詢當前索引對應的終點的索引
 *  如果當前索引對應的終點為0,則直接返回自己,
 *  如果不為0記錄向後找當前的終點  迴圈得到其終點
 *  例如: E的終點為F D的終點為E(此時會通過E找到F) 那麼就是D和E的終點均為F
 * @param end   陣列就是記錄了各個頂點對應的終點是哪個,ends 陣列是在遍歷過程中,逐步形成
 * @param i     表示傳入的頂點對應的下標
 * @return  返回的就是 下標為i的這個頂點對應的終點的下標
 */
public int getEnd(int[] end,int i) {
    while (end[i] != 0) {
        i = end[i];
    }
    return i;
}

4,程式碼實現

  遍歷圖的右半邊,將其構建成一個儲存所有邊的陣列。對邊進行排序(升序),遍歷每條邊獲取其start和end的終點是否相同,不同就加入結果集,並更新終點陣列

/**
 * 克魯斯卡爾演算法
 */
public void kruskal() {
    System.out.println("排序前:"+Arrays.toString(edges));
    sort(edges);
    System.out.println("排序後:"+Arrays.toString(edges));
    //最終的結果肯定是length-1條
    Edge[] res = new Edge[vertexs.length-1];
    int index = 0;

    for (int i = 0; i < edges.length; i++) {
        int start = getIndexByChar(edges[i].start);
        int end = getIndexByChar(edges[i].end);
        //獲取start結點的終點
        int startParent = getEnd(ends, start);
        //獲取end結點的終點
        int endParent = getEnd(ends, end);
        //如果兩個結點的 終點不是同一下標 則記錄
        if (startParent != endParent) {
            //將當前邊加入到結果集中
            res[index++] = edges[i];
            //同時更新start結點的終點為end結點的終點
            ends[startParent] = endParent;
        }
    }

    for (int i = 0; i < res.length; i++) {
        System.out.println(res[i]);
    }

}

三、迪傑斯特拉演算法

  原始碼:迪傑斯特拉演算法

1,介紹

  迪傑斯特拉(Dijkstra)演算法是典型最短路徑演算法,用於計算一個結點到其他結點的最短路徑。它的主要特點是以起始點為中心向外層層擴充套件(廣度優先搜尋思想),直到擴充套件到終點為止。

2,案例

  看一個應用場景和問題:

  • 戰爭時期,勝利鄉有7個村莊(A, B, C, D, E, F, G) ,現在有六個郵差,從G點出發,需要分別把郵件分別送到 A, B, C , D, E, F 六個村莊
  • 各個村莊的距離用邊線表示(權) ,比如 A – B 距離 5公里
  • 問:如何計算出G村莊到 其它各個村莊的最短距離?
  • 如果從其它點出發到各個點的最短距離又是多少?

    常用10種演算法(二)

3,思路

  • 從指定起點開始,找出所有鄰接節點更新起點到鄰接節點路徑權值和記錄的前驅節點,從中選出路徑權值最小的一個節點,作為下一輪的起點
  • 從次輪起點開始,重複第一輪的操作
  • 每一輪更新記錄的路徑權值,是把 “記錄得原始起點到該目標節點的路徑總權值” 與 “記錄中原始起點到本輪起點的路徑權值 + 本輪起點到鄰接節點的權值” 比較,保留小的
  • 更新了權值的同時要記得更新路徑終點的前驅節點
  • 每一輪都將此輪的起點設定為已訪問,並且尋找鄰接節點時也要跳過那些已訪問的
  • 所有節點都"已訪問"時結束

4,程式碼實現

思路:1,構建一個vistedvetex物件(pre陣列記錄到達當前頂點的上一個頂點索引;already陣列記錄當前頂點是否被訪問;dis陣列記錄從初始頂點出發到達當前頂點的距離2,初始化根據初始頂點,構建:除初始頂點以外所有dis均為最大值(65535),除初始頂點以外的所有頂點均為被訪問
     3以index(第一次index為初始頂點,之後均為獲取當前dis陣列中未被訪問的最短距離為index,並標記當前index為已訪問)
           作為基準點,獲取當前dis中index的值(該值則為從初始結點到index的距離) + index到i(遍歷圖變化) 距離之和 記為len,
           用len與dis中i的值進行比較:如果len < dis[i],則說明以index作為基準點 更為合適,此時更新dis[i] = len ; pre[i] = index
/**
 * 迪傑斯特拉演算法
 *
 * @param index 初始頂點的座標
 */
public void dsj(int index) {
    //初始化visitedVetex
    vv = new VisitedVetex(index, vertex.length);
    //以index作為介質,更新visitedvetex中的dis和pre
    update(index);
    //記錄當前儲存在dis陣列中 並 未被訪問的最小節點的距離
    int i;
    //如果當前i==-1 ,則表示dis陣列中所有的資料均被訪問過
    while ((i = vv.updateArr()) != -1) {
        //以i作為介質  更新vv
        update(i);
    }

    //列印
    vv.show();
}

/**
 * 更新以當前結點為 介質 的visitedvetex的 dis和pre陣列
 *
 * @param index 當前結點的索引
 */
private void update(int index) {
    //遍歷以index作為結點  獲取 與其連線的所有結點
    for (int i = 0; i < matrix[index].length; i++) {
        //len為  從index到i結點的鄰接距離 + 儲存在dis陣列中的起始點到index的距離
        int len = matrix[index][i] + vv.dis[index];
        //如果len < 儲存在dis陣列中的起始點到i 結點的距離  並且當前i結點未被訪問過(只有i在dis陣列中為最小的數時才會被訪問)
        if (len < vv.dis[i] && !vv.isVisited(i)) {
            //更新dis陣列中 起始點到i的距離為len
            vv.updateDis(i, len);
            //修改當前i結點的前驅結點為index
            vv.updatePre(i, index);
        }
    }
}

四、弗洛伊德演算法

  原始碼:弗洛伊德演算法

1,介紹

  • 弗洛伊德(Floyd)演算法也是一種用於尋找給定的加權圖中頂點間最短路徑的演算法
  • 弗洛伊德演算法(Floyd)計算圖中各個頂點之間的最短路徑
  • 迪傑斯特拉演算法用於計算圖中某一個頂點到其他頂點的最短路徑。
  • 弗洛伊德演算法 VS 迪傑斯特拉演算法:
    • 迪傑斯特拉演算法通過選定的被訪問頂點,求出從出發訪問頂點到其他頂點的最短路徑;
    • 弗洛伊德演算法中每一個頂點都是出發訪問點,所以需要將每一個頂點看做被訪問頂點,求出從每一個頂點到其他頂點的最短路徑。

2,案例

  • 勝利鄉有7個村莊(A, B, C, D, E, F, G)
  • 各個村莊的距離用邊線表示(權) ,比如 A – B 距離 5公里
  • 問:如何計算出各村莊到其它各村莊的最短距離?

        常用10種演算法(二)

3,思路

  • 設定頂點vi到頂點vk的最短路徑已知為Lik,頂點vk到vj的最短路徑已知為Lkj,頂點vi到vj的路徑為Lij,則vi到vj的最短路徑為:min((Lik+Lkj),Lij),vk的取值為圖中所有頂點,則可獲得vi到vj的最短路徑
  • 至於vi到vk的最短路徑Lik或者vk到vj的最短路徑Lkj,也是以同樣的方式獲得

4,程式碼實現

/**
 * 弗洛伊德演算法 : 以i為中間點 比較j->i->k 的距離與 j->k的距離
 *  如果採用i作為中間頂點距離短則記錄i為前驅頂點,並更新距離
 *  如果是j->k 距離更短  則直接記錄j
 * @param vetex 頂點集合
 * @param w     鄰接矩陣
 */
public static void floyd(char[] vetex, int[][] w) {
    //儲存前驅結點
    char[][] pre = new char[vetex.length][vetex.length];
    // 對pre陣列初始化, 注意存放的是前驅頂點的下標
    for (int i = 0; i < vetex.length; i++) {
        Arrays.fill(pre[i], vetex[i]);
    }

    //作為中間結點
    for (int i = 0; i < vetex.length; i++) {
        //初始結點
        for (int j = 0; j < vetex.length; j++) {
            //終點
            for (int k = 0; k < vetex.length; k++) {
                //j->i->k的距離
                int len = w[j][i] + w[i][k];
                if (w[j][k] > len) {
                    w[j][k] = len;
                    //記錄前驅結點為中間結點
                    pre[j][k] = pre[i][k];
                }
            }
        }
    }
}

五、馬踏棋盤演算法(騎士周遊問題)

  原始碼:馬踏棋盤演算法

1,介紹

  將馬隨機放在國際象棋的8×8棋盤Board[0~7][0~7]的某個方格中,馬按走棋規則(馬走日字)進行移動。要求每個方格只進入一次,走遍棋盤上全部64個方格

  常用10種演算法(二)

2,思路

  • 馬踏棋盤問題(騎士周遊問題)實際上是圖的深度優先搜尋(DFS)的應用。
  • 編碼思路
    • 建立棋盤 chessBoard,是一個二維陣列
    • 當前位置設定為已經訪問,然後根據當前位置,計算馬兒還能走哪些位置,並放入到一個集合中(ArrayList),下一步可選位置最多有8個位置, 每走一步,就使用 step+1
    • 遍歷ArrayList中存放的所有位置,看看哪個可以走通,如果走通,就繼續,走不通,就回溯
    • 判斷馬兒是否完成了任務,使用 step 和應該走的步數比較 , 如果沒有達到數量,則表示沒有完成任務,將整個棋盤置 0
    • 注意:馬兒不同的走法(策略),會得到不同的結果,效率也會有影響(優化)

3,程式碼實現

/**
 * 馬踏棋盤演算法核心
 *
 * @param x    初始棋子在棋盤的哪一行
 * @param y    初始棋子在棋盤的哪一列
 * @param step 當前是第幾步
 */
public void start(int x, int y, int step) {
    //首先儲存對應的步數
    chess[x][y] = step;
    //獲取當前x,y對應的 visited陣列中的索引位置
    int index = x * Y + y;
    //標記為已訪問
    visited[index] = true;
    //構建當前point
    Point currentPoint = new Point(x, y);
    List<Point> ps = next(currentPoint);
    //對ps進行排序,排序的規則就是對ps的所有的Point物件的下一步的位置的數目,進行非遞減排序
    sort(ps);
    while (!ps.isEmpty()) {
        //將第一個元素移除
        Point removePoint = ps.remove(0);
        //未被訪問過,則呼叫當前
        if (!visited[(removePoint.x) * Y + removePoint.y]) {
            start(removePoint.x, removePoint.y, step + 1);
        }
    }

    //回溯
    if (step < X * Y && !finished) {
        chess[x][y] = 0;
        visited[index] = false;
    } else {
        finished = true;
    }

}

/**
 * 對ps集合進行非遞減排序
 * @param ps    集合
 */
private void sort(List<Point> ps) {
    ps.sort((p1, p2) -> {
        //獲取當前p1和p2  各自對應的
        List<Point> next1 = next(p1);
        List<Point> next2 = next(p2);
        return next1.size() - next2.size();
    });
}

private List<Point> next(Point currentPoint) {
    List<Point> ps = new ArrayList<>();
    int x, y;
    //0
    if ((x = currentPoint.x - 1) >= 0 && (y = currentPoint.y + 2) < Y) {
        ps.add(new Point(x, y));
    }
    //1
    if ((x = currentPoint.x + 1) < X && (y = currentPoint.y + 2) < Y) {
        ps.add(new Point(x, y));
    }
    //2
    if ((x = currentPoint.x + 2) < X && (y = currentPoint.y + 1) < Y) {
        ps.add(new Point(x, y));
    }
    //3
    if ((x = currentPoint.x + 2) < X && (y = currentPoint.y - 1) >= 0) {
        ps.add(new Point(x, y));
    }
    //4
    if ((x = currentPoint.x + 1) < X && (y = currentPoint.y - 2) >= 0) {
        ps.add(new Point(x, y));
    }
    //5
    if ((x = currentPoint.x - 1) >= 0 && (y = currentPoint.y - 2) >= 0) {
        ps.add(new Point(x, y));
    }
    //6
    if ((x = currentPoint.x - 2) >= 0 && (y = currentPoint.y - 1) >= 0) {
        ps.add(new Point(x, y));
    }
    //7
    if ((x = currentPoint.x - 2) >= 0 && (y = currentPoint.y + 1) < Y) {
        ps.add(new Point(x, y));
    }
    return ps;
}

 

相關文章