寶寶也能看懂的 leetcode 周賽 - 169 - 4

1307. Verbal Arithmetic Puzzle

Hi 你們好,我是張小豬。歡迎來到『寶寶也能看懂』系列之 leetcode 題解。git

這裏是第 169 期的第 4 題,也是題目列表中的第 1307 題 -- 『Verbal Arithmetic Puzzle』github

題目描述

Given an equation, represented by words on left side and the result on right side.shell

You need to check if the equation is solvable under the following rules:express

  • Each character is decoded as one digit (0 - 9).
  • Every pair of different characters they must map to different digits.
  • Each words[i] and result are decoded as one number without leading zeros.
  • Sum of numbers on left side (words) will equal to the number on right side (result).

Return True if the equation is solvable otherwise return False.數組

Example 1:ide

Input: words = ["SEND","MORE"], result = "MONEY"
Output: true
Explanation: Map 'S'-> 9, 'E'->5, 'N'->6, 'D'->7, 'M'->1, 'O'->0, 'R'->8, 'Y'->'2'
Such that: "SEND" + "MORE" = "MONEY" ,  9567 + 1085 = 10652

Example 2:post

Input: words = ["SIX","SEVEN","SEVEN"], result = "TWENTY"
Output: true
Explanation: Map 'S'-> 6, 'I'->5, 'X'->0, 'E'->8, 'V'->7, 'N'->2, 'T'->1, 'W'->'3', 'Y'->4
Such that: "SIX" + "SEVEN" + "SEVEN" = "TWENTY" ,  650 + 68782 + 68782 = 138214

Example 3:性能

Input: words = ["THIS","IS","TOO"], result = "FUNNY"
Output: true

Example 4:測試

Input: words = ["LEET","CODE"], result = "POINT"
Output: false

Constraints:優化

  • 2 <= words.length <= 5
  • 1 <= words[i].length, result.length <= 7
  • words[i], result contains only upper case English letters.
  • Number of different characters used on the expression is at most 10.

官方難度

HARD

解決思路

題目的內容爲,給定一組字符串做爲因子和一個結果字符串,字符串中都是英文大寫字母。字符串中的每一個字符表明了一個數字,不容許兩個不一樣的字符表明同一個數字,也不容許兩個相同的字符表明不一樣的數字。而且每個字符串開頭的第一個字符不能夠表明 0。最終,指望這一組因子的數字求和須要等於結果,若是能實現的話返回 true,不然返回 false。結合上文中的例子能夠更清晰的理解題目的意思。

乍一看感受題目需求還挺簡單,可是解題思路一臉懵逼。思考了一會,以爲彷佛沒有什麼特殊的數學方法來處理,因而只能依賴計算機強大的計算能力來嘗試每一種可能。最終看是否能找到符合要求的解。

既然決定了要暴力莽一把,那麼一種很是常見的莽法就涌上心頭,即基於深度優先遍從來更快的探查到每個可能解,並結合遞歸來實現回溯。

因而擼起豬蹄子,揉揉豬鼻子,奧利給,淦了!

直接方案

基於上述思路,咱們用一個 Map 來記錄字符和它對應的數字,用一個數組來記錄全部不一樣的字符,用一個 Set 來記錄咱們已經使用過的數字。接下來須要作的,只是遍歷這個數組中的字符,給每一個字符嘗試枚舉全部的當前還未使用的值。最後測試一下等式是否成當即可。

如下代碼比較辣眼睛,可能會有損你的視力,請在堅強的本身的陪同下,一塊兒鄙視我。 T_T

const convertVal = (str, charVal) => {
  let val = 0;
  for (let i = 0; i < str.length; ++i) {
    val = val * 10 + charVal.get(str[i]);
  }
  return val;
};
const isSolvable = (words, result) => {
  const charVal = new Map();
  const chars = [];
  for (const char of result) {
    !charVal.has(char) && chars.push(char) && charVal.set(char, -1);
  }
  for (const word of words) {
    for (const char of word) {
      !charVal.has(char) && chars.push(char) && charVal.set(char, -1);
    }
  }
  if (charVal.size > 10) return false;
  const usedVal = new Set();
  return helper(0);

  function helper(idx) {
    if (idx === charVal.size) return check();
    const char = chars[idx];
    for (let i = 0; i <= 9; ++i) {
      if (usedVal.has(i) || (idx === 0 && i === 0)) continue;
      charVal.set(char, i);
      usedVal.add(i);
      if (helper(idx +1)) return true;
      usedVal.delete(i);
    }
    return false;
  }

  function check() {
    let sum = 0;
    for (const word of words) {
      if (charVal[word[0]] === 0) return false;
      sum += convertVal(word, charVal)
    }
    return sum === convertVal(result, charVal);
  }
};

爲何這麼辣眼睛還要放出來?由於寶寶真實(此處應有掌聲) >.<

這確實是我提交的第一次代碼,Acceped 了,可是時間是 8000+ms,這座城又多了只傷心的豬。 T_T

優化

上述代碼基本嘗試了各類可能,因此結果才那麼慢。那麼咱們的優化思路有兩個方向:

  • 減小基礎嘗試的鏈條長度
  • 提早終止沒必要要的嘗試

