不一樣的猜數字游戲 — leetcode 375. Guess Number Higher or Lower II


好久沒切 leetcode 的題了,靜下心來切了道,這道題比較有意思,和大家分享下。

我把它叫做 "不一樣的猜數字游戲",我們先來看看傳統的猜數字游戲,Guess Number Higher or Lower。題意非常的簡單,給定一個數字 n,系統會隨機從 1 到 n 中抽取一個數字,你需要寫一個函數 guessNumber,它的作用是返回系統選擇的數字,同時你還有一個外部的 API 可以調用,是為 guess 函數,它會將你猜的數字和系統選擇的數字比較,是大了還是小了。

非常的簡單,稍微有點常識的童鞋應該都能想到二分查找的方案(插句題外話,這游戲讓我想到了兒時的幸運52)。關於二分查找,可以參考下我以前寫的一篇文章 http://www.cnblogs.com/zichi/p/5118032.html,幾乎囊獲了所有二分查找的情況。這道題代碼比較簡單,可以參考 guess-number-higher-or-lower.cpp,比較蛋疼的是不支持 JavaScript。

核心代碼:

int guessNumber(int n) {
  int start = 1, end = n;
  int ans;

  while (start <= end) {
    int mid = start + (end - start) / 2;
    int val = guess(mid);

    if (val == -1)
      end = mid - 1;
    else if (val == 1)
      start = mid + 1;
    else {
      ans = mid;
      break;
    }
  }

  return ans;
}

還有一點需要注意下,取 mid 值時不能用 (start + end) / 2,不然會溢出,TLE 掉!

接着進入正題,來看 Guess Number Higher or Lower II 這道題,跟前者比,有何區別呢?同樣是給定一個數字 n,系統會隨機從 1 到 n 中選擇一個整數,你要做的還是將這個數猜出來。你每猜一個數字,需要花費一定的 money,比如你猜 m,那么你就要花費 m,求解你要將這個數字猜出來,至少需要的 money

舉個栗子,比如 n 為 5,系統選擇的數字是 1。如果我先猜 3,系統提示你猜大了,然后再猜 2,系統提示你猜大了,那么你就可以確定是 1 了,花費 3+2=5。但是很明顯第二次猜測應該猜 1,這樣花費就少了。再比如我選的是 4,第一次還是猜 3,系統提示你猜小了,第二次猜 4,中了,總共花費 3+4=7,如果 n 為 5,至少需要 7?非也,正確的解法是先猜 4,如果數字在 1-3 之間,那么再猜 2,至少需要的應該是 6!

這是一道很典型的動態規划題,你根本不可能去盲目地猜,然后使勁地暴力遞歸去解!這樣的復雜度是指數級的。是否能夠遞推求解?比如已經知道 n 為 1-5 的情況,當 n 為 6 時,第一次猜,我們可以有 6 種猜法,分別選擇 1,2,3,4,5 和 6,我們以猜 3 為例,比如說第一把猜了 3,那么如果猜的大了,那么我們接下去要求的是從 [1, 2] 中猜到正確數字所需要花費的最少 money,記為 x,如果猜的小了,那么我們接下去要求的是從 [4, 6] 中猜到正確數字所需要花費的最少 money,記為 y,如果剛好猜中,則結束。很顯然,如果第一把猜 3,那么猜中數字至少需要花費的 money 為 3 + max(x, y, 0),"至少需要的花費",就要我們 "做最壞的打算,盡最大的努力",即取最大值。這是第一把取 3 的情況,我們還需要考慮其他 5 種情況,然后六種情況再取個最小值,就是 n=6 至少需要的 money!(想想,是不是這樣?)

最后來編碼,我們需要一個二維數組來表示最值。首先我們定義一個二維數組 ans[][],ans[i][j] 表示 i-j 中任取一個數字,猜中這個數字需要至少花費的 money。

定義 ans 數組,並且初始化:

// ans[i][j] 表示從 [i, j] 中任取一個數字
// 猜中這個數字至少需要花費的 money
var ans = [];
for (var i = 0; i <= n; i++)
  ans[i] = [];

接着我們定義一個函數 DP,DP(ans, x, y) 表示 [x, y] 中任取一個數字,猜中這個數字需要花費的最少 money,而 ans 是為數組的引用。很顯然,我們要求的就是 DP(ans, 1, n) 的返回值,直接看代碼。

function DP(ans, from, to) {
  // 如果 from >= to
  if (from >= to)
    return 0;

  // 如果 ans[from][to] 已經求得
  // 直接 return
  if (ans[from][to])
    return ans[from][to];

  // 先賦值 Infinity,便於之后的比較
  ans[from][to] = Infinity;

  // 現在要從 [from, to] 中猜數字
  // 假設先猜 i,i 可以是 [from, to] 中的任何數字,遍歷之
  for (var i = from; i <= to; i++) {
    // left 為從 [from, i - 1] 猜對數字至少需要花費的 money
    var left = DP(ans, from, i - 1);
    // right 為從 [i + 1, to] 猜對數字至少需要花費的 money
    var right = DP(ans, i + 1, to);

    // tmp 為先猜 i,從 [from, to] 猜對數字至少需要花費的 money
    var tmp = i + Math.max(left, right);

    // 跟別的方案比較(即跟不是先猜 i 的方法比較)
    // 取最小值
    ans[from][to] = Math.min(ans[from][to], tmp);
  }

  return ans[from][to];
}

注釋寫的很清晰了,如果再細分的話,個人覺得這可以說是一道 "記憶化DP",不曉得有沒有這個詞?好像只聽說過 "記憶化搜索"?DP 本來就是記憶化的過程吧?好了不鑽牛角尖了,完整代碼可以從我們的 Repo https://github.com/hanzichi/leetcode 獲取。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM