打開轉盤鎖
題目
你有一個帶有四個圓形撥輪的轉盤鎖。每個撥輪都有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" 時這個鎖就會被鎖定。
示例 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
變爲 0
,0
變爲 9
,說明 頂點0
、頂點9
之間是有邊,而且是雙向的,也就是一個無向圖。
題目需要給出最小的旋轉次數,這個最小的旋轉次數,就是最短路徑。
如果將 0000
到 9999
這 10000
狀態看成圖上的 10000
個節點,兩個節點之間存在一條邊,當且僅當這兩個節點對應的狀態只有 1
位不同,且不同的那位相差 1
(包括 0
和 9
也相差 1
的情況),並且這兩個節點均不在數組 deadends
中。
最終的答案,即爲 0000
到 target
的最短路徑。
用廣度優先搜索來找到最短路徑,從 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求無權圖的最短路徑的複雜度:
- 時間複雜度:
- 空間複雜度: