LC1263-AI尋路優化: 距離優先bfs -> heuristic + A* -> tarjan + A*

原題連接 java

程序在文末算法

1.分析輸入數據數據結構

 

 輸入的地圖的大小在 1 ~ 20,規模小,若是用dfs或bfs,而且每一個點最多訪問一次,則最多訪問 400 個點ide

 推測dfs和bfs訪問一個點的過程當中須要調用其餘複雜函數,如此一來時間消耗才合理,由於單純訪問400個函數

 點20次(leetcode的測試用例通常在20左右)可能連1ms都用不到,一般來講一道題耗時 > 1ms測試

 

2.分析題目類型優化

遊戲題目,和地圖相關,和圖結構相關,並且和路徑搜索相關,很天然聯想到 bfs 或 dfsthis

可是不能只對玩家或者箱子使用bfs或dfs,由於要求人推着箱子,把箱子推到目的地,人和箱子都是要動的。spa

因此要把人和箱子的狀態結合起來使用bfs或dfs,因而有解決方案1 : bfs + 優先隊列(存狀態)。3d

下文的狀態幾乎都是指(人的位置 與 箱子的位置)

如下圖爲例,紫色表明玩家,橙色表明箱子,綠色表明箱子最後要到達的位置(Target),### 表示牆壁, ... 表明可走位置

 

 首先要解釋優先隊列以什麼優先,本題要作的是求箱子推到Target位置的最短推進次數,也就是箱子移動的距離,因此bfs以箱子移動到Target位置的曼哈頓距離爲優先級(距離優先bfs)

 每次都先考慮箱子離Target最近的狀態的話,那麼最後箱子移動的次數是最少的,而這裏度量 「近」 的標準是 「曼哈頓距離」。

 其實很簡單,就是起始點和終點的 垂直距離絕對值 + 鉛垂距離絕對值,起始點是箱子位置,終點是Target位置,因而當前狀態(包含箱子和人的狀態)的優先級 = 1 + 2

 

 

 爲何曼哈頓距離小,就代表箱子離Target近?

 

 由於只能上下左右移動,不能像上圖這樣直接穿過去,若是能夠的話能夠直接使用初中教的歐式距離(distance = (dx ^ 2 + dy ^ 2) ^ (1/2))

 按BFS的走法,上圖能走到如下兩種狀態,兩種狀態的箱子到Target的曼哈頓距離相同,因此優先級同樣。

 若是把這兩種狀態放到優先隊列裏,則下一次pop出來的是二者任一(確切地說應該是先進去的那個)

         

 

兩種狀態接着向下走,狀態2.3和狀態1.1相同,捨棄。並且要記得不能往回走,也就是狀態1.1不能走回狀態1,因此在程序中須要一個數據結構標記已經走過哪些狀態

            

 

 狀態1.1,2.2,箱子離Target的曼哈頓距離同樣,且都大於2.4的曼哈頓距離

 因此,2.4優先被選中,依此狀態接着走,到2.4.2忽略掉中間過程,箱子不動玩家動的幾個狀態。

 

 

 獲得上面這兩個狀態,答案就清晰明瞭了,由於兩個狀態最後都能把箱子推到Target,二者推進箱子的次數同樣,且都是最少次數,也即箱子最少移動步數。

 只要箱子推到Target就返回,因此優先級比較低的1.1和2.2沒有繼續走下去,減小了執行次數。就算高優先級的狀態最後走不通也無所謂,由於還有低優先級的走法存在,若是全部低優先級的走法

 都不能讓人推着箱子到Target,那確實走不通。

 上面的想法看似不錯,但還不夠,問題在於,題目只關注箱子移動的最少次數,重點在箱子,而咱們目前是把目光彙集在人上,讓人帶動着箱子動。這樣的話,在人還沒貼近箱子的狀況下,也就是

 除了狀態2.4,2.41,2.42 外,其餘狀態都是在像無頭蒼蠅同樣處處亂撞,由於優先級都同樣,這樣的話就添加了不少無所謂的狀態。

 如何減小這些可有可無的狀態?

 咱們把目光放到箱子上,而不是人上。由於想要獲得的是箱子到Target的最短距離,因此應該以箱子爲重點,找箱子到Target距離最短的路徑,再讓人到達能推箱子的位置

 怎麼找到箱子到Target的最短路徑?A*尋路能夠幫咱們解決問題,A* 的優先級函數 f = g + h . 其中 g爲箱子已經移動的次數,h 爲當前狀態的箱子到Target的哈夫曼距離

 把箱子的起始點放入優先隊列,每次都取優先隊列中優先級函數值最小的點(訪問該點),而且把這個點周圍未訪問的點加上優先級後入隊,直到訪問到Target

 放入優先隊列的其實不僅是箱子的位置,還要帶上人的位置,(把 人的位置 + 箱子的位置 稱爲狀態),由於須要知道人能不能到達某個位置去推如今的箱子,讓箱子到某個位置上

 固然,爲了保證步數最少,而且減小運算次數,須要記錄每一個狀態的箱子移動步數。若是當前訪問狀態步數 大於等於已經記錄過的步數,則直接跳過當前狀態

 一個例子:

