有趣的算法『打開轉盤鎖』

題目描述

你有一個帶有四個圓形撥輪的轉盤鎖。每一個撥輪都有 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

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

來源:力扣(LeetCode)連接ip

思路

這道題頗有意思,有個相似於咱們皮箱的那種密碼鎖,能夠經過上下轉動轉出任意的密碼,不過這裏有個限制,就是不能經過一些死亡數字,算是給咱們增長了一些難度,不然的話就只看密碼的數字是大仍是小了,例如是 3 的話就從0->1->2->3,是 8 的話就0->9->8

咱們能夠把0 0 0 0看作一個原點,而後這 1 個點能夠變化出 8 種不一樣的結果,以下圖所示:

圖1

而後這 8 個點能夠繼續變化:

圖2

請注意觀察上圖,其中有 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;
};

通過測試結果以下:

  • 43/43 cases passed (652 ms)
  • Your runtime beats 33.05 % of javascript submissions
  • Your memory usage beats 72.41 % of javascript submissions (48.8 MB)

結果正確,但好像運行時間有點長,哪裏能夠優化呢?

咱們目前的思路是由一個原點慢慢擴散到終點,也就是目標密碼。就像是向水面扔了一顆石子,激起了一圈的漣漪,而後這圈漣漪最終碰到了咱們的目標,那麼若是我同時在目標處扔一個石子,讓兩個漣漪互相靠近,這樣是否是會快不少呢?直覺告訴我確定會快不少,並且,漣漪不須要擴散得很大就能夠發現目標,咱們來試一下

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;
};

代碼稍加改動的結果:

  • 43/43 cases passed (208 ms)
  • Your runtime beats 80 % of javascript submissions
  • Your memory usage beats 100 % of javascript submissions (44.2 MB)

借用斯溫(DOTA 英雄)的一句名言:『這下牛 b 了』,運行時間和所佔內存都上了一個臺階

最近在看算法方面的內容,碰到有趣的就分享一下,會持續分享

相關文章
相關標籤/搜索