聊聊刷題中的頓悟時刻

lucifer發表於2021-10-20

和幾位一直堅持刷題好朋友討論了一下刷題的頓悟時刻,他們幾位大都取得了不錯的 offer,比如 Google 微軟,Amazon,BAT 等。

通過和他們的溝通,我發現了大家頓悟時刻都是類似的。那具體他們有哪些相似點呢?我們一起來看下。

1. 同樣的型別要刷很多才能頓悟

比如你想頓悟二分,那麼首先你需要做足夠多的二分題。

而由於二分其實是一個大的分類。因此理論上你如果想對二分大類頓悟,那麼必不可少的是先做足夠多的二分題,並且這些題目可以覆蓋所有的二分型別

比如西法總結的基礎二分,最左最右二分以及能力檢測二分,其中大部分有點困難的題目都是能力檢測二分。

對二分不熟悉的可以看下西法之前總結的《二分專題》:

那麼推而廣之,如果你想對刷演算法題整體進行頓悟,那麼就不得不先做足夠多的題目,且這些題目能覆蓋所有你想頓悟的考點

這也就是說為什麼你看的大佬中的大佬都刷了上千道題的原因。因為沒有上千道題目的積累,你很難對所有題目型別都頓悟的。當然你如果只是應付大多數的考點並且不參與競賽的話,也許小几百道也是 ok 的。

2. 回顧做過的題目

有的同學比較直接,他們就是直接複習做過的題目。而有的同學則是通過做新的題目回想到之前做過的某些題,從而達到複習的作用。

不管是哪種型別。他們都必須經過一個階段,那就是和已經做過的題目建立聯絡。如果你只是盲目做題的話,效率肯定上不去。

最開始刷題的時候,我會建立一些 anki 卡片。這其實就是為了強制回顧做過的題目。另外做新的題目的時候,我會強迫自己思考:

  • 這道題考察了什麼知識?
  • 和之前做過的哪些題可以建立聯絡?
  • 是否可以用之前刷題的解法套?
  • corner case 有哪些?
  • 。。。

經過這些思考,慢慢就會把做過的題目有機地結合起來,而不是讓這些題目變成彼此的資訊孤島。

3. 對做過的題目進行抽象

這個是我要講的最後一點,但是這點卻尤為重要,說它是最重要也不過分。

一方面,如果一道題目沒有經過抽象,那麼我們很難記住,很難在未來回憶起來。另一方面,如果一道題目能夠抽象為純粹的題目,那麼說明你對這個題目看的比較透徹了。將來碰到換皮題,你一抽象,就會發現: 這不就是之前 xxxx 的換皮題麼?

經常看我題解和文章的同學知道我之前寫過不少換皮題的扒皮解析,這就是我做題和寫文章風格。

在這裡,我再舉個例子。

注意:下面舉的三道題例子都需要你掌握二分法的能力檢測二分,如果不瞭解建議先看下我上面的文章。

Shopee 的零食櫃

這是 shopee 的校招程式設計題。

題目描述

shopee的零食櫃,有著各式各樣的零食,但是因為貪吃,小蝦同學體重日益增加,終於被人叫為小胖了,他終於下定決心減肥了,他決定每天晚上去操場跑兩圈,但是跑步太累人了,他想轉移注意力,忘記痛苦,正在聽著音樂的他,突然有個想法,他想跟著音樂的節奏來跑步,音樂有7種音符,對應的是1到7,那麼他對應的步長就可以是1-7分米,這樣的話他就可以轉移注意力了,但是他想保持自己跑步的速度,在規定時間m分鐘跑完。為了避免被累死,他需要規劃他每分鐘需要跑過的音符,這些音符的步長總和要儘量小。下面是小蝦同學聽的歌曲的音符,以及規定的時間,你能告訴他每分鐘他應該跑多少步長?



輸入描述:
輸入的第一行輸入 n(1 ≤ n ≤ 1000000,表示音符數),m(1<=m< 1000000, m <= n)組成,

第二行有 n 個數,表示每個音符(1<= f <= 7)


輸出描述:
輸出每分鐘應該跑的步長
示例1
輸入
8 5 6 5 6 7 6 6 3 1
輸出
11

連結:https://www.nowcoder.com/ques...
來源:牛客網

思路

經過抽象,這道題本質上就是給你一個陣列(陣列值範圍是 1 到 7 的整數),讓你將陣列分為最多 m 子陣列,求 m 個子陣列和的最小值

直接回答子陣列和最小值比較困難,但是回答某一個具體的值是否可以達到相對容易。

比如回答子陣列和最小值為 100 可以不可以相對容易。因為我們只需要遍歷一次陣列,如果連續子陣列大於 100 就切分新的一塊,這樣最後切分的塊數小於等於 m 就意味著 100 可以。

另外一個關鍵點是這種檢測具有單調性。比如 100 可以,那麼任何大於 100 的數(比如 101)肯定都是可以的。如果你看過我上面的《二分專題》或者做過不少能力檢測二分的話, 不難想到可以利用這種單調性做能力檢測二分得到答案。並且我們要找到滿足條件的最小的數,因此可以套用最左能力檢測二分得到答案。

程式碼

暫時不寫,因為這道題和後面的一道題是一樣的。

410. 分割陣列的最大值

題目描述

