[752].打開轉盤鎖

打開轉盤鎖

 


題目

你有一個帶有四個圓形撥輪的轉盤鎖。每個撥輪都有10個數字:0, 1, 2, 3, 4, 5, 6, 7, 8, 9。每個撥輪可以自由旋轉:例如把 9 變爲 00 變爲 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" 時這個鎖就會被鎖定。

示例 2:

輸入: deadends = ["8888"], target = "0009"
輸出:1
解釋:
把最後一位反向旋轉一次即可 "0000" -> "0009"

示例 3:

輸入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
輸出:-1
解釋:
無法旋轉到目標數字且不被鎖定。

示例 4:

輸入: deadends = ["0000"], target = "8888"
輸出:-1

 


函數原型

C的函數原型:

int openLock(char ** deadends, int deadendsSize, char * target){}

 


邊界判斷

int openLock(char ** deadends, int deadendsSize, char * target){}

 


算法設計:BFS求無權圖的最短路徑

圖論:可以描繪從某一步開始,一步一步,最終達到終止狀態。

如果把這樣一個問題轉爲圖論問題,用圖論解決,核心在於[狀態表達]。

怎麼把原問題中每一步問題的樣子表達爲圖論中的一個頂點。

每個撥輪都有10個數字:0, 1, 2, 3, 4, 5, 6, 7, 8, 9,但每次旋轉都只能旋轉一個撥輪的一位數字。

如果把這 10 個數字看成一個頂點,每次只能撥一位(要麼+1,要麼-1),撥一位後數字就改變了,例如把 9 變爲 00 變爲 9 ,說明 頂點0、頂點9 之間是有邊,而且是雙向的,也就是一個無向圖。

題目需要給出最小的旋轉次數,這個最小的旋轉次數,就是最短路徑。

如果將 0000999910000 狀態看成圖上的 10000 個節點,兩個節點之間存在一條邊,當且僅當這兩個節點對應的狀態只有 1 位不同,且不同的那位相差 1(包括 09 也相差 1 的情況),並且這兩個節點均不在數組 deadends 中。

最終的答案,即爲 0000target 的最短路徑。

用廣度優先搜索來找到最短路徑,從 0000 開始搜索。對於每一個狀態,它可以擴展到最多 8 個狀態,即將它的第 i = 0, 1, 2, 3 位增加 1 或減少 1,將這些狀態中沒有搜索過並且不在 deadends 中的狀態全部加入到隊列中,並繼續進行搜索。

注意 0000 本身有可能也在 deadends 中。

BFS模版:

void BFS()
{
    // 定義隊列
    // 建立備忘錄,用於記錄已經訪問的位置

    // 判斷邊界條件,是否能直接返回結果的

    // 將起始位置加入到隊列中,同時更新備忘錄

    while ( /* 隊列不爲空 */) {
        // 獲取當前隊列中的元素個數
        for ( /* 元素個數 */ ) {
            // 取出一個位置節點
            // 判斷是否到達終點位置
            // 獲取它對應的下一個所有的節點
            // 條件判斷,過濾掉不符合條件的位置
            // 新位置重新加入隊列
        }
    }

}

思路:

