1. 程式人生 > 實用技巧 >LeetCode Notes_#752 開啟轉盤鎖

LeetCode Notes_#752 開啟轉盤鎖

LeetCode Notes_#752 開啟轉盤鎖

LeetCode

Contents

題目

你有一個帶有四個圓形撥輪的轉盤鎖。每個撥輪都有10個數字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每個撥輪可以自由旋轉:例如把 '9' 變為 '0','0' 變為 '9' 。每次旋轉都只能旋轉一個撥輪的一位數字。
鎖的初始數字為 '0000' ,一個代表四個撥輪的數字的字串。
列表 deadends 包含了一組死亡數字,一旦撥輪的數字和列表裡的任何一個元素相同,這個鎖將會被永久鎖定,無法再被旋轉。
字串 target 代表可以解鎖的數字,你需要給出最小的旋轉次數,如果無論如何不能解鎖,返回 -1。
示例 1:

輸入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
輸出:6
解釋:
可能的移動序列為 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 這樣的序列是不能解鎖的,
因為當撥動到 "0102" 時這個鎖就會被鎖定。

提示:

  1. 死亡列表 deadends 的長度範圍為 [1, 500]。
  2. 目標數字 target 不會在 deadends 之中。
  3. 每個 deadends 和 target 中的字串的數字會在 10,000 個可能的情況 '0000' 到 '9999' 中產生。

思路分析

題目描述的是一個很具體的問題,但是乍一看是沒有什麼思路的,需要將問題轉化,將問題抽象成一個狀態轉移的問題。
0000~9999看作不同的狀態,這些狀態之間可以互相轉換。
轉換的條件是,兩個數字僅有一位相差1(0和9也算在內)。
按照上述描述,可以畫出一個狀態轉移圖,圖中有0000~9999這些節點,滿足狀態轉移條件的節點之間有邊連線。最小旋轉次數就等於從起始狀態0000target的最短路徑。
求最短路徑的一個方法就是廣度優先遍歷BFS,廣度優先遍歷也就是層序遍歷,從0000target的過程中,廣度優先遍歷的層數就是最短路徑。具體寫法類似於樹的BFS。

需要注意的點:

  1. 狀態轉移的過程不可以出現deadends
    裡的元素,遇到了就直接跳過
  2. BFS過程中,可能出現重複訪問的節點,所以還需要判斷重複,如果是重複的節點就不入隊
  3. 需要使用null作為層與層之間的分隔符,否則不好計數

解答

class Solution {
    public int openLock(String[] deadends, String target) {
        //兩個HashSet,用於判斷重複
        HashSet<String> dead = new HashSet<>();
        for(String str:deadends){
            dead.add(str);
        }
        HashSet<String> seen = new HashSet<>();
        //建立BFS用到的佇列,先把遍歷的起點‘0000’入隊
        Queue<String> queue = new LinkedList<>();
        queue.offer("0000");
        //第一層只有只有一個節點,所以加入分隔符
        queue.offer(null);
        //計數器,記錄路徑的長度
        int depth = 0;

        while(!queue.isEmpty()){
            String cur = queue.poll();
            //null是層與層之間的分隔符
            if(cur == null){
                //遇到null說明接下來的元素就是新的一層了,depth增加1
                depth++;
                //判斷下一層是否還有元素,如果有,就要新增null到隊尾(因為這時下一層已經全部入隊)
                //這時新增到隊尾,剛好就可以分隔開下一層與下下層
                if(queue.peek() != null)
                    queue.offer(null);
            }else if(cur.equals(target)){
                return depth;
            //如果cur在dead當中,就不能從這個節點走,在迴圈中不做任何操作,直接進入下一輪迴圈
            //否則,就把cur的所有鄰居節點入隊,等待遍歷
            }else if(!dead.contains(cur)){
                //外層迴圈:4位數
                for(int i = 0;i <= 3;i++){
                    //內層迴圈:加1或者減1(加-1)
                    for(int d = -1;d <= 1;d += 2){
                        //修改的那一位數字,記為newDigit
                        //求餘是為了符合0,9之間的轉換
                        int newDigit = ((cur.charAt(i) - '0') + d + 10) % 10;
                        //將newDigit拼接到cur字串的合適位置
                        String StringNeibor = cur.substring(0,i) + newDigit + cur.substring(i+1);
                        //對於每個鄰居節點,需要判斷是否是已經訪問過的,如果已經訪問過,就不再入隊
                        if(!seen.contains(StringNeibor)){
                            seen.add(StringNeibor);
                            queue.offer(StringNeibor);
                        }
                    }
                }
            }
        }
        //最後沒找到,就返回-1
        return -1;
    }
}

複雜度分析

時間複雜度:O(N2 * AN + D)。我們用 A 表示數字的個數,N 表示狀態的位數,D表示陣列 deadends 的大小。在最壞情況下,我們需要搜尋完所有狀態,狀態的總數為 O(AN)。對於每個狀態,我們要列舉修改的位置,需要 O(N) 的時間,列舉後得到新的狀態同樣需要 O(N) 的時間。

空間複雜度:O(AN + D)),用來儲存佇列以及 deadends 的集合。