https://oj.leetcode.com/problems/4sum/
Given an array S of n integers, are there elements a, b, c, and d in S such that a + b + c + d = target? Find all unique quadruplets in the array which gives the sum of target.
Note:
For example, given array S = {1 0 -1 0 -2 2}, and target = 0. A solution set is: (-1, 0, 0, 1) (-2, -1, 1, 2) (-2, 0, 0, 2)
解题思路:
根据上面3Sum的解法,很容易解决这题。取第i个数,于是剩下的问题就变成了在[i + 1, length -1]这个区间内的3sum问题。代码如下。
public class Solution { public List<List<Integer>> fourSum(int[] num, int target) { List<List<Integer>> quadruplets = new ArrayList<List<Integer>>(); if(num.length < 4){ return quadruplets; } Arrays.sort(num); for(int i = 0; i < num.length - 3; i++){ if(i > 0 && num[i] == num[i - 1]){ continue; } for(int j = i + 1; j < num.length - 2; j++){ if(j > i + 1 && num[j] == num[j - 1]){ continue; } int start = j + 1; int end = num.length - 1; while(start < end){ if(start > j + 1 && num[start] == num[start - 1]){ start++; continue; } if(end < num.length - 1 && num[end] == num[end + 1]){ end--; continue; } int sum = num[i] + num[j] + num[start] + num[end]; if(sum == target){ List list = new ArrayList(); list.add(num[i]); list.add(num[j]); list.add(num[start]); list.add(num[end]); quadruplets.add(list); start++; end--; }else if(sum < target){ start++; }else if(sum > target){ end--; } } } } return quadruplets; } }
这么做的时间复杂度为O(n^3)。网上有人宣称有O(n^2)的解法,于是开始尝试写。
大概意思就是,首先对原数组中任意两两求和,记录这每个pair的和以及坐标情况。一个二维数组用来记录pair的和,一个hashmap,key为pair的和,value是一个list,list内的成员是一个个的数组,数组的length为2,就是和为这个key的两两下标。这个sumMap的泛型格式为Map<Integer, List<int[]>>。于是需要O(n^2)的时间。
首先想到遍历这个hashmap,对于一个key,也就是存在的一种可能的和,看看target-sum,在不在sumMap中,不在当然放弃。在的话,就取出value,分析它的pair情况,并且加入到结果的list里。
这么做其实是把这个4Sum的问题,化解为两个2Sum问题。由于2Sum问题是可以在O(n)时间内求解的,4Sum问题可以在O(n^2)的时间内求解。
但是,这时会遇到一个很难解决的问题,无法判断坐标是否重复。极端情况下,如果num数组的所有元素都相同,他在sumMap里其实是只有一个key,而value的组合却有n^2种之多。因为是4sum,要判断这所有N^2个pair对的index组合在已有答案中不存在,光这个问题的时间复杂度就要达到O(n^4)。
有人说,不应考虑index是否重复,而是实际的num[i]是否重复,因为题目要求的是解唯一。比如,对于{0,0,0,0,0,0,0}这样的数组。这样看似可以无需用上面O(n^4)的时间去比较,但也必须声明一种新的类型后来又想到,这里可以将这个结果定义为另一种类,比如quadruplet,并且重写他的equals()方法(重写equals()方法就必须重写hashCode()方法),用来判断他们实际上是否相等。至少,要生成一个triplet的类,因为四个数字重复和三个数字重复实际上是一样的,总和一定,剩下一个必然相同。
为什么要定义一个新的类型,因为传统的set只适合一个int的去重,这里除非范围缩小到两个数字的去重,否则无法实现。这个解法比较繁琐,而且花费另外的内存,这里就没有再去实现。下面是一个尝试的代码。
事实上,这么做的逼近解,我感觉只能有O(n^2*2logn),当然比O(n^3)有改进。因为问题化简为两个2sum的问题后,问题就是对pair求2sum。因为原来2sum的解法是,首先花费nlogn的时间排序,然后用hashmap在O(1)的时间内求sum。那么这里,n=n^2。于是时间复杂度就是O(n^2*2logn)。
于是,和上面一样,你花费O(n^2)的时间,获得所有两两可能的pair,然后将它们放入一个list中,对其进行排序。这里就需要为这个pair类,实现Comparable接口,重写compareTo()方法。以及,equals()和hashCode()方法。
public class Solution { public List<List<Integer>> fourSum(int[] num, int target) { List<List<Integer>> quadruplets = new ArrayList<List<Integer>>(); if(num.length < 4){ return quadruplets; } //排序,便于后面从小到大排序 Arrays.sort(num); int[][] sumArray = new int[num.length][num.length]; Map<Integer, List<int[]>> sumMap = new HashMap<Integer, List<int[]>>(); for(int i = 0; i < num.length; i++){ for(int j = 0; j < i; j++){ sumArray[i][j] = num[i] + num[j]; List<int[]> list; if(sumMap.get(num[i] + num[j]) == null){ list = new ArrayList<int[]>(); }else{ list = sumMap.get(num[i] + num[j]); } list.add(new int[]{i, j}); sumMap.put(num[i] + num[j], list); } } Iterator iter = sumMap.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = (Map.Entry) iter.next(); Integer sum = entry.getKey(); List<int[]> pairList = entry.getValue(); int remaining = target - sum; if(sumMap.get(remaining) == null){ continue; }else{ List<int[]> list1 = sumMap.get(remaining); for(int[] pair : list1){ if(pair[0] != i && pair[0] != j && pair[1] != i && pair[1] != j){ List list = new ArrayList(); list.add(num[i]); list.add(num[j]); list.add(num[pair[0]]); list.add(num[pair[1]]); quadruplets.add(list); } } sumMap.remove(remaining); } } // for(int i = 0; i < num.length; i++){ // for(int j = 0; j < i; j++){ // int remaining = target - sumArray[i][j]; // if(sumMap.get(remaining) == null){ // continue; // }else{ // List<int[]> list1 = sumMap.get(remaining); // for(int[] pair : list1){ // if(pair[0] != i && pair[0] != j && pair[1] != i && pair[1] != j){ // List list = new ArrayList(); // list.add(num[i]); // list.add(num[j]); // list.add(num[pair[0]]); // list.add(num[pair[1]]); // quadruplets.add(list); // } // } // sumMap.remove(remaining); // } // } // } return quadruplets; } }
辗转反侧,参考了网上的考虑思路,写出了一个近似于O(n^2)的解。思路和上面类似,还是首先花费O(n^2)的时间,生成一个个pair对,和他们的和,以及这样的一个sumMap。区别就是,下面不遍历这个sumMap,去对每一个sum,看看另一个被加数是否存在。而是,首先仍然双重遍历num数组,先确定两个元素,剩下的和,不用最上面的夹逼方法,而是从sumMap中去找,这样只需花费O(1)的时间就可以了。但是!判断这个剩下的pair是否重复,还是依赖于这个sumMap的这个key下的value,也就是list的长度的。也就是下面代码中的 for(int[] pair : list1)。
考虑大多数情况,这个list的长度并不会达到n,所以最差情况才会达到O(n^3)的复杂度,比较理想的这里应该还是仅仅花费O(1)而已。
另一个需要主要的一点,后面的双重循环,内层j是从i+1开始,就是代表和是取得sum[0][1]这样的形式。那么前面的sumArray就一定要取在坐下角,也就是前面的j也要从i+1开始,否则不对应的话,sumArray都没有初始化过,就都是0了。
如果前面的sumArray初始化了右上角,后面的循环也只能在右上角里。
前提是,你要理解,sumArray[i][j] == sumArray[j][i],所以仅仅要取以对角线划分的左下角或右上角即可。
public class Solution { public List<List<Integer>> fourSum(int[] num, int target) { List<List<Integer>> quadruplets = new ArrayList<List<Integer>>(); if(num.length < 4){ return quadruplets; } //排序,便于后面从小到大排序 Arrays.sort(num); int[][] sumArray = new int[num.length][num.length]; Map<Integer, List<int[]>> sumMap = new HashMap<Integer, List<int[]>>(); Set<Integer> set = new HashSet<Integer>(); //首先生成一个二维数组sumArray,包num数组每两位的和 //同时生成一个sumMap,key是每两位的和,value是每对的index for(int i = 0; i < num.length; i++){ for(int j = i + 1; j < num.length; j++){ sumArray[i][j] = num[i] + num[j]; List<int[]> list; if(sumMap.get(num[i] + num[j]) == null){ list = new ArrayList<int[]>(); }else{ list = sumMap.get(num[i] + num[j]); } list.add(new int[]{i, j}); sumMap.put(num[i] + num[j], list); } } for(int i = 0; i < num.length - 3; i++){ //防止重复 if(i > 0 && num[i] == num[i - 1]){ continue; } for(int j = i + 1; j < num.length - 2; j++){ //防止重复 if(j > i + 1 && num[j] == num[j - 1]){ continue; } int remaining = target - sumArray[i][j]; if(sumMap.get(remaining) == null){ continue; }else{ //如果有和为期望值的pair,遍历这些所有的pair List<int[]> list1 = sumMap.get(remaining); //这个set很重要,用来判断这个remaining值下的pair是否重复 set.clear(); for(int[] pair : list1){ //他们的index必须大于前两个数字的index,并且第一个index没有在set里出现过 if(pair[0] > i && pair[0] > j && pair[1] > i && pair[1] > j && !set.contains(num[pair[0]])){ set.add(num[pair[0]]); List list = new ArrayList(); list.add(num[i]); list.add(num[j]); //从小到大排列 if(pair[0] <= pair[1]){ list.add(num[pair[0]]); list.add(num[pair[1]]); }else { list.add(num[pair[1]]); list.add(num[pair[0]]); } quadruplets.add(list); } } // sumMap.remove(remaining); } } } return quadruplets; } }
我到最后也没找到严格意义上的O(n^2)解法,主要是去重。有人坚持说有,参见下面几个链接吧。
https://oj.leetcode.com/discuss/21401/is-there-really-a-o-n-2-solution
https://oj.leetcode.com/discuss/18953/share-my-ac-o-n2-python-solution
http://cs.stackexchange.com/questions/2973/generalised-3sum-k-sum-problem
http://tech-wonderland.net/blog/summary-of-ksum-problems.html
http://blog.csdn.net/linhuanmars/article/details/24826871