前者除了能下降調用棧的深度,減小空間使用以外,更是因爲分支的鋪開速度是接近指數級的,因此能有效的提高性能。後者在前者的基礎上,能夠必定程度上的提升性能。

那麼咱們回看題目描述,知足要求的因子和結果這是一個等式。看到這裏其實咱們能夠想到一件事情,咱們不用針對全部字符枚舉出全部的可能值。由於若是等式成立的話,其中某一項的值是能夠基於其餘項目的值計算出來的。基於此,咱們便能減小基礎嘗試鏈條的長度。另外因爲題目不容許數字出現先導 0,即第一個字符不能對應 0,因此咱們能夠用這個條件提早終止沒必要要的嘗試。

如下代碼把一個因子從基礎嘗試鏈條中去掉了,而且在初始化的時候記錄了全部字符串的第一個字符用於判斷先導 0。

const convertVal = (str, charVal) => {
  let val = 0;
  for (let i = 0; i < str.length; ++i) {
    val = val * 10 + charVal.get(str[i]);
  }
  return val;
};

const isSolvable = (words, result) => {
  const charVal = new Map();
  const usedVal = new Set();
  const lastWord = words[words.length - 1];
  const leadChar = new Set([result[0], lastWord[0]]);
  let chars = result;
  for (let i = 0; i < words.length - 1; ++i) {
    chars += words[i];
    leadChar.add(words[i][0]);
  }
  return helper(0);

  function helper(idx) {
    if (idx === chars.length) return check();
    const char = chars[idx];
    if (charVal.has(char)) return helper(idx + 1);
    for (let i = 0; i <= 9; ++i) {
      if (usedVal.has(i) || (i === 0 && leadChar.has(char))) continue;
      usedVal.add(i);
      charVal.set(char, i);
      if (helper(idx + 1)) return true;
      charVal.delete(char);
      usedVal.delete(i);
    }
    return false;
  }

  function check() {
    let sum = convertVal(result, charVal);
    for (let i = 0; i < words.length - 1; ++i) {
      sum -= convertVal(words[i], charVal);
      if (sum < 0) return false;
    }
    sum = sum.toString().split('');
    if (sum.length !== lastWord.length) return false;
    for (let i = 0; i < sum.length; ++i) {
      if (charVal.has(lastWord[i]) && +sum[i] !== charVal.get(lastWord[i])) return false;
    }
    return true;
  }
};

這段代碼提交後時間來到了 5000ms+,仍舊是十分慢。

換個思路

看到上面的時間我意識到,這個思路是有問題的。我必定是忽略了什麼很是重要的信息。回頭再看看題目中等式這個條件,想一想加法的運算過程,恍然大悟,其實咱們只須要按照加法的運算順序來判斷便可。過程以下:

  1. 咱們遍歷全部因子,檢查佔據着個位的字符。若是它已經被賦值了,則直接使用,若是沒有被賦值,則猜想一個可能值。
  2. 求和後咱們便能計算出結果中的個位的數值。接下來判斷結果中佔據着個位的字符,看看是否符合咱們的要求。而且注意,這裏的求和進位必定不要忘了。
  3. 依次處理十位、百位等全部的位置,直到咱們超過告終果字符串的長度。那麼意味着咱們找到了一個符合要求的解,也就是等式能夠成立。若是過程當中有任何不符合要求的地方,都直接跳出以免沒必要要的遞歸判斷。

有了這個思路後,咱們能夠發現整個嘗試的過程變得理性了不少,再也不像是徹底盲目的嘗試。而且大多數不合理的嘗試均可以在較早的時候被及時終止。因而咱們來作代碼實現。

const isSolvable = (words, result) => {
  const charVal = new Map();
  const usedVal = new Set();
  const leadChar = new Set(result[0]);
  const WORDS_COUNT = words.length;
  const MAX_WORD_LEN = result.length;

  for (let i = 0; i < WORDS_COUNT; ++i) {
    if (words[i].length > MAX_WORD_LEN) return false;
    leadChar.add(words[i][0]);
  }
  return helper(1, 0, 0);

  function helper(digit, wordIdx, carry) {
    if (digit > MAX_WORD_LEN) return true;

    if (wordIdx === WORDS_COUNT) {
      const resultNum = carry % 10;
      const resultChar = result[MAX_WORD_LEN - digit];
      const isUsed = charVal.has(resultChar);
      if (
        (!isUsed && usedVal.has(resultNum))
        || (isUsed && charVal.get(resultChar) !== resultNum)
        || (resultNum === 0 && leadChar.has(resultChar))
      ) return false;
      usedVal.add(resultNum);
      charVal.set(resultChar, resultNum);
      if (helper(digit + 1, 0, (carry - resultNum) / 10)) return true;
      !isUsed && usedVal.delete(resultNum) && charVal.delete(resultChar);
      return false;
    }

    const idx = words[wordIdx].length - digit;
    if (idx < 0) return helper(digit, wordIdx + 1, carry);
    const char = words[wordIdx][idx];
    if (charVal.has(char)) return helper(digit, wordIdx + 1, carry + charVal.get(char));
    for (let i = 0; i < 10; ++i) {
      if (usedVal.has(i) || (i === 0 && leadChar.has(char))) continue;
      usedVal.add(i);
      charVal.set(char, i);
      if (helper(digit, wordIdx + 1, carry + i)) return true;
      usedVal.delete(i);
      charVal.delete(char);
    }

    return false;
  }
};

