[LeetCode] #698. Partition to K Equal Sum Subsets
題目
Given an integer array nums
and an integer k
, return true
if it is possible to divide this array into k
non-empty subsets whose sums are all equal.
直覺解
構想:
把 nums 加總後除以 k,算出每個 subsets 要達到的總和是多少。
要分成 k 個 subset,那就迭代 nums 四次。每次都從 nums[0] 開始迭代,迭代到的每個數,如果加進來不會超過 target,就把該數加進來,然後從 nums 裡把用過的數刪掉(用 for 迭代,一邊迭代一邊刪東西,會有元素在迭代過程中被跳過去,所以採用的方式是,沒被用到的數就加進去新陣列裡面,下次就迭代新陣列)。如果加到剛好等於 target,那就找到一個符合題目要求的 subset。
若符合題目要求的 subset 不足 k 個,就回傳 False。
python 3 實作(失敗):
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
target, remainder = divmod(sum(nums), k)
if remainder != 0:
return False
subset_count = 0
for i in range(k):
total = 0
new_nums = []
for index, num in enumerate(nums):
if total + num <= target:
total += num
if total == target:
subset_count += 1
new_nums.extend(nums[index+1:])
break
else:
new_nums.append(num)
nums = new_nums
if subset_count == k:
return True
else:
return False
Submission Detail
Input:
[1,1,1,1,2,2,2,2]
4
Output: false
Expected: true
失敗原因
適當的 subset 組合是 (1, 2), (1, 2), (1, 2), (1, 2)
,但我一開始找到的是組合是 (1, 1, 1), (1, 2)
,所以就湊不齊 4 個 subset 了。
構想:
把 nums sort,每次加總的時候從最大的開始加。把迴圈從 for 改成 while,並且從 nums[-1] 往前迭代,這樣邊刪東西邊迭代的的時候,就不會有元素被跳過去了。
python 3 實作(失敗):
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
target, remainder = divmod(sum(nums), k)
if remainder != 0:
return False
subset_count = 0
nums.sort()
for i in range(k):
total = 0
new_nums = []
i = len(nums) - 1
while i >= 0:
if total + nums[i] <= target:
total += nums[i]
del nums[i]
if total == target:
subset_count += 1
i -= 1
if subset_count == k:
return True
else:
return False
Submission Detail
Input:
[3522,181,521,515,304,123,2512,312,922,407,146,1932,4037,2646,3871,269]
5
Output: false
Expected: true
失敗原因
適當的 subset 組合是 (4037, 407),(3871, 304, 269), (3522, 922), (2646, 521, 515, 312, 181, 146, 123), (2512, 1932)
但我找到的是 (4037, 407), (3522, 922), (2512, 1932)
。在找的過程中總和不是 target 卻依然被刪掉的 subset 是 (3871, 521), (2646, 515, 312, 304, 269, 181, 146)
,nums
剩下 [123]
,所以就湊不齊 5 個 subset 了。
⋯⋯好吧我終於明白這一題難在哪裡了,我之前真是太天真了。不能因為加起來不會超過 target 就粗暴地直接把該數字加進去然後刪掉。
研究別人的解
來源:Python DFS
研究重點:我想到的「不要重複使用同一個 nums[i]」 的方式,是用完後就把他從 nums 裡刪掉。他沒有刪東西,那他是怎麼避免重複使用同一個 nums[i]?他怎麼確保 nums[i] 有放在合適的 subset 裡?
別人的 python 3 實作(已加上研究用的 print):
class Solution:
@staticmethod
def canPartitionKSubsets(nums, k):
target, m = divmod(sum(nums), k)
if m:
return False
dp, n = [0]*k, len(nums)
nums.sort(reverse=True)
def dfs(i):
print('<--')
if i == n:
print('i == n', 'set(dp):', set(dp))
print('-->')
return len(set(dp)) == 1
print('i:', i, 'nums[i]:', nums[i])
for j in range(k):
dp[j] += nums[i]
print('j:', j, 'dp:', dp)
if dp[j] <= target and dfs(i+1):
print('-->')
return True
dp[j] -= nums[i]
if not dp[j]:
print('not dp[j].', 'dp[j]:', dp[j])
print('-->')
break
print('-->')
return False
return nums[0] <= target and dfs(0)
效能(沒有加 print 的版本):
Runtime: 44 ms, faster than 84.28% of Python3 online submissions for Partition to K Equal Sum Subsets.
Memory Usage: 14.3 MB, less than 51.31% of Python3 online submissions for Partition to K Equal Sum Subsets.
研究感想
我的寫法是,蒐集完一個 subset 裡要放哪些 nums[i] 之後,再蒐集下一個 subset 裡要放哪些 nums[i],所以我的 for 迴圈順序是,外圈迭代 range(k),內圈迭代 range(len(nums))。他則是把一個 nums[i] 放進合適的 subset 後,再把下一個 nums[i] 放進合適的 subset,所以他的做法是,最外層用遞迴函式遍歷 nums,遞迴函式裡面的 for 迴圈則迭代 range(k)>。因為他最外層是迭代 nums,所以他要從 sum 裡面把 nums[i] 減掉就不會重複使用 nums[i] 了,不需要做到「把 nums[i] 從 nums 裡刪掉」的地步。
先把 nums[i] 暫時先放在第一個「加進去後總和不會超過 target」的 subset 裡,然後確定 nums[i+1] 也有合適的 subset 可以安置以後,才真正確定下來 nums[i] 的確要放在這個 subset 裡。如果發現 nums[i+1] 沒有合適的 subset 可以安置,那 nums[i] 就要放到別的 subset 裡。
如果 i == n,代表 nums 裡的所有數字都被放在某個 subset 裡了,此時就檢查是否每個 subset 的總和都相等。若相等的話,len(set(dp)) 會是 1。
關於 if not dp[j]: break
試著把此行程式碼刪掉,結果效能變成了 Runtime: 2448 ms, faster than 9.12% of Python3 online submissions。看來這一行應該是為效能而增加的,例如盡快脫離不可能的 subset 組合。
試著用 nums = [2, 2, 2, 2, 2, 2], k = 4
這組 input,觀察這行程式碼如何增進效能。
subset 裡面連第一個數字都放不進去的時候,會用到這行程式碼。主要是因為 dfs(i+1) 是 false 而放不進去。不可能是因為總和大於 target 而放不進去,因為如果 nums[i] 本身就大於 target 的話,這個函式早在 return nums[0] <= target and dfs(0)
時就 return False 了,根本沒辦法執行到 if not dp[j]: break
這一行。過程如下列程式碼註釋處:
def dfs(i):
if i == n:
return len(set(dp)) == 1
for j in range(k):
dp[j] += nums[i] # dp[j] 原本是 0,現在加上了 nums[i]
if dp[j] <= target and dfs(i+1):
return True
dp[j] -= nums[i] # nums[i] 不適合放在 j 這個 subset,所以把 nums[i] 減掉,變回 0
if not dp[j]: break # 0 是 falsy,所以符合 if 設定的條件
return False
放數字到 subset 時,是前面的 subset 都放不進去,才輪得到放在後面的 subset。前面的 subset 都放不進去 nums[i] 了,後面的 subset 在 sum 明明是 0 (代表還沒有放任何數字)的情況下卻也放不進去的話,代表 nums[i] 是真的放不進任何一個 subset,所以 return False。
研究別人的解
別人的 python 3 實作:
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
target, remainder = divmod(sum(nums), k)
if remainder: return False
def searchSubsets(subsets):
if not nums: return True
v = nums.pop()
for i, val in enumerate(subsets):
if val + v <= target:
subsets[i] += v
if searchSubsets(subsets):
return True
# backtrack
subsets[i] -= v
if not subsets:
break
# backtrack
nums.append(v)
# This combination did not work return False and backtrack or go back to calling function
return False
nums.sort()
if nums and nums[-1] > target:
return False
if nums and nums[-1] == target:
nums.pop()
k -= 1
return searchSubsets([0] * k)
效能:
Runtime: 1696 ms, faster than 15.16% of Python3 online submissions for Partition to K Equal Sum Subsets.
Memory Usage: 14.5 MB, less than 15.44% of Python3 online submissions for Partition to K Equal Sum Subsets.
研究感想
解題構想跟上一個解差不多。在實作上不太一樣的地方:
-
上一個解:
if i == n: return len(set(dp)) == 1
這一個解:if not nums: return True
把上一個解改成if i == n: return True
的話也能 submit 成功 -
上一個解:
if not dp[j]: break
這一個解:if not subsets: break
把這一個解改成if not subsets[i]: break
的話效能改善很多:
Runtime: 36 ms, faster than 96.35% of Python3 online submissions for Partition to K Equal Sum Subsets.
Memory Usage: 14.2 MB, less than 75.37% of Python3 online submissions for Partition to K Equal Sum Subsets.
Comments
Post a Comment