你有一個帶有四個圓形撥輪的轉盤鎖。每一個撥輪都有 10 個數字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每一個撥輪能夠自由旋轉:例如把 '9' 變爲 '0','0' 變爲 '9' 。每次旋轉都只能旋轉一個撥輪的一位數字。javascript
鎖的初始數字爲 '0000' ,一個表明四個撥輪的數字的字符串。java
列表 deadends 包含了一組死亡數字,一旦撥輪的數字和列表裏的任何一個元素相同,這個鎖將會被永久鎖定,沒法再被旋轉。node
字符串 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:spa
輸入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888" 輸出:-1 解釋: 沒法旋轉到目標數字且不被鎖定。
示例 4:code
輸入: deadends = ["0000"], target = "8888" 輸出:-1
提示:blog
來源:力扣(LeetCode)連接ip
這道題頗有意思,有個相似於咱們皮箱的那種密碼鎖,能夠經過上下轉動轉出任意的密碼,不過這裏有個限制,就是不能經過一些死亡數字,算是給咱們增長了一些難度,不然的話就只看密碼的數字是大仍是小了,例如是 3 的話就從0->1->2->3
,是 8 的話就0->9->8
。
咱們能夠把0 0 0 0
看作一個原點,而後這 1 個點能夠變化出 8 種不一樣的結果,以下圖所示:
而後這 8 個點能夠繼續變化:
請注意觀察上圖,其中有 8 種組合又回到了 0 0 0 0
,還有藍色部分都是具備重合項的。
那麼咱們的思路來了,能夠利用這種變化,從0 0 0 0
變化爲 8 個點,再繼續由先和 8 個點繼續變化。。。直到咱們找到了目標密碼,每變化一次須要旋轉一次。
var openLock = function (deadends, target) { // 存儲全部的原點,最初是 0000 let nodes = new Set(); nodes.add('0000'); // 匹配過的點,好比 0000 這種,對於匹配過的點不會加入到原點集合裏面去 const history = new Set(); // 初始化旋轉次數 let step = 0; // 向上旋轉,例如從0->1 const plus = function (nums, i) { let array = nums.split(''); if (array[i] === '9') { array[i] = '0'; } else { array[i] = Number(array[i]) + 1; } return array.join(''); }; // 向下旋轉,例如從0->9 const miuns = function (nums, i) { let array = nums.split(''); if (array[i] === '0') { array[i] = '9'; } else { array[i] = Number(array[i]) - 1; } return array.join(''); }; // 原點沒有目標密碼 while (!nodes.has(target)) { // 新增的原點集合 const newNodes = new Set(); // 當前原點集合 for (const nums of nodes) { // 遇到不通的路就跳過 if (deadends.includes(nums)) { continue; } // 遍歷數字,分別作向上和向下旋轉 for (let i = 0; i < nums.length; i++) { // 旋轉後的結果,把向上和向下旋轉後的原點都存儲起來 let result = plus(nums, i); // 排除已選擇的原點 if (!history.has(result) && !newNodes.has(result)) { newNodes.add(result); } result = miuns(nums, i); if (!history.has(result) && !newNodes.has(result)) { newNodes.add(result); } } // 已檢查過的原點 history.add(nums); } step++; // 新生成的原點集合,下一輪將對這些原點進行旋轉 nodes = newNodes; // 這裏很關鍵,最後可能收斂沒了 if (nodes.size === 0) { return -1; } } return step; };
通過測試結果以下:
結果正確,但好像運行時間有點長,哪裏能夠優化呢?
咱們目前的思路是由一個原點慢慢擴散到終點,也就是目標密碼。就像是向水面扔了一顆石子,激起了一圈的漣漪,而後這圈漣漪最終碰到了咱們的目標,那麼若是我同時在目標處扔一個石子,讓兩個漣漪互相靠近,這樣是否是會快不少呢?直覺告訴我確定會快不少,並且,漣漪不須要擴散得很大就能夠發現目標,咱們來試一下
var openLock = function (deadends, target) { // 存儲全部的原點,最初是 0000 let nodes = new Set(); nodes.add('0000'); // 目標原點 let targetNodes = new Set(); targetNodes.add(target); // 匹配過的點,好比 0000 這種,對於匹配過的點不會加入到原點集合裏面去 const history = new Set(); // 初始化旋轉次數 let step = 0; // 向上旋轉,例如從0->1 const plus = function (nums, i) { let array = nums.split(''); if (array[i] === '9') { array[i] = '0'; } else { array[i] = Number(array[i]) + 1; } return array.join(''); }; // 向下旋轉,例如從0->9 const miuns = function (nums, i) { let array = nums.split(''); if (array[i] === '0') { array[i] = '9'; } else { array[i] = Number(array[i]) - 1; } return array.join(''); }; // 原點沒有目標密碼 while (nodes.size > 0 && targetNodes.size > 0) { // 新增的原點集合 const newNodes = new Set(); // 當前原點集合 for (const nums of nodes) { // 遇到不通的路就跳過 if (deadends.includes(nums)) { continue; } // 相遇 if (targetNodes.has(nums)) { return step; } // 遍歷數字,分別作向上和向下旋轉 for (let i = 0; i < nums.length; i++) { // 旋轉後的結果,把向上和向下旋轉後的原點都存儲起來 let result = plus(nums, i); // 排除已選擇的原點 if (!history.has(result) && !newNodes.has(result)) { newNodes.add(result); } result = miuns(nums, i); if (!history.has(result) && !newNodes.has(result)) { newNodes.add(result); } } // 已檢查過的原點 history.add(nums); } step++; // 交換集合,下一輪對targetNodes進行檢查 nodes = targetNodes; // 新生成的原點集合,下下一輪將對這些原點進行旋轉 targetNodes = newNodes; } // 沒有結果 return -1; };
代碼稍加改動的結果:
借用斯溫(DOTA 英雄)的一句名言:『這下牛 b 了』,運行時間和所佔內存都上了一個臺階
最近在看算法方面的內容,碰到有趣的就分享一下,會持續分享