給定一個非負整數陣列 nums 和一個整數 m ,你需要將這個陣列分成 m 個非空的連續子陣列。

設計一個演算法使得這 m 個子陣列各自和的最大值最小。

 

示例 1:

輸入:nums = [7,2,5,10,8], m = 2
輸出:18
解釋:
一共有四種方法將 nums 分割為 2 個子陣列。 其中最好的方式是將其分為 [7,2,5] 和 [10,8] 。
因為此時這兩個子陣列各自的和的最大值為18,在所有情況中最小。
示例 2:

輸入:nums = [1,2,3,4,5], m = 2
輸出:9
示例 3:

輸入:nums = [1,4,4], m = 3
輸出:4
 

提示:

1 <= nums.length <= 1000
0 <= nums[i] <= 106
1 <= m <= min(50, nums.length)

來源:力扣(LeetCode)
連結:https://leetcode-cn.com/probl...

思路

這道題官方難度是 hard。和前面題抽象後一模一樣,不用我多解釋了吧?

你看經過這樣的抽象,是不是有種殊途同歸的頓悟感覺?

程式碼

程式碼支援:Python3

Python3 Code:


class Solution:
    def splitArray(self, nums: List[int], m: int) -> int:
        lo, hi = max(nums), sum(nums)
        def test(mid):
            cnt = acc = 0
            for num in nums:
                if acc + num > mid:
                    cnt += 1
                    acc = num
                else:
                    acc += num
            return cnt + 1 <= m

        while lo <= hi:
            mid = (lo + hi) // 2
            if test(mid):
                hi = mid - 1
            else:
                lo = mid + 1
        return lo

你以為這就完了麼? 類似的題目簡直不要太多了。西法再給你舉個例子。

LCP 12. 小張刷題計劃

題目描述

為了提高自己的程式碼能力,小張制定了 LeetCode 刷題計劃,他選中了 LeetCode 題庫中的 n 道題,編號從 0 到 n-1,並計劃在 m 天內按照題目編號順序刷完所有的題目(注意,小張不能用多天完成同一題)。

在小張刷題計劃中,小張需要用 time[i] 的時間完成編號 i 的題目。此外,小張還可以使用場外求助功能,通過詢問他的好朋友小楊題目的解法,可以省去該題的做題時間。為了防止“小張刷題計劃”變成“小楊刷題計劃”,小張每天最多使用一次求助。

我們定義 m 天中做題時間最多的一天耗時為 T(小楊完成的題目不計入做題總時間)。請你幫小張求出最小的 T是多少。

示例 1:

輸入:time = [1,2,3,3], m = 2

輸出:3

解釋:第一天小張完成前三題,其中第三題找小楊幫忙;第二天完成第四題,並且找小楊幫忙。這樣做題時間最多的一天花費了 3 的時間,並且這個值是最小的。

示例 2:

輸入:time = [999,999,999], m = 4

輸出:0

解釋:在前三天中,小張每天求助小楊一次,這樣他可以在三天內完成所有的題目並不花任何時間。

 

限制:

1 <= time.length <= 10^5
1 <= time[i] <= 10000
1 <= m <= 1000

來源:力扣(LeetCode)
連結:https://leetcode-cn.com/probl...

思路

和前面的題目類似。經過抽象,這道題本質上就是給你一個陣列(陣列值範圍是 1 到 10000 的整數),讓你將陣列分為最多 m 子陣列,每個子陣列可以刪除最多一個數,求 m 個子陣列和的最小值

和上面題目唯一的不同是,這道題允許我們在子陣列中刪除一個數。顯然,我們應該貪心地刪除子陣列中最大的數。

因此我的思路就是能力檢測部分維護子陣列的最大值,並在每次遍歷過程中增加判斷:如果刪除子陣列最大值後以後可以滿足子陣列和小於檢測值(也就是 mid)。

程式碼

程式碼支援:Python3

Python3 Code:

class Solution:
    def minTime(self, time: List[int], m: int) -> int:
        def can(mid):
            k = 1 # 需要多少天
            t = 0 # 當前塊的總時間
            max_time = time[0]
            for a in time[1:]:
                if t + min(max_time, a) > mid:
                    t = 0
                    k += 1
                    max_time = a
                else:
                    t += min(max_time, a)
                    max_time = max(max_time, a)
            return k <= m

        l, r = 0, sum(time)

        while l <= r:
            mid = (l+r)//2
            if can(mid):
                r = mid - 1
            else:
                l = mid + 1
        return l

時間複雜度的話三道題都是一樣的,我們來分析一下。

我們知道,時間複雜度分析就看執行次數最多的程式碼即可,顯然這道題就是能力檢測函式中的程式碼。由於能力檢測部分我們需要遍歷一次陣列,因此時間為 $O(n)$,而能力檢測函式執行的次數是 $logm$。因此時間複雜度都是 $nlogm$,其中 n 為陣列長度,m 為陣列和。

總結

頓悟真的是一種非常美妙的感覺,我通過採訪幾位大佬發現大家頓悟的經歷都是類似的,那就是:

  1. 同樣的型別要刷很多才能頓悟
  2. 回顧做過的題目
  3. 對做過的題目進行抽象

對第三點西法通過三道題給大家做了細緻的講解,希望大家做題的時候也能掌握好節奏,舉一反三。最後祝大家刷題快樂,offer 多多。

相關文章