题目:
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)
Answer 1: 通用法 退化到2Sum问题
有篇博客详细描述了kSum问题:Summary for LeetCode 2Sum, 3Sum, 4Sum, K Sum
K Sum总结:
K Sum问题描述:给你一组N个数字(比如 vector<int> num), 然后给你一个常数(比如 int target) ,我们的goal是在这一堆数里面找到K个数字,使得这K个数字的和等于target。
注意事项:
1.去除重复项。
注意这一组数字可能有重复项:比如 1 1 2 3 , 求3sum, 然后 target = 6, 你搜的时候可能会得到 两组1 2 3, 1 2 3,1 来自第一个1或者第二个1, 但是结果其实只有一组,所以最后结果要去重。
2.注意退化过程中,确定前k-2个数字时所取的范围。
比如3Sum问题,第一个数的范围是0~num.size()-2. 因为我们排好序以后, 只需要检测到倒数第三个数字就行了, 因为剩下的只有一种triplet 由最后三个数字组成. 其他k sum问题类似确定范围。
解决方法:先排序后夹逼,将K Sum问题退化为 k-1 Sum问题,最终退化到2Sum问题。然后利用头尾指针找到两个数使得他们的和等于新的target.
给出2 Sum的核心代码:
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">//2 sum int i = starting; //头指针 int j = num.size() - 1; //尾指针 while(i < j) { int sum = num[i] + num[j]; if(sum == target) { store num[i] and num[j] somewhere; if(we need only one such pair of numbers) break; otherwise do ++i, --j; } else if(sum < target) ++i; else --j; }<strong> </strong></span></span></span>2sum的算法复杂度是O(N log N) 因为排序用了N log N以及头尾指针的搜索是线性的,所以总体是O(N log N),好了现在考虑3sum, 有了2sum其实3sum就不难了,这样想:先取出一个数,那么我只要在剩下的数字里面找到两个数字使得他们的和等于(target – 那个取出的数)就可以了吧。所以3sum就退化成了2sum, 取出一个数字,这样的数字有N个,所以3sum的算法复杂度就是O(N^2 ), 注意这里复杂度是N平方,因为你排序只需要排一次,后面的工作都是取出一个数字,然后找剩下的两个数字,找两个数字是2sum用头尾指针线性扫,这里很容易错误的将复杂度算成O(N^2 log N),这个是不对的。我们继续的话4sum也就可以退化成3sum问题(copyright @sigmainfy),那么以此类推,K-sum一步一步退化,最后也就是解决一个2sum的问题,K sum的复杂度是O(n^(K-1))。 这个界好像是最好的界了,也就是K-sum问题最好也就能做到O(n^(K-1))复杂度,之前有看到过有人说可以严格数学证明,这里就不深入研究了。
注意点:注意第一数和第二个数确定的起止范围。
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">class Solution { public: vector<vector<int> > fourSum(vector<int> &num, int target) { vector<vector<int>> ret; if(num.size() < 4) return ret; int n = num.size(); vector<int> ivec(num); sort(ivec.begin(), ivec.end()); for(int i = 0; i < n - 3;i++) //(1) trick 1 确定范围到倒数第四个数 { if(i > 0 && ivec[i] == ivec[i-1]) continue; //防止第一个数字重复 (2)trick 2 避免重复项 for(int j = i+1; j < n - 2;j++) { if(j > i + 1 && ivec[j] == ivec[j-1]) continue; //防止第二个数字重复 int newtarget = target - ivec[i] - ivec[j]; int k = j + 1; int l = n - 1; while(k < l) { if(ivec[k] + ivec[l] == newtarget) { vector<int> tmp{ivec[i], ivec[j], ivec[k], ivec[l]}; ret.push_back(tmp); //trick 3 跳过重复项 do{ k++; }while(k < l && ivec[k] == ivec[k-1]); do{ l--; }while(k < l && ivec[l] == ivec[l+1]); } else if(ivec[k] + ivec[l] > newtarget) { l--; } else { k++; } } } } return ret; } };</span></span></span>
思路:O(n^2)的算法,和前面相当,都是先对数组排序。我们先枚举出所有二个数的和存放在哈希map中,其中map的key对应的是二个数的和,因为多对元素求和可能是相同的值,故哈希map的value是一个链表(下面的代码中用数组代替),链表每个节点存的是这两个数在数组的下标;这个预处理的时间复杂度是O(n^2)。接着和算法1类似,枚举第一个和第二个元素,假设分别为v1,v2, 然后在哈希map中查找和为target-v1-v2的所有二元对(在对应的链表中),查找的时间为O(1),为了保证不重复计算,我们只保留两个数下标都大于V2的二元对(其实我们在前面3sum问题中所求得的三个数在排序后的数组中下标都是递增的),即时是这样也有可能重复:比如排好序后数组为-9 -4 -2 0 2 4 4,target = 0,当第一个和第二个元素分别是-4,-2时,我们要得到和为0-(-2)-(-4) = 6的二元对,这样的二元对有两个,都是(2,4),且他们在数组中的下标都大于-4和-2,如果都加入结果,则(-4,-2,2,4)会出现两次,因此在加入二元对时,要判断是否和已经加入的二元对重复(由于过早二元对之前数组已经排过序,所以两个元素都相同的二元对可以保证在链表中是相邻的,链表不会出现(2,4)->(1,5)->(2,4)的情况,因此只要判断新加入的二元对和上一个加入的二元对是否重复即可),因为同一个链表中的二元对两个元素的和都是相同的,因此只要二元对的一个元素不同,则这个二元对就不同。我们可以认为哈希map中key对应的链表长度为常数,那么算法总的复杂度为O(n^2)
Attention:
1. 想要将有相同和的对,存到链表中,可以构造value为数组的map.
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;"> unordered_map<int, vector<pair<int,int> > > pairs;</span></span></span>2. 初始化map, 存储两两的和。注意j从i+1开始,否则会把自身求和算进去。
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">for(int i = 0; i < n; i++) for(int j = i+1 ; j < n; j++) pairs[num[i]+num[j]].push_back(make_pair(i,j));</span></span></span>3. 去除重复的手段和3Sum类似。
4. 针对pair的问题,我们还需要找到符合下标递增要求,并且不能有重复的数字添加(判断第三位是否一致)。
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;">bool isFstpush = true; //将符合要求的不重复且下标递增的pair存进答案 for(int k = 0; k < sum2.size(); k++) { if(sum2[k].first <= j) continue; //第一次存储的重复数字,或者不是重复数字,判断第三位是否相等 if(isFstpush || (ret.back())[2] != ivec[sum2[k].first]){...}</span></span></span>
5. 注意pair的操作和数组调用原数组数字的使用。
<span style="font-size:14px;"><span style="font-size:14px;"><span style="font-size:14px;"> vector<int> tmp{ivec[i], ivec[j], ivec[sum2[k].first], ivec[sum2[k].second]}; </span></span></span>
6.reserve函数:
给vector预分配存储区大小,即capacity的值 ,但是没有给这段内存进行初始化。reserve 的参数n是推荐预分配内存的大小,实际分配的可能等于或大于这个值,即n大于实际分配空间的值,就会reallocate内存 (实际分配空间的值)capacity的值会大于或者等于n 。这样,当ector调用push_back函数使得size 超过原来的默认分配的capacity值时 避免了内存重分配开销。
需要注意的是:reserve 函数分配出来的内存空间,只是表示vector可以利用这部分内存,但vector不能有效地访问这些内存空间(在创建对象之前,不能引用容器内的元素),访问的时候就会出现越界现象,导致程序崩溃。
<span style="font-size:14px;"><span style="font-size:14px;"> pairs.reserve(n*n);</span></span>AC Code:
<span style="font-size:14px;"><span style="font-size:14px;">class Solution { public: vector<vector<int> > fourSum(vector<int> &num, int target) { int n = num.size(); vector<vector<int> > ret; if(n < 4) return ret; vector<int> ivec(num); sort(ivec.begin(), ivec.end()); unordered_map<int, vector<pair<int,int> > > pairs; pairs.reserve(n*n); //将n个数字的两两组合求和存进map 相同和的对在map中按照链表存储,即存在vector中 j从i+1开始 for(int i = 0; i < n; i++) { for(int j = i+1; j < n; j++) { pairs[ivec[i] + ivec[j]].push_back(make_pair(i,j)); } } for(int i = 0; i < n - 3; i++) { if(i > 0 && ivec[i] == ivec[i-1]) continue; for(int j = i+1; j < n - 2; j++) { if(j > i+1 && ivec[j] == ivec[j-1]) continue; if(pairs.find(target - ivec[i] - ivec[j]) != pairs.end()) { vector<pair<int, int> > sum2 = pairs[target - ivec[i] - ivec[j]]; bool isFstpush = true; //将符合要求的不重复且下标递增的pair存进答案 for(int k = 0; k < sum2.size(); k++) { if(sum2[k].first <= j) continue; //第一次存储的重复数字,或者不是重复数字,判断第三位是否相等 if(isFstpush || (ret.back())[2] != ivec[sum2[k].first]) { vector<int> tmp{ivec[i], ivec[j], ivec[sum2[k].first], ivec[sum2[k].second]}; ret.push_back(tmp); isFstpush = false; } } } } } return ret; } };</span></span>