LeteCode刷題:416. 分割等和子集(中等難度)

給定一個只包含正整數非空數組。是否可以將這個數組分割成兩個子集,使得兩個子集的元素和相等。

注意:

每個數組中的元素不會超過 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
當然這道題還可以用動態規劃來做,作爲算法題,應該優先考慮動態規劃的解法:

一、動態規劃思路:

 建立二維表格dpdp[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的情況,然後遍歷i1開始,這樣才能保證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%的用戶

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章