动态规划【0-1背包问题】—leetcode每日一题—416.分割等和子集

题目:416分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
动态规划【0-1背包问题】—leetcode每日一题—416.分割等和子集_第1张图片
思路:
先排除一些特殊情况:
1.若数组长度为0,则返回false
2.若数组之和为奇数,则返回false(可利用与1的位运算来判别)
3.若数组中最大元素比数组和的一半还大,则返回false
采用动态规划,
dp[i][j]:表示可否可从数组[0,i]中选择若干个数(可为0个数),使得其和为j
初始化:令dp[i][j]中所有元素则为false
边界条件:
dp[i][0]=True(和为0,则什么数都不选,其和就是0
dp[0][nums[0]]=True,对于前0个元素,若只选第0个元素,则其和为nums[0]
状态转移方程:
若nums[i]>j,则dp[i][j]=dp[i-1][j]
若nums[i]<=j,则nums[i]可选可不选。若选,则dp[i][j]=dp[i-1][j-nums[i];若不选,则dp[i][j]=dp[i-1][j]

解答:
方法1:

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        n=len(nums)
        if n<2:
            return False
        total=sum(nums)
        Max=max(nums)
        #利用位运算来判断数组和是否为奇数,若为奇数,则位运算结果为1
        if total & 1:
            return False
        target=total//2
        if Max>target:
            return False
        
        dp=[[False]*(target+1) for _ in range(n)]
        #设立边界条件
        for i in range(n):
            dp[i][0]=True
        dp[0][nums[0]]=True
        for i in range(1,n):
            num=nums[i]
            for j in range(1,target+1):
                if num>j:
                    dp[i][j]=dp[i-1][j]
                else:
                    dp[i][j]=dp[i-1][j-num] or dp[i-1][j]
        return dp[n-1][target]

方法2:根据状态转移方程,我们知道:第i行的计算 只依赖第i-1行。因此可以只用一个两行的数组来存储中间结果。根据当前计算的行号是偶数还是奇数,来交替使用第0行和第1行。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        n=len(nums)
        if n<2:
            return False
        total=sum(nums)
        Max=max(nums)
        #利用位运算来判断数组和是否为奇数,若为奇数,则位运算结果为1
        if total & 1:
            return False
        target=total//2
        if Max>target:
            return False

        #dp[i][j]表示:从数组的 [0, i] (包含i)这个子区间内挑选一些正整数,每个数只能用一次,是否能使得这些数的和恰好等于j
        dp=[[False]*(target+1) for _ in range(2)]
        #设立边界条件
        dp[0][0],dp[0][nums[0]]=True,True
        for i in range(1,n):
            num=nums[i]
            for j in range(target+1):
                if num>j:
                    dp[i&1][j]=dp[(i-1)&1][j]
                else:
                    dp[i&1][j]=dp[(i-1)&1][j-num] or dp[(i-1)&1][j]
        return dp[(n-1)&1][target]

方法3:仔细观察状态转移方程,第i行的求解不仅是只依赖第i-1行,还明确只依赖第i-1行的第 j 个格子和第 j-num 个格子(换句话说,只依赖于【上一个格子】和【上一个格子左边的格子】。故可将第i 行的求解的顺序由【从0到target】改为【从target到0】,就可将原先的两行数组压缩到一行。对空间做了进一步优化。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        n=len(nums)
        if n<2:
            return False
        total=sum(nums)
        Max=max(nums)
        #利用位运算来判断数组和是否为奇数,若为奇数,则位运算结果为1
        if total & 1:
            return False
        target=total//2
        if Max>target:
            return False

        #dp[j]表示:从数组的 [0, i] (包含i)这个子区间内挑选一些正整数,每个数只能用一次,是否能使得这些数的和恰好等于j
        dp=[False]*(target+1)
        #设立边界条件
        dp[0],dp[nums[0]]=True,True
        for i in range(1,n):
            num=nums[i]
            for j in range(target,-1,-1):
                if num<=j:
                    dp[j]=dp[j-num] or dp[j]
        return dp[target]

方法4:按【0-1背包问题】思路求解

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        n=len(nums)
        if n<2:
            return False
        total=sum(nums)
        Max=max(nums)
        #利用位运算来判断数组和是否为奇数,若为奇数,则位运算结果为1
        if total & 1:
            return False
        target=total//2
        if Max>target:
            return False

        #dp[i][j]表示:考虑数组前i个数值,其选择数字总和不超过j的最大和。
        dp=[0]*(target+1)
        for num in nums:
            for j in range(target,-1,-1):
                if num<=j:
                    dp[j]=max(dp[j-num]+num,dp[j])
        return dp[target]==target

你可能感兴趣的:(leetcode,Python)