Leetcode Weekly Contest 95解題報告

caibirdme發表於2018-08-02

Contest 95

本文是同步自我的 github 專案leetforfun,歡迎關注和討論

連結串列的中間節點

原題連結

題目大意

給定一個連結串列,返回連結串列的中間節點。如果節點數是偶數,返回靠後的那一個節點 比如:

  • 3->5->7->6->9 返回 7
  • 3->4->5->6 返回 5

題解

難度:1 顆星

這道題最容易想到的就是:

  1. 遍歷一次連結串列,得到其長度 K
  2. 得到長度 K 後可以算出哪個是中點,再遍歷一次到那個節點即可

那麼,有沒有看起來更好一點的辦法呢?

可以假設有兩個小朋友 A 和 B,都站在連結串列的頭部,他們要一起遍歷這個連結串列。假設 A 一次前進兩個節點,B 一次只能前進一個節點。那當 A 走到連結串列盡頭時,B 剛好就走到連結串列中間。AC 程式碼就是採用這種方法。 但是這種方法是不是就快呢?第一種方法是 O(n)+O(n/2),第二種方法雖然看起來是 O(n),但實際上迴圈體內部進行了兩次遍歷,看起來遍歷的量還是一樣的啊?恩,這是一個好問題(有空再想想)。參見 CSAPP 上的迴圈展開,然而,是不是真的會快還是得實際測試,並且需要到彙編層面去看這種展開能否利用上 CPU 的流水線。如果能用上,那就牛逼了。

石子游戲

原題連結

題目大意

有一個陣列 a[i],A 和 B 每次能從陣列的頭或者尾取一個數,最後誰的總數大誰贏。如果 A 先取,問 A 有沒有必勝的策略。(保證陣列有偶數個元素,總和是奇數)

題解

動態規劃

先計算出 a[i] 的前項和 sum[i],即 sum[i] = a[0]+a[1]+...a[i-1] 設 dp(i,j) 表示:在面對陣列 a[i..j] 時,如果某個人先取,最大能取得的總和 如果 A 有必勝的策略,那麼 dp(1,n) > sum[n]/2。

那怎麼來求 dp(i,j) 呢?對於 a[i..j],假設 A 先取,他有兩種選擇:

  • 如果 A 取 a[i],那麼剩下的陣列就變成了 a[i+1..j],此時該 B 取了。B 也不笨吶,B 也會用最佳的策略來取,所以 B 在 a[i+1..j] 中能取到的最大值是 dp(i+1,j)。設 a[i+1..j] 的總和是 t,想一想t-dp(i+1,j)代表什麼。t 是 a[i+1..j] 的總和,dp(i+1,j) 是先取數的那個選手所能取到的總和,顯然t-dp(i+1,j)就是另一個人能取到的總和了。回過頭來看,對於 a[i..j],Aq 取 a[i],B 在剩下 a[i+1..j] 中取的最大總和是 dp(i+1,j),因此 A 實際能取到的值是a[i]+t-dp(i+1,j)
  • 如果 A 去 a[j],那麼剩下的陣列就成了 a[i..j-1],同樣的,設 a[i..j-1] 的和是 t2,這時 A 能取到的最大值就是a[j]+t2-dp(i,j-1)

綜上, dp(i,j) = Max{a[i]+t-dp(i+1,j), a[j]+t2-dp(i,j-1)}

因此很明顯就是一個記憶化搜尋,AC 程式碼

數學歸納法

在完成動態規劃的程式碼之後,我嘗試寫一些測試用例。但是寫著寫著我發現,我竟然構造不出 A 先取但結果是 false 的 case,這讓我產生了一些想法。於是試著做一下數學歸納:

  • 假設只有兩個數 a,b,A 先取,顯然他必勝,哪個大取哪個嘛
  • 如果有 4 個數 a,b,c,d:
    • 假設 A 先取 a, 不論 B 怎麼取,A 一定能取到 c
    • 假設 A 先取 d, 不論 B 怎麼取,A 一定能取到 b

也就是說如果 A 先取,他一定可以要麼取 a,c 要麼取 b,d。如果 a+c>b+d,A 就取 a,c,否則他就取 b,d。所以對於 N 等於 4 的 case,先手一樣是必勝

  • 當有 6 個數 a,b,c,d,e,f,A 一定能取到 a,c,e 或者 b,d,f,如果 a+c+e>b+d+f,那麼就取 a,c,e 否則取 b,d,f
  • 當有 N=2*n 時,a[1...2n]
    • 如果 A 先取 a[1],他一定能取到 a[1],a[3],a[5],... 即所有的奇數項
    • 如果 A 先取 a[2n],他一定能取到所有偶數項: a[2],a[4],...a[2n]

所以如果奇數項和大於偶數項和,那麼 A 就取奇數項,否則取偶數項,因此 A 也是必勝的。

這裡你可能有個疑惑點,憑什麼說 A 先取 a[1],就一定能取到所有奇數項? 舉個例子,假設 A 取 a[1],如果 B 取 a[2],那麼 A 就可以取到 a[3]。如果 B 取 a[2n],那麼 A 可以取 a[2n-1]。A 始終可以取到奇數項。 同理,A 先取 a[2n],如果 B 取 a[1],A 就可以取 a[2],如果 B 取 a[2n-1],A 就可以取 a[2n-2],A 始終能取到偶數項。

因此,先手一定有必勝的策略。

所以最簡單的,這道題可以直接:

return true

第 N 個神奇數字

原題連結

題目大意

如果一個數能被 A或者被 B 整除,這個數就是神奇數。給定 A B,求第 i 小的神奇數

計算 + 模擬

首先求出 A 和 B 的最小公倍數 lcm(least common multiple)。lcm(A,B) = A*B/gcd(A,B)。 gcd(A,B) 是 A B 的最大公約數。 我們用 p 表示 lcm(a,b),可以很容易發現,a,2a,3a,4a ... (p/a)*a都是神奇數;同樣的b,2b,...(p/b)*b也是神奇數。所以從 0 到 p,一共有t=p/a + p/b - 1個神奇數。在 0 到 p 中,除了 p 以外,x*ay*b可能相等嗎?——不可能,如果x*a == y*b,那 p 就不是 a,b 的最小公倍數了。p 2p 3p ... tp,每增加 p,就增加了 t 個神奇數。由於求第 N 小的,所以q=N/t*t*p就是離第 N 小的神奇數最近的那個 AB 的公倍數,q 是第N/t*t個神奇數。剩下的就列舉一下即可找到。 通過計算最大公約數,最大限度的減少列舉量。不知道還有沒有更好的解法,沒關注了… AC 程式碼

盈利計劃

原題連結

題目大意

有 G 個人,有 m 個活動,每個活動需要 group[i] 個人參與,並且獲得 profit[i] 的利潤。問一個有多少種方法,使得總利潤不少於 P。

DP

仔細一看,這道題其實就是最基礎的01 揹包問題。G 其實就是揹包的容量,每個活動 group[i] 就是物品 i 的體積,profit[i] 就是把物品 i 裝入揹包得到的價值。最後求解的就是總價值不少於 P,一共有多少種裝法。

設 f(i,j,k) 表示前 i 個物品,容量 j,總價值 k,一共有多少種裝法。

  • 如果第 i 個物品不裝,f(i,j,k) = f(i-1,j,k)
  • 如果第 i 個物品裝,f(i,j,k) = f(i-1,j-group[i], k-profit[i])

由於我們求大於等於 P 一共有多少種裝法,因此我們要列舉所有f[n][j][k] (j∈[1,G], k>=P),把這些值累加起來。

對於 01 揹包,有個非常常用且簡單的優化空間的方法,就是倒序列舉,能節約 1 維空間,把f[i][j][j]變成f[j][k],感興趣自己可以在網上搜一下,詳見AC 程式碼

總結

這套題還是沒有涉及太多的演算法,主要都是靠動腦筋。石子游戲比較好,挺考驗思維的。最後一題需要對 DP 模型比較敏感,能舉一反三。這倒不是說去背題,而是說一定要把某一種型別的題吃透,之後的題會無限的多,你不可能每道題都做過,它們必然派生自某類題目。如果你吃透了那類題目的解法,那你就能很快想到解題策略。

更多原創文章乾貨分享,請關注公眾號
  • Leetcode Weekly Contest 95解題報告
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章