紫色 = 人,綠色 = Target 終點,紅色 = 箱子,橙色方塊表明箱子bfs可能選擇的位置,上面的數字表示bfs的延伸順序

其實每一個狀態的箱子都有4個方向能夠走,可是隻有方向1和方向2能更接近Target,因此無視3,4,只有1,2走不通才考慮3,4

同理,沒有表現狀態1往方向1移動後的狀態,由於雖然箱子往方向1移動和往方向2的移動步數(累積)相同,可是方向1的位置與Target終點的曼哈頓距離大於方向2的位置

只有往方向2走不通才考慮方向1

 我看過一種寫法是用 Map 來保存狀態和移動步數的,若是已經記錄的當前狀態的移動步數小於等於 父狀態移動步數 + 1,就直接返回,不然則把當前狀態的步數更新爲新的最小值

 這是一種相似迪傑斯特拉的寫法,可是要知道 A* 已是自帶迪傑斯特拉的了。並且筆者試了這種用 Map 保存移動步數的方法。發現 <= 號 打成 < 的話,運行時間天差地別。

 後來發現緣由,沒有使用數據結構來標記bfs過程當中哪些點走過了,因此只能單純用距離來判斷是否訪問過,若是沒有不是 <= ,而是 < 的話,就會形成一樣狀態重複訪問

 以下就是一個訪問循環

 代價以下

 

 

 表如今代碼上就只是一個 <= 和 < 的區別,速率卻快了 200倍不僅,因此BFS和DFS必定要作好標記!

//17ms
if
((distance = distances.get(nextState)) != null && distance <= fatherDistance + 1) { continue; }

//457ms
if ((distance = distances.get(nextState)) != null && distance < fatherDistance + 1) {
     continue; }

可能會有疑問,爲何是用人的位置 + 箱子的位置 作爲狀態,而不是隻用箱子的位置?不是重點研究箱子嗎?

舉極端例子,紫色=人,紅色=箱子,綠色=Target終點,黑色=牆

若是隻考慮箱子的位置,簡單看出 狀況2是由狀況1 變化而來的,狀況2比狀況1移動箱子的次數多

可是狀況1和狀況2的箱子位置相同,因此狀況2被忽略,而狀況1必須變成狀況2才能把箱子推到綠色的Target,狀況1本身沒法直接把箱子推到Target

最後的結果是沒法到達Target,誤判。

 

讀者可能還有個問題:A* 尋路真的能保證箱子移動最少次數嗎?A* 考慮的是 移動距離 + 曼哈頓距離 的和,找的是綜合最短的路徑。最短路徑,必然是箱子移動距離最少。

其實A* 算法是 迪傑斯特拉 + 啓發算法(曼哈頓距離)的綜合體,其中迪傑斯特拉能夠找到最短路徑,而啓發函數只是加快了 箱子向Target終點的收斂速度

 

 還有一個疑問:怎麼判斷人是否能夠推進箱子?

 首先要保證人能到推進箱子的那個位置

 

 下圖爲例:若是人(紫色)要把箱子(紅色)推到位置2,那麼人應該要能達到位置4才能夠,也就是本來箱子位置的反方向

 其實很簡單,只要從人的位置開始,bfs到這個位置就能夠了。bfs 的路程上遇到牆就不訪問

 

 可是須要注意一種特殊狀況:人是不能穿過箱子的(黑色 = 牆,紅色=箱,紫色=人,綠色=Target終點)

 下面這種狀況,人就沒法直接到達2,把箱子推到4

  接着這張圖討論:

  可是對人只用bfs的話,人仍是會像無頭蒼蠅同樣亂竄,竄到位置4,因此,接下去的想法和對箱子的想法相似,要減小狀態,加速人到位置4的收斂速度。

  可是對人不須要使用 A* ,只須要使用啓發函數就足夠了,由於對於人,咱們只用判斷是否能到達位置4。也就是以前的A*算法,把 f = g + h , 改成 f = h

  不須要步數(g)影響優先級,只須要快速收斂便可。(人用heuristic + 箱子用A*)

 

  可是,每次判斷人是否能到某個指定位置(好比位置4)都要至少曼哈頓距離次訪問(好比下圖就是 2 + 1 次訪問),才能知道是否能到達那個位置。

  並且是沒有障礙物的前提下,有障礙物時間複雜度會更高。

  咱們對於人的判斷只用判斷能不能到就能夠了,是否有一種算法能作到直接告訴我是否能到呢(時間複雜度O(1))?而不用搜索?

  這就是咱們的最後一步優化:

  Tarjan算法:

  使用Tarjan算法的目的是找到全部的強連通份量

  強連通份量在有向圖中指的是能夠互相訪問的節點和他們的邊造成的子圖

  好比:下圖中 2, 3, 4 以及他們互聯的邊組成的子圖就是一個強連通份量,5,6,7亦同

  在無向圖中,只要兩個點相鄰,就能夠認爲二者能相互連通,因此無向圖造成強連通較爲簡單。

  下圖中1,2,3,4,5,6,7 造成強連通份量

 

 

 

  把地圖的每一個方格當作是圖的節點,把整個地圖當作有向圖,下圖黑色的是牆,那麼整個地圖被分割成左邊的綠色強連通份量和右邊藍色的強連通份量。

 

  咱們想經過顏色,或者說爲同一個強連通份量裏的全部方格(位置)賦上一個相同的值,這樣就能夠直接判斷玩家是否能從一個點到另外一個點,若是兩點的值不一樣,則不屬於同一個強連通

  份量,不能到達。判斷的時間複雜度從最壞狀況的 O(N) (N 爲方格數,最壞狀況是幾乎全部點都訪問才肯定是否能到達) 降爲 O(1);

  如下的地圖被分爲三個強連通份量,每種顏色表明一個。

  

 

 

  可是實際上,這個方法是有BUG的,必須箱子一開始和人在同一個強連通份量才能正確執行。

  像下面這種狀況,由於人沒法到達箱子周圍的 1, 2, 3,4 這四個中的任一位置(由於人在綠色強連通份量,4個位置都在藍色強連通份量,顏色不一樣)

  雖然人是能推箱子到Target的,可是會誤判成沒法推到Target

 

 LeetCode 的測試用例不足?讓我過了?

 

  18個測試用例。

 

  98%,還差2%,也許是一些細節上還有待改進

 

 heuristic + A* :

public class HeuristicAstart {
    //浪費了一個下午的教訓 : 不要亂用位運算,算hashCode是先移位再異或,否則會移出邊界的
    //還有就是,迪傑斯特拉的距離,只要是 以前的距離 <= 當前距離,均可以棄掉當前的距離不用
    //哇,這個真的是,浪費了我一個下午排查

    static int count = 0;
    static int skip = 0;
    static int stateCount = 0;

    public int minPushBox(char[][] grid) {
        doMain(grid);
        return walk();
    }
    
    static Vector boxStart;
    static Vector myStart;
    static Vector target;
    static Astart.TarJan jan;
   
    static char[][] map;

