和幾位一直堅持刷題好朋友討論了一下刷題的頓悟時刻,他們幾位大都取得了不錯的 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 為陣列和。
總結
頓悟真的是一種非常美妙的感覺,我通過採訪幾位大佬發現大家頓悟的經歷都是類似的,那就是:
- 同樣的型別要刷很多才能頓悟
- 回顧做過的題目
- 對做過的題目進行抽象
對第三點西法通過三道題給大家做了細緻的講解,希望大家做題的時候也能掌握好節奏,舉一反三。最後祝大家刷題快樂,offer 多多。