[LeetCode] #1553. Minimum Number of Days to Eat N Oranges
題目
There are n oranges in the kitchen and you decided to eat some of these oranges every day as follows:
Eat one orange.
If the number of remaining oranges (
n
) is divisible by 2 then you can eat n/2 oranges.If the number of remaining oranges (
n
) is divisible by 3 then you can eat 2*(n/3) oranges.
You can only choose one of the actions per day.
Return the minimum number of days to eat n
oranges.
直覺解
構想:
每天,看哪個方式可以吃最多橘子,就用那個吃法。但是這題是 Hard,總覺得事情沒有這麼簡單⋯⋯
python 3 實作(失敗):
class Solution:
def minDays(self, n: int) -> int:
days = 0
while n > 0:
if n % 3 == 0:
n = n // 3
elif n % 2 == 0:
n = n >> 1
else:
n -= 1
days += 1
return days
Submission Detail
Wrong Answer
Input: 10
Output: 5
Expected: 4
失敗原因
我吃橘子的顆數是:5, 1, 2, 1, 1,花了五天。解答吃橘子的顆數是:1, 6, 2, 1,只花了四天。
並不是簡單的「每天都選三個方法中,能吃最多橘子的方法」,要為「將來能吃更多橘子」製造機會。
構想:
先用上一個版本的解,算出 days 的最大值 max_days。用 recursion 嘗試所有吃橘子的組合,如果花的時間超過 max_days 就放棄這個組合。
python 3 實作(失敗):
class Solution:
minDays(self, n: int) -> int:
max_days = 0
number_of_oranges = n
while n > 0:
if n % 3 == 0:
n = 2 * (n // 3)
elif n % 2 == 0:
n = n >> 1
else:
n -= 1
max_days += 1
return self.recursion(number_of_oranges, 0, max_days)
def recursion(self, n: int, days, max_days: int) -> int:
if n == 0:
return days
if days >= max_days:
return max_days
days += 1
action1_days = self.recursion(n - 1, days, max_days)
action2_days = self.recursion(n >> 1, days, max_days) if n % 2 == 0 else max_days
action3_days = self.recursion(n // 3, days, max_days) if n % 3 == 0 else max_days
return min(action1_days, action2_days, action3_days)
Submission Detail
Time Limit Exceeded
Last executed input: 9314
失敗原因
算太久了。
構想:
「嘗試所有可能的吃橘子方式」看來是不行的,會算太久。嘗試更改吃橘子的順序。
python 3 實作(失敗):
class Solution:
def minDays(self, n: int) -> int:
days = 0
while n > 0:
remainder = n % 3
if remainder == 0:
n = 1 * (n // 3)
elif remainder == 2 and n % 2 == 0:
n = n >> 1
else:
n -= 1
days += 1
return days
Submission Detail
Wrong Answer
Input: 16
Output: 6
Expected: 5
失敗原因
我吃橘子的顆數是:1, 10, 1, 1, 2, 1,花了六天。解答吃橘子的顆數是:8, 4, 2, 1, 1,只花了五天。
input 是 16(24)的時候,可以一直採用「吃一半」的方式,而不需要額外浪費一天用「吃一個」去調整橘子的數量以符合 2 或 3 的倍數。
input 是 10 的時候,吃一個就可以把橘子數量調整成 9(32),就可以一直採用「吃 2 / 3」的方式,而不需要額外浪費一天用「吃一個」去調整橘子的數量以符合 2 或 3 的倍數。
所以這其實是類似 n = (1 * x) + 2y * 3z,然後要取最小的 x + y + z 這樣的概念?
研究別人的解
研究重點:想看更簡潔、更有效率的寫法。
別人的 python 3 實作:
from functools import lru_cache
class Solution:
@lru_cache()
def minDays(self, n: int) -> int:
if n <= 1:
return n;
return 1 + min(n % 2 + self.minDays(n // 2), n % 3 + self.minDays(n // 3));
效能:
Runtime: 44 ms, faster than 67.61% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
Memory Usage: 14 MB, less than 99.69% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
研究感想
用 + n % 2(或 n % 3)取代「把 n 取成 2 或 3 的倍數」。這樣就不會像我的解那樣,花很多時間在「一天吃一個橘子」的步驟裡了。
用 lru_cache 記憶某些 input 的 return 值,以後遇到相同 input 時就不需要再重算一次。
試著優化我的 recursion 解:
from functools import lru_cache
class Solution:
def minDays(self, n: int) -> int:
max_days = 0
number_of_oranges = n
while n > 0:
if n % 3 == 0:
n = 2 * (n // 3)
elif n % 2 == 0:
n = n >> 1
else:
n -= 1
max_days += 1
return self.recursion(number_of_oranges, 0, max_days)
@lru_cache()
def recursion(self, n: int, days, max_days: int) -> int:
if n == 1:
return days + 1
if days >= max_days:
return max_days
days += 1
action2_days = self.recursion(n >> 1, days + (n % 2), max_days)
action3_days = self.recursion(n // 3, days + (n % 3), max_days)
return min(action2_days, action3_days)
效能:
Runtime: 892 ms, faster than 5.03% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
Memory Usage: 14.3 MB, less than 88.68% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
我覺得我和他的解概念應該差不多啊,我甚至還有檢查 max_days 提早結束遞迴,為什麼我的會慢這麼多?繼續做實驗。聽說遞回函式,如果遞迴的方式是 return 自己的話,通常編譯器會做一些優化避免記憶體被函式塞爆。試試看 retunr recursion 的寫法,並且取消 max_days 的檢查:
from functools import lru_cache
class Solution:
def minDays(self, n: int) -> int:
return self.recursion(n, 0)
@lru_cache()
def recursion(self, n: int, days) -> int:
print(n)
if n <= 1:
return days + n
return min(self.recursion(n >> 1, days + 1 + (n % 2)), self.recursion(n // 3, days + 1 + (n % 3)))
效能:
Runtime: 832 ms, faster than 5.03% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
Memory Usage: 14.1 MB, less than 94.97% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
取消 max_days 的檢查之後,居然還變快一點點,真是太不給我面子了。繼續做實驗,這次改成取消 days 這個參數。
from functools import lru_cache
class Solution:
def minDays(self, n: int) -> int:
return self.recursion(n)
@lru_cache()
def recursion(self, n: int) -> int:
if n <= 1:
return n
return 1 + min(n % 2 + self.recursion(n >> 1), n % 3 + self.recursion(n // 3))
效能:
Runtime: 32 ms, faster than 98.74% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
Memory Usage: 14.1 MB, less than 94.97% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
一個 days 參數就有這麼驚人的變化,到底是為什麼啊⋯⋯跟 lru_cache 的特性有關嗎?
根據 通過原始碼學習@functools.lru_cache 這篇文章,看起來,lru_cache 是用輸入函式的參數當做快取的 key。所以多了一個參數 day,如果 n 相同但 days 不同,會被當成不同的 key,就要再重算一次;而且快取的 maxsize 預設是 128,如果紀錄了很多 n 相同但 days 不同 的 key,會多佔空間。
接下來試著不用 lru_cache。自己手刻 dp。既然用了 dp,其實就不用檢查 max_days 了。
他的快取的紀錄方式,key 代表橘子總共有幾顆, value 代表要用幾天吃完。我的 dp 紀錄方式,key 是 (n, days),因為我的算法會提前把 days 加一天,所以 dp 的 key 解讀方式是「用 days - 1 的時間把橘子吃到剩下 n 顆」。每一筆 key-value 代表「用 key 所描述的方式吃橘子的話,把 input 這麼多的橘子吃完需要 value 天」。 這個紀錄方式,讀起來真是有夠複雜的⋯⋯
class Solution:
def __init__(self):
self.dp = {}
def minDays(self, n: int) -> int:
return self.recursion(n, 0)
def recursion(self, n: int, days) -> int:
if n <= 1:
return days + n
if self.dp.get((n, days)) is None:
days += 1
action2_days = self.recursion(n >> 1, days + (n % 2))
action3_days = self.recursion(n // 3, days + (n % 3))
self.dp[(n, days)] = min(action2_days, action3_days)
return self.dp[(n, days)]
效能:
Runtime: 2576 ms, faster than 5.03% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
Memory Usage: 14.4 MB, less than 77.04% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
取消 days 這個參數:
class Solution:
def __init__(self):
self.dp = {}
def minDays(self, n: int) -> int:
if n <= 1:
return n
if self.dp.get(n) is None:
self.dp[n] = 1 + min(n % 2 + self.minDays(n >> 1), n % 3 + self.minDays(n // 3))
return self.dp[n]
效能:
Runtime: 36 ms, faster than 95.22% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
Memory Usage: 14 MB, less than 98.73% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
研究別人的解
來源:Python 3 Dynamic Programming
研究重點:不用 recursion 的做法。
別人的 python 3 實作:
class Solution:
def minDays(self, n: int) -> int:
dp_dict = {n: 0}
queue = [n]
while queue:
num = queue.pop( 0 )
new_num = num // 2
if new_num:
new_dp = dp_dict[num] + 1 + num % 2
else:
new_dp = dp_dict[num] + num % 2
if new_num not in dp_dict or new_dp < dp_dict[new_num]:
dp_dict[new_num] = new_dp
if new_num and new_num not in queue:
queue.append( new_num )
new_num = num // 3
if new_num:
new_dp = dp_dict[num] + 1 + num % 3
else:
new_dp = dp_dict[num] + num % 3
if new_num not in dp_dict or new_dp < dp_dict[new_num]:
dp_dict[new_num] = new_dp
if new_num and new_num not in queue:
queue.append( new_num )
return dp_dict[0]
效能:
Runtime: 40 ms, faster than 81.85% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
Memory Usage: 14.2 MB, less than 88.54% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
研究感想
他的 dp_dict, key-value 代表「花了 value 這麼多天,把橘子吃到剩下 key 顆」。
因為不像遞回那樣,可以設定 n >= 1 時 return n,所以計算 new_dp 時,要根據 new_dp 是否為零,判斷要不要多加 1。發現可以更快吃完橘子的話,會更新 dp_dict,所以不需要保證把紀錄寫進 dp_dict 時,就必須是最小天數。
研究別人的解非遞迴解後,自己寫一個非遞迴解
構想:
dp 的紀錄方式,key 代表橘子總共有幾顆, value 代表要用幾天吃完。因為一但把紀錄寫進 dp,就不會再更新了,所以一開始寫進去的時候,就必須保證是最小天數,導致需要把 num 從 stack 拿出來又放回去(因為目前的資訊不足以算出最小天數,所以放回去)。
python 3 實作:
class Solution:
def __init__(self):
self.dp = {0: 0, 1: 1}
def minDays(self, n: int) -> int:
stack = [n]
while stack:
num = stack.pop()
if self.dp.get(num) is not None:
continue
num_div_2 = num >> 1
num_div_3 = num // 3
if self.dp.get(num_div_2) is not None and self.dp.get(num_div_3) is not None:
num_div_2_dp = self.dp[num_div_2] + num % 2 + bool(num_div_2)
num_div_3_dp = self.dp[num_div_3] + num % 3 + bool(num_div_3)
self.dp[num] = min(num_div_2_dp, num_div_3_dp)
continue
stack.append(num)
if self.dp.get(num_div_2) is None:
stack.append(num_div_2)
if self.dp.get(num_div_3) is None:
stack.append(num_div_3)
return self.dp[n]
效能:
Runtime: 56 ms, faster than 37.90% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
Memory Usage: 14.2 MB, less than 88.54% of Python3 online submissions for Minimum Number of Days to Eat N Oranges.
Comments
Post a Comment