    static Vector[] ops = new Vector[]{new Vector(-1, 0), new Vector(1, 0), new Vector(0, -1), new Vector(0, 1)};
final static class Vector implements Comparable {
        public int[] vec;
        double priority;
        int hashCode;public void setPriority(double priority) {
            this.priority = priority;
        }

        public Vector(int... vec) {
            this.vec = vec;
            for (int value : vec) {
                this.hashCode <<= 8;
                this.hashCode |= value;
            }
        }

        public Vector combine(Vector b) {
            int[] cA = new int[vec.length + b.vec.length];
            System.arraycopy(vec, 0, cA, 0, vec.length);
            System.arraycopy(b.vec, 0, cA, vec.length, b.vec.length);
            return new Vector(cA);
        }

        public Vector slice(int from, int to) {
            int[] vec1 = new int[to - from + 1];
            if (to + 1 - from >= 0){
                System.arraycopy(vec, from, vec1, 0, to + 1 - from);
            }
            return new Vector(vec1);
        }

        public Vector add (Vector vector) {
            int[] vec1 = new int[vector.vec.length];
            for (int i = 0; i < vec.length; i ++) {
                vec1[i] = vec[i] + vector.vec[i];
            }
            return new Vector(vec1);
        }

        public Vector sub (Vector vector) {
            int[] vec1 = new int[vector.vec.length];
            for (int i = 0; i < vec.length; i ++) {
                vec1[i] = vec[i] - vector.vec[i];
            }
            return new Vector(vec1);
        }

        public double distancePow2(Vector vector) {
            double res = 0;
            for (int i = 0; i < vec.length; i ++) {
                res += Math.abs(vector.vec[i] - vec[i]);
            }
            return  res;
        }

