給定一個只包含正整數的非空數組。是否可以將這個數組分割成兩個子集,使得兩個子集的元素和相等。
注意:
每個數組中的元素不會超過 100
數組的大小不會超過 200
示例 1:
輸入: [1, 5, 11, 5]
輸出: true
解釋: 數組可以分割成 [1, 5, 5] 和 [11].
示例 2:
輸入: [1, 2, 3, 5]
輸出: false
解釋: 數組不能分割成兩個元素和相等的子集.
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/partition-equal-subset-sum
解析:首先由題意正整數和非空,能分成兩個等集合,則數組和sums
必須是偶數,sums%2==0
能被2整除,其次裏面的數進行組合相加要能等於target=sum/2
,否則無法分成兩部分。這道題就變成了,集合nums
中是否存在幾個數相加等於target
。
題目給的數組大小不會超過200,元素不會超過100,暴力搜索的話,100*100=10000複雜度也不會太高,計算機基本能完成。所以可以嘗試計算出所有組合的和,看是否等於sums/2
。
當然這道題還可以用動態規劃來做,作爲算法題,應該優先考慮動態規劃的解法:
一、動態規劃思路:
建立二維表格dp
,dp[i][j]
表示nums[0]~nums[i]
之間的數,是否能經過一系列組合得到j
,能的話dp[i][j]=True
,反之False
。我們的目標就是要知道dp[len(nums)-1][target]
是True
還是False
判斷轉移條件:當前nums[i] whether == j
:
nums[i]>j | dp[i][j]=dp[i-1][j] |
不選用,查找下一個數 |
---|---|---|
nums[i]<=j | dp[i][j]=dp[i-1][j-nums[i]] |
選用當前數 |
邊界條件:
由上表可知,我們首先要初始化i=0
的情況,然後遍歷i
從1
開始,這樣才能保證i-1!=負數
,
dp[0][j]
表示,nums[0]
是否等於j
,所以只有dp[0][nums[0]]=True
其他情況都是False
。當然前提是nums[0]<=target
否則就超出表的邊界了。
代碼:
class Solution:
def canPartition(self, nums: List[int]) -> bool:
leng=len(nums) # 數組總數
target,remain=sums=divmod(sum(nums),2) # 得到target並判斷是否是偶數
if remain :
return False # 如果不是偶數,肯定不能分成兩部分整數
dp=[[False for _ in range(target+1) ] for _ in range(leng)]
# 先分析下:如果nums[i]==j,則 dp[i][j]=True
#如果nums[i]>j,dp[i][j]=dp[i-1][j] # 從這裏可以看出我們需要對i=0這一列初始化,
#否則i-1可能是負數,計算從i=1開始
#如果 nums[i]<j ,dp[i][j]=dp[i-1][j-num[i]]
# 初始化
# for j in range(target+1):
# if nums[0]==target:
if nums[0]<=target: # 判斷第一個數是否超出邊界,如果超出了 那肯定也不能分成兩部分
dp[0][nums[0]]=True# 只有這個是true 其他一定都是false
else:
return False
# i 表示長度區間,從1開始,上面已經說了i=0這一行已經初始化過了
for i in range(1,leng):
#j表示當前的目標值
for j in range(target+1):
# if nums[i] == j:
# dp[i][j]=True
dp[i][j]= dp[i-1][j] or dp[i-1][j-nums[i]] #兩種情況只要有一個是true結果就是true
#原本應該這樣寫:
#if j >= nums[i]:
# dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]
#else:
# dp[i][j] = dp[i - 1][j]
# 但是通過列出表格可以看到即使j-nums[i]是負數,得到的結果依然沒有影響,主要是or的作用
#只要同一列的都是True,其實這也算特殊性吧,實際設計的思路是要考慮這種情況的
return dp[-1][-1] # dp[leng-1][target]
執行用時 :1624 ms
, 在所有 python3
提交中擊敗了31.99%
的用戶
內存消耗 :17.7 MB
, 在所有 python3
提交中擊敗了5.82%
的用戶
降二維數組爲一維數組,由於每次當前位置都有上一行以及左上值決定,下一行是把上一行先完全複製即dp[i-1][j]
,在根據上一行的轉移狀態dp[i-1][j-nums[i]]
來共同決定的。所以這個複製上一行狀態完全可以省略,只需要每次根據之前狀態和轉移狀態決定就可以了。變成一維數組,數組的狀態,從原來二維數組的第一行一直更新到最後一行,就是我們要的最終狀態。
借用letecode別人畫的圖,第一行如果確定下來了,從第二行開始,先依據第一行的狀態,在查看轉移狀態,比如第二行,先copy第一行狀態,然後[1,6]
位轉移狀態是對應的[0,1]
爲True
,所以跟新爲True
重新定義dp
表的意義,dp[j]
表示j是否能被nums裏面的數表示。
轉移判定爲是否使用nums[i]。 是:dp[j]=dp[j-nums[i]]
否:dp[j]=dp[j]
沿用之前狀態
代碼:
class Solution:
def canPartition(self, nums: List[int]) -> bool:
leng=len(nums)
target,remain=sums=divmod(sum(nums),2)
if remain :
return False
dp=[False for _ in range(target+1) ]
# 先分析下:
#初始狀態:就是二維數組的第一行初始狀態
#dp[nums[0]]的位置是True,其他都是False
#開始跟新:
#同二維數組,只不過i-1的操作可以省略,直接是當且一維表
#注意:這裏列的遍歷需要逆序,因爲在二維表中,我們是從上一行左側開始尋找值,也就是說一維在跟新的時候,必須維持原來的狀態(對應二維數組的上一行)的左側。
#所以跟新狀態應該從右到左跟新,才能保證右側的狀態先跟新,尋找左側的時候還是原來的狀態
if nums[0]<=target:
dp[nums[0]]=True# 只有這個是true 其他一定都是false
else:
return False
# i 表示長度區間
for i in range(1,leng):
#j表示當前的目標值
dp[-1]=dp[-1] or dp[target-nums[i]]
if dp[-1]==True :
return True
for j in range(target-1,0,-1): # 0不需要計算
# if nums[i] == j:
# dp[j]=True
# continue
if j>= nums[i]:
dp[j]=dp[j] or dp[j-nums[i]]
# else:
# break #如果j<numsp[i],由於逆序,後面的j肯定都小於nums[i],所以不必判斷了
return dp[target]
執行用時 :580 ms
, 在所有 python3
提交中擊敗了78.06%
的用戶
內存消耗 :12.8 MB
, 在所有 python3
提交中擊敗了99.03%
的用戶
“哈希迭代”:
找出所有可能的組合,並判斷target
是否在組合裏面
代碼設計:比如一個集合裏面有{t1,t2,t3}
那麼他們的組合列表是{t1,t2,t3,t1+t2,t1+t3,t2+t3}
,如果組合到了target
就可以提前結束搜尋。
但是這樣所有組合保存,可能造成內存溢出,我們只關注組合的數值,而不需要關注具體組合的數,所以可以把不同組合但是最後數值相同的組合,只保留一個數值就可以了。比如t1+t2==t1+t3==t3+t4=6
那麼只要保存一個6
就可以了。注意這裏的組合方式還是比較有意思的。每次組合是在之前的基礎上的累加,有點類似於樹搜索。
class Solution:
def canPartition(self, nums: List[int]) -> bool:
leng=len(nums)
target,remain=sums=divmod(sum(nums),2)
if remain :
return False
ans={0} # 收集組合的集合,set的話可以去重,因爲只關注組合的數值,而不需要關注具體組合的數,防止內存溢出
for i in nums:
for j in list(ans): # 這個集合會在迭代過程中改變,所以需要轉換爲List
j+=i
if j==target:
return True
ans.add(j)
return False
執行用時 :292 ms
, 在所有 python3
提交中擊敗了87.26%
的用戶
內存消耗 :12.8 MB
, 在所有 python3
提交中擊敗了99.03%
的用戶
這種方法速度反而更快,對比動態規劃的方法,其實也是設置了數值保存可能的組合值,但是這裏有去重的操作,所以總的遍歷要少。
使用Bitset數據結構記錄:
這種方法的思路其實和上面是一樣的,保存所有組合的可能值,但是用的是二進制的數據結構儲存,在內存和速度上都有極大的優勢!
class Solution:
def canPartition(self, nums: List[int]) -> bool:
flag = 1 # 初始化
sumnums = 0
for i in nums:
sumnums += i # 記錄和
flag = flag | flag << i # 記錄所有可能的結果
if sumnums % 2 == 0: # 和爲偶數纔有解
sumnums //= 2
else:
return False
target = 1 << sumnums # 目標和
if target & flag != 0: # 目標位置上不爲0
return True
else:
return False
執行用時 :64 ms
, 在所有 python3
提交中擊敗了93.22%
的用戶
內存消耗 :12.8 MB
, 在所有 python3
提交中擊敗了100.00%
的用戶