這段代碼相比以前的代碼是否是沒有那麼辣眼睛了。固然,從時間上來看咱們的思路也獲得了回報。跑出了 112ms 有了數量級的提高,暫時 beats 了 100%。

再優化

已經 beats 100% 了爲何還要再繼續嘗試優化呢?由於上述 3 段代碼中,都使用到了 1 個 Map 和 2 個 Set。而我以爲其實均可以去掉。轉換爲只使用一個定長的 Uint8Array 來記錄咱們須要的數據。那麼如今須要解決的問題就是,咱們如何來記錄這些數據。

看了一下題目的限制條件,每一個字符必定是英文大寫字符,也就是隻會從 'A' 到 'Z'。這裏第一反應就是取字符的 char code 便可。可是如何同時來記錄上面用到的 charValusedValleadChar 呢?

這裏首先咱們看一下,'A' 的 char code 是 65,也就是說若是不使用減去偏移量的方式,在知足了 charVal 的存儲需求後,咱們的定長數組中還存在着前面 65 個空缺。這時候能夠想到,咱們能夠把 usedVal 的需求,也就是已經使用的數值也記錄在裏面,它們將佔據 [0, 9] 這一段範圍,也就是還剩下 [10, 64] 這麼多空缺。這一大段徹底夠 leadChar 來使用了。具體儲存規則以下:

  • 基於 Uint8Array 的定長數組長度爲 91,由於 'Z' 的 char code 是 90。
  • 使用 10 做爲數組的初始值,由於默認的 0 在咱們的取值範圍 [0, 9] 以內。
  • 使用下標範圍 [0, 9] 來標識已經被使用的數值。
  • 使用下標範圍 [65, 90] 來記錄每一個字符對應的數字值。
  • 使用下標範圍 [35, 60] 來標識佔據着先導 0 位置的字符,這個範圍是每一個字符 char code 減去偏移量 30。

固然,上述規則的偏移量能夠自行設定。只須要 3 個範圍不重疊便可。具體代碼以下。

const isSolvable = (words, result) => {
  const WORDS_COUNT = words.length;
  const MAX_WORD_LEN = result.length;
  const OFFSET = 30;
  const INIT_VAL = 10;
  const charVal = new Uint8Array(91).fill(INIT_VAL);

  charVal[result.charCodeAt(0) - OFFSET] = 1;
  for (let i = 0; i < WORDS_COUNT; ++i) {
    if (words[i].length > MAX_WORD_LEN) return false;
    charVal[words[i].charCodeAt(0) - OFFSET] = 1;
  }
  return helper(1, 0, 0);

  function helper(digit, wordIdx, carry) {
    if (digit > MAX_WORD_LEN) return true;

    if (wordIdx === WORDS_COUNT) {
      const resultNum = carry % 10;
      const resultCharCode = result.charCodeAt(MAX_WORD_LEN - digit);
      const isUsed = charVal[resultCharCode] !== INIT_VAL;
      if (
        (!isUsed && charVal[resultNum] === 1)
        || (isUsed && charVal[resultCharCode] !== resultNum)
        || (resultNum === 0 && charVal[resultCharCode - OFFSET] === 1)
      ) return false;
      charVal[resultNum] = 1;
      charVal[resultCharCode] = resultNum;
      if (helper(digit + 1, 0, (carry - resultNum) / 10)) return true;
      !isUsed && (charVal[resultNum] = INIT_VAL) && (charVal[resultCharCode] = INIT_VAL);
      return false;
    }

    const idx = words[wordIdx].length - digit;
    if (idx < 0) return helper(digit, wordIdx + 1, carry);
    const charCode = words[wordIdx].charCodeAt(idx);
    if (charVal[charCode] !== INIT_VAL) return helper(digit, wordIdx + 1, carry + charVal[charCode]);
    for (let i = 0; i < 10; ++i) {
      if (charVal[i] !== INIT_VAL || (i === 0 && charVal[charCode - OFFSET] !== INIT_VAL)) continue;
      charVal[i] = 1;
      charVal[charCode] = i;
      if (helper(digit, wordIdx + 1, carry + i)) return true;
      charVal[i] = INIT_VAL;
      charVal[charCode] = INIT_VAL;
    }

    return false;
  }
};

這段代碼最終跑到了 68ms 的時間,固然也替代了上面的代碼暫時 beats 100% 了。

總結

這道題中,對於優化過程的思路進行了較多的分析。也給出了我從十分辣眼睛的代碼到最後方案的完整過程。其中比較關鍵的一點是,當意識到狀況不對的時候,從新看條件並整理思路,可能比仍舊在舊思路上死磕更好。

相關連接

相關文章
相關標籤/搜索