     //減小 hashCode 的重複運算 @Override
public int hashCode() { return hashCode; } @Override public int compareTo(Object o) { if (!(o instanceof Vector)) { throw new RuntimeException("Not a vector"); } Vector vector = (Vector) o; return (int) (this.priority - vector.priority); } @Override public boolean equals(Object o) { if (!(o instanceof Vector)) { throw new RuntimeException("Not a vector"); } Vector vector = (Vector) o; return vector.hashCode == this.hashCode; } } public static void doMain(char[][] set){ map = set; for (int i = 0 ; i < set.length; i ++) { for (int j = 0; j < set[0].length; j ++) { if (set[i][j] == 'S') { myStart = new Vector(j, i); } if (set[i][j] == 'B') { boxStart = new Vector(j, i); } if (set[i][j] == 'T') { target = new Vector(j, i); } } } } public static int playerGoTo(Vector from, Vector to, Vector nowBox) { PriorityQueue<Vector> states = new PriorityQueue<Vector>(16); Set<Vector> mark = new HashSet<>(16); states.add(from); while (!states.isEmpty()) { Vector now = states.poll(); if (mark.contains(now)) { continue; } mark.add(now); for (Vector op : ops) { Vector nowPos = now.add(op); if (nowPos.equals(nowBox)) { continue; } if (inRange(nowPos)) { if (nowPos.equals(to)) { return 1; } nowPos.setPriority(nowPos.distancePow2(to)); states.add(nowPos); } } } return -1; } public static boolean inRange (Vector vector) { return (vector.vec[0] >= 0 && vector.vec[0] < map[0].length && vector.vec[1] >= 0 && vector.vec[1] < map.length) && map[vector.vec[1]][vector.vec[0]] != '#'; } public static boolean inRange (int x , int y) { return (x >= 0 && x < map[0].length && y >= 0 && y < map.length) && map[y][x] != '#'; } public static boolean isTarget (Vector vector) { return vector.equals(target); } public static int walk () { PriorityQueue<Vector> states = new PriorityQueue<Vector>(16); Vector startState = boxStart.combine(myStart); Set<Vector> mark = new HashSet<>(); startState.setPriority(boxStart.distancePow2(target)); states.add(startState); while (!states.isEmpty()) { // 狀態彈出 Vector now = states.poll(); if (mark.contains(now)) { continue; } mark.add(now); Vector nowBox = now.slice(0, 1); if (isTarget(nowBox)) { return now.step; } Vector nowPlayer = now.slice(2, 3); for (Vector op : ops) { Vector nextBox = nowBox.add(op); if (!inRange(nextBox)){ continue; } Vector playerNeeding = nowBox.sub(op); if (!inRange(playerNeeding)){ continue; } Vector nextState = nextBox.combine(nowBox); if (playerGoTo(nowPlayer, playerNeeding, nowBox) == -1) { continue; } nextState.step = now.step + 1; nextState.setPriority(nextState.step + nextBox.distancePow2(target)); states.add(nextState); } } return -1; } }

Tarjan + A*:

import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.Stack;

/**
 * @description:
 * @author: HP
 * @date: 2020-08-09 15:58
 */
public class TarjanAstart {
    static Vector boxStart;
    static Vector myStart;
    static Vector target;
    static TarJan jan;

    static char[][] map;

    static Vector[] ops = new Vector[]{new Vector(-1, 0), new Vector(1, 0), new Vector(0, -1), new Vector(0, 1)};

    final static class TarJan{
        int[][] tarMap;
        int[][] timeMap;
        Stack<Integer> stack = new Stack<>();
        int count = 0;

        public TarJan(char[][] map) {
            tarMap = new int[map.length][map[0].length];
            timeMap = new int[map.length][map[0].length];
            tarjan(map);
        }

        public int getPoint (int x, int y) {
            return (x << 16) | y;
        }

        public void tarjan(char[][] map) {
            tarjan(new boolean[map.length][map[0].length], map, boxStart.vec[0], boxStart.vec[1], boxStart.vec[0], boxStart.vec[1]);

        }

        private int tarjan(boolean[][] mark, char[][] map, int x, int y, int px, int py){
            count ++;
            tarMap[y][x] = count;
            timeMap[y][x] = count;
            int min = count;
            mark[y][x] = true;
            int mine = getPoint(x, y);
            stack.push(mine);

            for (Vector p : ops) {
                int nextX = x + p.vec[0];
                int nextY = y + p.vec[1];

                if (!inRange(nextX, nextY)) {
                    continue;
                }

                if (mark[nextY][nextX]) {
                    //或者
                    if (nextY != py || nextX != px) {
                        min = Math.min(min, tarMap[nextY][nextX]);
                    }
                } else {
                    min = Math.min(min, tarjan(mark, map, nextX, nextY, x, y));
                }
            }
            //mark[y][x] = false;
            tarMap[y][x] = Math.min( tarMap[y][x] , min);
            if (tarMap[y][x] == timeMap[y][x]) {
                int point = stack.pop();

                // 把點都取出來
                while (point != mine) {
                    int nowX = point >>> 16;
                    int nowY = point & ((1 << 16) - 1) ;
                    tarMap[nowY][nowX] = tarMap[y][x];
                    point = stack.pop();
                }
            }
            return min;
        }

        public boolean connect (int x, int y, int x1, int y1) {
            return tarMap[y][x] == tarMap[y1][x1];
        }
    }

    final static class Vector implements Comparable {
        public int[] vec;
        double priority;
        int hashCode;
        int step;


        public void setPriority(double priority) {
            this.priority = priority;
        }

        public Vector(int... vec) {
            this.vec = vec;
            for (int value : vec) {
                this.hashCode <<= 8;
                this.hashCode |= value;
            }
        }

        public Vector combine(Vector b) {
            int[] cA = new int[vec.length + b.vec.length];
            System.arraycopy(vec, 0, cA, 0, vec.length);
            System.arraycopy(b.vec, 0, cA, vec.length, b.vec.length);
            return new Vector(cA);
        }

        public Vector slice(int from, int to) {
            int[] vec1 = new int[to - from + 1];
            if (to + 1 - from >= 0){
                System.arraycopy(vec, from, vec1, 0, to + 1 - from);
            }
            return new Vector(vec1);
        }

        public Vector add (Vector vector) {
            int[] vec1 = new int[vector.vec.length];
            for (int i = 0; i < vec.length; i ++) {
                vec1[i] = vec[i] + vector.vec[i];
            }
            return new Vector(vec1);
        }

        public Vector sub (Vector vector) {
            int[] vec1 = new int[vector.vec.length];
            for (int i = 0; i < vec.length; i ++) {
                vec1[i] = vec[i] - vector.vec[i];
            }
            return new Vector(vec1);
        }

        public double distancePow2(Vector vector) {
            double res = 0;
            for (int i = 0; i < vec.length; i ++) {
                res += Math.abs(vector.vec[i] - vec[i]);
            }
            return  res;
        }

        @Override
        public int hashCode() {
            return hashCode;
        }

        @Override
        public int compareTo(Object o) {
            if (!(o instanceof  Vector)) {
                throw new RuntimeException("Not a vector");
            }
            Vector vector = (Vector) o;
            return (int) (this.priority - vector.priority);
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof  Vector)) {
                throw new RuntimeException("Not a vector");
            }
            Vector vector = (Vector) o;
            return vector.hashCode == this.hashCode;
        }
    }

    public static void doMain(char[][] set){
        map = set;

        for (int i = 0 ; i < set.length; i ++) {
            for (int j = 0; j < set[0].length; j ++) {
                if (set[i][j] == 'S') {
                    myStart = new Vector(j, i);
                }
                if (set[i][j] == 'B') {
                    boxStart = new Vector(j, i);
                }
                if (set[i][j] == 'T') {
                    target = new Vector(j, i);
                }
            }
        }

        jan = new TarJan(map);
    }



    public static int playerGoTo(Vector from, Vector to, Vector nowBox) {
        PriorityQueue<Vector> states = new PriorityQueue<Vector>(16);
        Set<Vector> mark = new HashSet<>(16);
        states.add(from);

        while (!states.isEmpty()) {
            Vector now = states.poll();

            if (mark.contains(now)) {
                continue;
            }
            mark.add(now);
            for (Vector op : ops) {
                Vector nowPos = now.add(op);
                if (nowPos.equals(nowBox)) {
                    continue;
                }
                if (inRange(nowPos)) {
                    if (nowPos.equals(to)) {
                        return 1;
                    }
                    nowPos.setPriority(nowPos.distancePow2(to));
                    states.add(nowPos);
                }
            }
        }
        return -1;
    }

    public static boolean inRange (Vector vector) {
        return (vector.vec[0] >= 0 && vector.vec[0] < map[0].length
                && vector.vec[1] >= 0 && vector.vec[1] < map.length)
                && map[vector.vec[1]][vector.vec[0]] != '#';
    }

    public static boolean inRange (int x , int y) {
        return (x >= 0 && x < map[0].length
                && y >= 0 && y < map.length)
                && map[y][x] != '#';
    }

    public static boolean isTarget (Vector vector) {
        return vector.equals(target);
    }

    public static int walk () {
        PriorityQueue<Vector> states = new PriorityQueue<Vector>(16);
        Vector startState = boxStart.combine(myStart);
        Set<Vector> mark = new HashSet<>();
        startState.setPriority(boxStart.distancePow2(target));
        states.add(startState);
        startState.step = 0;
        while (!states.isEmpty()) {
            // 狀態彈出
            Vector now = states.poll();
            if (mark.contains(now)) {
                continue;
            }
            mark.add(now);
            Vector nowBox = now.slice(0, 1);

            if (isTarget(nowBox)) {
                return now.step;
            }
            Vector nowPlayer = now.slice(2, 3);

            for (Vector op : ops) {
                Vector nextBox = nowBox.add(op);
                if (!inRange(nextBox)){
                    continue;
                }
                Vector playerNeeding = nowBox.sub(op);
                if (!inRange(playerNeeding)){
                    continue;
                }

                Vector nextState = nextBox.combine(nowBox);

                 if (!jan.connect(nowPlayer.vec[0], nowPlayer.vec[1], playerNeeding.vec[0], playerNeeding.vec[1])){              
                    continue;
                    
                 }

                nextState.step = now.step + 1;
                nextState.setPriority(nextState.step + nextBox.distancePow2(target));
                states.add(nextState);
            }

        }
        return -1;
    }
}
相關文章
相關標籤/搜索