回溯与递归----leet_416

177 阅读3分钟

递归思想解决问题

本题为lc第416题, 分割等和子集,即判断一个数字是否能分成两个数组,使得每个数组的和相等。
题目链接

分析

方法一:回溯(递归)

  1. 如果数组的sum(和)为奇数,则肯定不满足题意
  2. 如果数组中某些值的组合之和为sum的一半,定义为target则满足题意
  3. 将数组先逆序排列有助于降低时间复杂度,因为,我们一旦找到正确值,就返回true, 逆序排列有助于快速找到正确值。
  4. 逆序排列之后,如果第一个值就大于target,无需进行下去,肯定不满足题意,因为存在一个值大于sum的一半,剩下的和肯定小于sum的1/2,不满足。
  5. 此时,有两种情况,第一种,某些值的组合之和,这些值中包含第一个值,则现在target变为target减去第一个值,我们只需要判断从第二个值开始是否能找到某些组合之和等于targrt - firstOne即可;第二种情况,这些值并不包含第一个值,那么我们只需要判断从第二个值开始是否能找到某些值的组合之后等于target。二者结合,得到结果。
  6. step5中两种情况,每一种情况继续递归下去,得到最终结果

方法二: 动态规划(递推)

  1. (同理方法一)
  2. (同理方法二)
  3. (同理方法一),但是此方法逆序和顺序应该差别不大。
  4. 从第一个数开始,用一个集合保存下这个数之前的所有可能出现的和,一旦出现等于target,返回结果,否则,继续递推下去。
  5. 此方法空间复杂度相对较高,因为每一次都需要一个list或者set来存储当前的可能的和。

代码实现

方法一:

public boolean canPartition(int[] nums) {
        int sum = 0;
        //求和
        for(int each : nums) {
            sum += each;
        }
        if(sum % 2 == 1) return false;
        
        //数组顺序排序
        Arrays.sort(nums);
        
        //数组倒序(变为逆序)
        int left = 0;
        int right = nums.length - 1;
        while(left < right) {
            int temp = nums[left];
            nums[left] = nums[right];
            nums[right] = temp;
            left += 1;
            right -= 1;
        }
        
        //调用递归函数 helper()
        int value = sum / 2;
        return helper(value, nums, 0);
    }

    public static boolean helper(int target, int[] nums, int startIndex) {
        //当前判断的第一个值的索引不能超过数组的最大索引
        if(startIndex >= nums.length) return false;
        //找到一个正确值,返回
        if(nums[startIndex] == target) return true;
        //如果当前第一个值已经大于target, 肯定错误,返回
        if(nums[startIndex] > target) return false;
        // 递归,分别为包含第一个值和不包含第一个值的情况
        return helper(target - nums[startIndex], nums, startIndex + 1) || helper(target, nums, startIndex + 1);
    }

方法二代码:

    public boolean canPartition(int[] nums) {
        //求和
        int sum = 0;
        for(int each : nums) {
            sum += each;
        }
        if(sum % 2 == 1) return false;
        
        //顺序排列数组
        Arrays.sort(nums);
        
        //逆序,可加可不加
        // int left = 0;
        // int right = nums.length - 1;
        // while(left < right) {
        //     int temp = nums[left];
        //     nums[left] = nums[right];
        //     nums[right] = temp;
        //     left += 1;
        //     right -= 1;
        // }
        
        Set<Integer> set = new HashSet<>();
        //如果顺序排列,则可以减去数组中最大值,方便更快得到结果,如果逆序,则value = sum / 2 即可
        int value = sum / 2 - nums[nums.length - 1];

        if(value == 0) return true;
        for(int each : nums) {
            if(each == value) return true;
            Set<Integer> temp = new HashSet<>(set);
            for(int each1 : set) {
                if(each + each1 == value) return true;
                temp.add(each1 + each);
            }
            temp.add(each);
            set = temp;
        }
         return false;
    }

简单理解和总结

针对这道题,回溯是更好的算法。但是单纯从思路上来说,都很重要。回溯好理解,只需列举出现的情况,然后找出跳出跳出递归的条件和界线即可。动态规划,在很多问题上,虽然会付出空间上的代价, 但是往往会大大降低时间复杂度。