K-SUM 算法及子问题 2-SUM、3-SUM、4-SUM

2-SUM 问题

Question

​ Given an array of integers, return indices of the two numbers such that they add up to a specific target. You may assume that each input would have exactly one solution, and you may not use the same element twice.

​ 根据给定整数数组,选出相加为给定整数的两个数组元素的位置;假定有且只有一个正解,数组中每个元素只能使用一次;

解法一

​ 根据题目可知,只需要计算数组中两两组合的和等于给定数即可,首先想到的是暴力遍历的方法,对数组遍历两次,两两计算和,找到正解就返回;

public int[] sum2(int[] nums, int target){
    int length = nums.length;
    for (int i = 0; i < length - 1; i++){
        for (int j = i + 1; j < length; j++){
            if (nums[i] + nums[j] == target){
                return new int[]{i, j};
            }
        }
    }
    return null;
}

​ 这种解法思路比较简单,但是时间复杂度是 $O(n^2)$;

解法二

​ 对解法一进一步分析,暴力破解时的两层循环中的内层循环的作用其实就是在寻找外层循环元素的补足;内层循环的时间复杂度是 $O(n)$,如果利用 HashMap 来完成这种查找,那么内层循环的时间复杂度是$O(1)$;

public int[] sum2(int[] nums, int target){
    int length = nums.length;
    Map numsMap = new HashMap<>();
    for (int i = 0; i < length; i++){
        numsMap.put(nums[i], i);
    }
    for (int i = 0; i< length; i++){
        int complement = target - nums[i];
        if (numsMap.containsKey(complement) && numsMap.get(complement) != i){
            return new int[]{i, numsMap.get(complement)};
        }
    }
    return null;
}

​ 在执行遍历之前,先将所有数组元素置入 HashMap,其中键为数组中元素,值为其索引;

​ 对数组元素进行遍历,每次遍历中计算当前元素的补足值,利用 HashMap 的 containsKey 方法查询是否存在,若存在则返回;

解法三

​ 在解法二中,利用 HashMap 实现了将时间复杂度降到了$O(n)$,将两层循环改成了两次循环;在第一次循环完成了 HashMap 的填充,第二次循环实现计算与查找;仔细分析会发现解法一与解法二查找补足值的范围是不同的,解法一只在当前元素位置之后的元素中查找,而解法二中因为事先已经完成了 HashMap 的填充,所以其查找范围是整个数组,这也是为什么还需要做“ numsMap.get(complement) != i ”的判断;

​ 解法二中的查找范围是浪费的,但是因为我们采用的是 HashMap,其查找方式不是遍历的,所以查找范围并不影响性能;但是第一次循环完成 HashMap 的填充是浪费性能的,既然查找补足值的范围不需要全数组,那么可以进一步优化;

public int[] sum2(int[] nums, int target){
    int length = nums.length;
    Map numsMap = new HashMap<>();
    for (int i = 0; i < length; i++) {
        int complement = target - nums[i];
        if (numsMap.containsKey(complement)){
            return new int[]{i, numsMap.get(complement)};
        }
        numsMap.put(nums[i], i);
    }
    return null;
}

​ 去掉了第一次循环完成了 HashMap 的填充的步骤,边遍历边填充;

3-SUM 问题

Question

​ Given an array nums of n integers, are there elements a, b, c in nums such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.The solution set must not contain duplicate triplets.

​ 根据给定的容量为 n 的整数数组,找到所有满足 a + b + c = 0 的三个元素a、b 、c组合,需去重;

解法

​ 修改问题为:满足 a + b + c = target,target是给定数,原题即 target = 0;

​ 根据题目可知,与 2-SUM 问题类似,在整数数组中不放回的取出三个元素,其和等于给定数(0),不同的是,满足条件的解有多个而且需要去重;

​ 首先想到的解法是,遍历数组,然后调用 2-SUM 问题中的方法寻找两个元素相加等于当前元素的补足,最后执行去重操作;这样的话,查询的时间复杂度是$O(n^2)$,空间复杂度是$O(n^2)$,去重的时间复杂度是$O(n^2)$,空间复杂度是$O(1)$,这绝对不能算一个好方案;

​ 思考其他思路,既然要去重,可以先对数组执行一次排序,这样的话在遍历的时候可以跳过相同元素;在确定一个当前元素后,找另外两个元素相加作为当前元素的补足,此时的解可能是多个的,采用首尾标记的方式可以一次遍历完成查找;