#define MAX_SIZE 10000
int openLock(char ** deadends, int deadendsSize, char * target){
     /* 查找當前數字是否 是[死亡數字],最快的查找算法是[哈希] */
     int *hash = (int *)malloc(sizeof(int)*MAX_SIZE);                    
     // 1.創建一個哈希數組
     memset(hash, -1, sizeof(hash));        
     // 2.哈希數組初始化爲 -1,因爲鎖的範圍是 [0,9]
     int num = 0;
     for(int i=0; i<deadendsSize; i++){
            num = 0;
     	for(int j=0; j<4; j++){
	    	num = (deadends[i][j]-'0') + num*10;    
	    	// 將數字字符轉換成整型數字,如 "1234" -> 1234
        }
	    hash[num] = i;                      
	    // 3.讓數組中的每個元素與其索引相互對應: hash_array[值] = 下標;
	 }
     
     /* 邊界判斷 */
     int target_num = 0;
     sscanf(target, "%d", &target_num);                // 將 target 轉爲數字
     if( hash[0] != -1 || hash[target_num] != -1 )     // 如果 起點0000、終點target 有一個是死亡數字,鎖就打不開
     	return -1;
     if( target_num == 0 )                             // 終點target = 起點0000
     	return 0;
     
     /* 模擬隊列 */
     int *pQueue = (int*)malloc(sizeof(int) * MAX_SIZE);
     memset(pQueue, -1, sizeof(pQueue)); 
     int front = -1;           				    // 隊尾
     int rear  = -1;                        	// 隊頭
     
     int *visited = (int*)malloc(sizeof(int) * MAX_SIZE);
     memset(visited, -1, sizeof(visited));         	
     // 建立備忘錄,默認都沒訪問過,這裏不用 bool 類型,結合哈希思想,哈希值就是從 `0000` 到 `target` 的最短路徑
     
     pQueue[++rear] = 0;                    	// 將 起始位置0000 入隊
     visited[0] = 0;                     	    // 更新備忘錄,最短路徑默認爲 0

     /* BFS */
     while( front != rear ){                    // 隊列不爲空
	     int cur = pQueue[++front];             // 出隊
	     /* 從 cur 這個密碼鎖開始變化,4位數字,每位都可以+1、-1,一共 8 種可能 */
	     int cur_all[4] = {cur/1000, (cur/100)%10, (cur/10)%10, cur%10};             
	     										// 把每一位分解出來,方便+1、-1		
	
	     int n = 0;
	     int m = 0;
	     int next[MAX_SIZE] = {-1};
	     for( int i=0; i<4; i++ ){
	        int v = cur_all[i];    				// 保存原數據 cur[i]
	     	cur_all[i] = (cur_all[i]+1)%10;     // 每一位+1產生新數字,%10讓 9+1 = 0
            n = cur_all[i] + n * 10;		    // 再把4個單獨的數組合起來
            next[i] = n;						// 保存到 next 數組裏
            cur_all[i] = v;                     // 數據換回來了,和進入循環時一樣,就可以進行-1
            
            cur_all[i] = (cur_all[i]+9)%10;     // 每一位-1產生新數字,但如果把 -1 寫成 +9可以避免 (0 - 1)%10的隨機值
            m = cur_all[i] + m * 10;		    // 再把4個單獨的數組合起來
            next[i+4] = m;						// 接着從第 4 位,保存到數組裏
            cur_all[i] = v;                     // 數據換回來了,和進入循環時一樣
	     }
	     
	     for( int i=0; i<8; i++){               // 一共 8 種狀態
         	if( hash[next[i]] == -1 && visited[next[i]] == -1){   
         	// 如果這種可能數字不是死亡數字 且 沒有被訪問過
         		pQueue[++rear] = next[i];
         		// 入隊
         		visited[next[i]] = visited[cur] + 1;
         		// 達到這種可能數字的步數,就是達到 cur 數字的步數 + 1
         		if( next[i] == target_num ){    // 抵達了 target 數字
         			free(hash); hash = NULL;
     				free(pQueue); pQueue = NULL;
     				free(visited); visited = NULL;
         			return visited[next[i]];
         		}
         	}
		 }
	 }
     
     free(hash); hash = NULL;
     free(pQueue); pQueue = NULL;
     free(visited); visited = NULL;
     return -1;
}

思路就是這樣,但上傳到 Leetcode 又不能 AC

因爲我們把 "1234" 轉爲了 1234,這裏有一個漏洞。

  • "0123" 轉爲 123,前面的 0 就沒了。

這樣就有倆個思路:

  • 一:把消失的 0 補上;
  • 二:不用字符轉整數,直接用字符,不過計算的時暫時性轉下。

我打算採用[二],但 C 的數組索引只能是 字符或者整數,不能是字符串。

C 版本的代碼,容我再二刷 Leetcode 的時候再寫吧,付上一份參考 code

int strtoint(char *s){                          // 字符轉整數
    int res, i;
    for(i = 0, res = 0; i < 4; i++){
        res *= 10;
        res += s[i]-'0';
    }
    return res;
}

int openLock(char ** deadends, int deadendsSize, char * target){
    int array[10000];
    int qu[10000];
    int i, j, k, tmp, this;
    int aim = strtoint(target);                 // 目標數字
    for(i = 0; i <10000; i++)                   // 一共 1萬 個結點
        array[i] = -2;                          // 初始化爲 -2
    for(i = 0; i < deadendsSize; i++){
        tmp = strtoint(deadends[i]);            // 取每行字符
        if(tmp==0)
            return -1;
        array[tmp] = -1;
    }
    if(0 == aim)
        return 0;
    int tail = 1;                               // 隊尾
    int head = 0;                               // 隊頭
    int layer = 0;                              // 層數
    int size;                                   // 隊列的大小
    int d[4];
    int out[4];
    qu[head] = 0;                               // 備忘錄visited
    
    while(tail != head){                        // 隊列不爲空
        ++layer;                                // 層數 +1
        size = tail - head;                     // 隊列size = 隊尾 - 隊頭
        for(i = 0; i < size; i++){
            tmp = qu[(head++)%10000];           // 出隊
            d[0] = tmp%10;                      // 1234 的 4
            d[1] = (tmp/10)%10;                 // 1234 的 3
            d[2] = (tmp/100)%10;                // 1234 的 2
            d[3] = tmp/1000;                    // 1234 的 1
            
            for(j = 0; j < 4; j++){
                for(k = -1; k <= 1; k+=2){
                    out[0]=d[0];out[1]=d[1];out[2]=d[2];out[3]=d[3];
                    out[j] = (10 + d[j] + k) % 10;
                    this = out[0]+out[1]*10+out[2]*100+out[3]*1000;  // 組合
                    if(this == aim)            
                        return layer;
                    if(array[this] == -2){
                        qu[(tail++)%10000] = this;
                        array[this] = layer;
                    }
                }
            }
        }
    }
    return -1;
}

會發現和我們上面寫的代碼,重合度很高。

BFS求無權圖的最短路徑的複雜度:

  • 時間複雜度:Θ(n2)\Theta(n^{2})
  • 空間複雜度:Θ(n)\Theta(n)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章