public List> threeSum(int[] nums, int target){
    int length = nums.length;
    List> result = new ArrayList<>();
    Arrays.sort(nums);  
    for(int i = 0; i < length - 2; i++) {
        if(nums[i] + nums[i+1] + nums[i+2] > target)break; // too large
        if(nums[i] + nums[length-1] + nums[length-2] < target)continue; // too small
        if(i > 0 && nums[i] == nums[i - 1]) continue;
        int left = i + 1;
        int right = length - 1;
        while(left < right){
            int diff = target - nums[i] - nums[left] - nums[right];
            if (diff == 0){
                result.add(new ArrayList(Arrays.asList(nums[i], nums[left], nums[right])));
                while(left < right && nums[left] == nums[left + 1]) left++;
                while(left < right && nums[right] == nums[right - 1]) right--;
                left++;
                right--;
            }else if (diff > 0){
                left++;
            }else {
                right--;
            }
        }
    }
    return result;
}
  1. 首先执行 Arrays.sort(nums) 对数组进行一次排序,此处仍有优化空间,但针对的是排序,所以不考虑优化;
  2. 对数组中的元素进行循环,若当前元素与其相邻的后两个元素相加仍大于给定数则结束,若当前元素与末尾两个元素相加仍小于给定数则跳过,若当前元素与上一元素相同则跳过,终点是 length - 2 或 nums[i] > 0,因为总是在当前元素之后选择其补足,而且明显可知解中至少有一个元素小于等于0,既然排序了就要充分利用排序带来的便利;
  3. 标记首尾,首是当前元素之后的第一个元素,尾是数组最后一个元素;
  4. 计算当前元素与首尾两个元素的和与给定数的差值,若差值为 0,则满足解条件,将其加入解集,并且移动首尾标记跳过相同元素以避免重复解;若差值大于 0,则说明三个元素之和小于给定数,右移首标记,使得三个元素之和增大;若差值小于 0,则同理左移尾标记;直至首尾标记重合或交错;

3-SUM Closest 问题

Question

​ Given an array nums of n integers and an integer target, find three integers in nums such that the sum is closest to target. Return the sum of the three integers. You may assume that each input would have exactly one solution.

​ 根据给定的长度为 n 的整数数组,找到三个整数的和最接近给定数,返回三个数的和;假定有且只有一个解;

解法

​ 这个题目是 3-SUM 问题的一个变种,整体思想并没有什么变化,简单改动就可以实现了

public int threeSumCloesest(int[] nums, int target) {
    int length = nums.length;
    int result = Integer.MAX_VALUE;
    Arrays.sort(nums);  
    for(int i = 0; i < length - 2; i++) {
        if(i > 0 && nums[i] == nums[i - 1]) continue;
        int left = i + 1;
        int right = length - 1;
        while(left < right){
            int diff = target - nums[i] - nums[left] - nums[right];
            if (diff == 0){
                return target;
            }
            if (Math.abs(diff) < Math.abs(result)){
                result = diff;
            }
            if (diff > 0){
                left++;
            }else {
                right--;
            }
        }
    }
    return target - result;
}

4-SUM 问题

Question

​ Given an array nums of n integers and an integer target, are there elements a, b, c, and d in nums such that a + b + c + d = target? Find all unique quadruplets in the array which gives the sum of target.

​ 根据给定的容量为 n 的整数数组,找到所有满足 a + b + c + d = 0 的三个元素 abcd组合,需去重;

解法

​ 4-SUM 问题与 3-SUM 问题非常类似,其解法也如出一辙,基本思路是,遍历数组,选取当前元素,然后调用 3-SUM 问题的解法,在当前元素之后的元素范围内找到 3-SUM 问题的解即可;

public List> threeSum(int[] nums, int target, int from){
    int length = nums.length;
    List> result = new ArrayList<>();
    for(int i = from + 1; i < length - 2; i++) {
        if(nums[i] + nums[i+1] + nums[i+2] > target)break;
        if(nums[i] + nums[length-1] + nums[length-2] < target)continue;
        if(i > from + 1 && nums[i] == nums[i - 1]) continue;
        int left = i + 1;
        int right = length - 1;
        while(left < right){
            int diff = target - nums[i] - nums[left] - nums[right];
            if (diff == 0){
                result.add(new ArrayList(Arrays.asList(nums[from], nums[i], nums[left], nums[right])));
                while(left < right && nums[left] == nums[left + 1]) left++;
                while(left < right && nums[right] == nums[right - 1]) right--;
                left++;
                right--;
            }else if (diff > 0){
                left++;
            }else {
                right--;
            }
        }
    }
    return result;
}

public List> fourSum(int[] nums, int target){
    int length = nums.length;
    List> result = new ArrayList<>();
    Arrays.sort(nums);
    for(int i = 0; i < length - 3; i++) {
        if(nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target)break; // too large
        if(nums[i] + nums[length-1] + nums[length-2] + nums[length-3] < target)continue; // too small
        if(i > 0 && nums[i] == nums[i - 1]) continue;
        int complement = target - nums[i];
        result.addAll(threeSum(nums, complement, i));
    }
    return result;
}

首先对数组进行排序,进行第一层数组遍历,类似 3-SUM问题 中,判断 too large 与 too small 并跳过重复元素;调用 threeSum 函数获取 3-SUM问题的解;

K-SUM 问题

Question

​ 在解决之前的 2-SUM、3-SUM、4-SUM之后,延伸一个问题,如果是一个 极大数-SUM 的问题应该如何解;

解法

​ 根据之前的思路,我们往往是固定一个数,然后计算小范围解,例如 4-SUM 问题中,我们遍历的固定每个元素,然后在其之后的元素范围内解 3-SUM 问题;也就是说对于 极大数-SUM 的问题,可以不停的缩小问题,直至缩小至 2-SUM 问题;

​ 这样的话,可以使用递归的方式来解决问题,那么现在需要抽象公共问题,以及公共问题的最小解;

  1. 公共问题抽象

    公共问题是,在数组中找到 k 个元素相加的和为 target,其中 k、target 均是变量;另外还需要两个变量来表示解的找寻范围;此外我们使用List来保存已经被固定的元素,使用 List 来保存解;

    那么,公共问题对应的函数就可以创建了

    public void kSum(int[] nums, int k, int target, int from, int end, List cur, List> result){
    ···
    }
  2. 公共问题的最小解

    根据之前解决 3-SUM、4-SUM 问题的经验,可以知道 2-SUM 问题是最小解,也就是说当 k == 2 时问题达到最小解

    public void kSum(int[] nums, int k, int target, int from, int end, List cur, List> result){
    if(k == 2) {
        int left = from;
        int right = end;
        while(left < right){
            int diff = target- nums[left] - nums[right];
            if (diff == 0){
                List r = new ArrayList<>();
                r.add(nums[left]);
                r.add(nums[right]);
                r.addAll(cur);
                result.add(r);
                while(left < right && nums[left] == nums[left + 1]) left++;
                while(left < right && nums[right] == nums[right - 1]) right--;
                left++;
                right--;
            }else if (diff > 0){
                left++;
            }else {
                right--;
            }
        }
    }
    }

    可以看到,当 k == 2 时,可以计算出当前已固定的元素在剩余范围内是否存在解,如果存在则 2-SUM 问题的解加上已固定的元素为 K-SUM 问题的解,将其添加进入 result;

  3. 公共问题的缩小

    当 k > 2 时,问题都可以由 K-SUM 问题降至 (K-1)-SUM 问题

    public void kSum(int[] nums, int k, int target, int from, int end, List cur, List> result){
        if(k == 2) {
            ···
        } else {
            for(int i = from; i < end - k + 2; i++){
                if(i > from && nums[i] == nums[i - 1]) continue;
                cur.add(nums[i]);
                kSum(nums, k - 1, target - nums[i], i + 1, end, cur, result);
                cur.remove(cur.size() - 1);
            }
        }
    }

    若 k > 2,遍历 from - end 中的元素,固定元素将其添加至 cur 中,并修改 k 为 k-1,修改解范围为当前位置加一至末尾;

​ 至此,K-SUM 问题就解决了,但是还有优化的空间,和之前解决 4-SUM 问题一样,可以添加固定值的极大值与极小值用于判断是否有必要缩小问题,如果极大值小于 target 或极小值大于 target 就可以直接知道无解可以直接返回;

​ 以下就是完整代码

public void kSum(int[] nums, int k, int target, int from, int end, List cur, List> result){
        if(k == 2) {
            int left = from;
            int right = end;
            while(left < right){
                int diff = target- nums[left] - nums[right];
                if (diff == 0){
                    List r = new ArrayList<>();
                    r.add(nums[left]);
                    r.add(nums[right]);
                    r.addAll(cur);
                    result.add(r);
                    while(left < right && nums[left] == nums[left + 1]) left++;
                    while(left < right && nums[right] == nums[right - 1]) right--;
                    left++;
                    right--;
                }else if (diff > 0){
                    left++;
                }else {
                    right--;
                }
            }
        } else {
            for(int i = from; i < end - k + 2; i++){
                int temp = k;
                int large = 0;
                int small = 0;
                while (temp > 0){
                    large += nums[end - temp + 1];
                    small += nums[from + temp - 1];
                    temp--;
                }
                if(small > target) return;
                if(large < target) return;
                if(i > from && nums[i] == nums[i - 1]) continue;
                cur.add(nums[i]);
                kSum(nums, k - 1, target - nums[i], i + 1, end, cur, result);
                cur.remove(cur.size() - 1);
            }
        }
    }

    public List> sum(int[] nums, int target, int k){
        int length = nums.length;
        List> result = new ArrayList<>();
        List cur = new ArrayList<>();
        Arrays.sort(nums);
        if (k < 2) return;
        kSum(nums, k, target, 0, length - 1, cur, result);
        return result;
    }

你可能感兴趣的:(算法)