当前位置 博文首页 > 4Sum_让代码改变世界:LeetCode 18

    4Sum_让代码改变世界:LeetCode 18

    作者:[db:作者] 时间:2021-07-10 18:56

    说明一下,这道题是后来补的,因为后面两道比较顺手,所以先写的后面两道,不过大家也不用担心,并不是说这一道很难还是怎么着,算是有点儿深度的第一题吧。
    现在是农历八月十四晚上七点多,没错,就是中秋节前夜,身边的朋友大多回家或出去玩儿了,实验室就剩我自己这个孤家寡人了,想想也是有点儿凄凉,哎,这就是命运啊!不过还好可以在这里发泄一下心情,希望自己的坚持有一天能有所收获吧,不说没用的了,来看看题目吧

    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:

    • Elements in a quadruplet (a,b,c,d) must be in non-descending order. (ie,?a?≤?b?≤?c?≤?d)
    • The solution set must not contain duplicate quadruplets.

        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和3sum closest或者更前面那个2sun的题,那你和这个题算是老朋友了,在这里我也建议大家去看一下前面几道题,可以对这种问题有个全面的把握。鉴于前面的文章已经说得很清楚了,这里就不过多解释题目了。
    最直接的思路,也是本人实现的方法,就是把3sum的扩展一下,再加一个指针,前面两个指针做O(n^2)的搜索,后面两个做O(n)的搜索,这样算法的复杂度显然为O(n^3)。不是很理想,但鉴于本题的性质,算法还是通过了leetcode验证,然后通过观察运行时间发现这题目确实水很深,时间分别范围非常广,有最短的12ms,最长的250多ms,而且中间分布的很是平均,几乎每个时间段都有不少人,可见本题的算法及其改进是很多的。
    前面说的代码就不粘出来了,和3sum的几乎相同。在这里我们介绍两种好的算法思路:剪枝策略和2-2sum策略。
    我们先来说一下剪枝策略,这是leetcode一位大神的思路,我们拿来学习一下。
    (感谢 aileengw的无私分享)
    其实说来也简单,剪枝策略就是在前面我们提到的算法上增加一下检查策略以提前结束没有必要的搜索,比如说前面三个数已经大于target了,那显然就没有必要往后搜索了。通过增加很多这样的剪枝策略,我们可以对算法有所改进,那么能改进多少呢?说出来你可能不信,可以改进90%以上!前面没有增加剪枝函数的代码耗时120ms左右,而增加剪枝函数后的代码运行时间为16ms! 当然,这跟测试用的例子有关系,还有一些处理细节的问题,这种改进是不稳定的,而且最坏情况下其时间复杂度仍为O(n^3)。但剪枝策略是我们必须学会的思路,这种策略在一些理想输入或者问题规模很大时对算法的时间有很大的提升。来看一下具体代码吧

    class Solution {
    public:
        vector<vector<int>> fourSum(vector<int>& nums, int target) {
            vector<vector<int>>  result;
            if(nums.size() < 4) return result;
    
    
            vector<int> solution(4,0);
            std::sort(nums.begin(),nums.end());
            int sum,a,b,c,d,Max_d_when_a_increase = nums.size() - 1,Max_d_when_b_increase;
    
    
            //a,b,c,d are the four index
            //Max_d_when_a_increase is the max possible d when a increase. To have the same sum, when a increase, d can only decrease
            //Max_d_when_b_increase is the max possible d when b increase
    
    
            for( a = 0; a < Max_d_when_a_increase - 2;a++ )
            {
                //remove dupilcate & do pruning if a too small or too big
                if((a>0 && nums[a] == nums[a-1])
                || nums[a] + nums[Max_d_when_a_increase] + nums[Max_d_when_a_increase-1] + nums[Max_d_when_a_increase-2] < target) continue;
                if(nums[a]+nums[a+1]+nums[a+2]+nums[a+3] > target) break;            
    
    
                //update Max_d_when_a_increase
                sum = nums[a]+nums[a+1]+nums[a+2];
                while(sum+nums[Max_d_when_a_increase] > target)Max_d_when_a_increase--;
                Max_d_when_b_increase = Max_d_when_a_increase;
    
    
                solution[0] = nums[a];
                for( b=a+1; b < Max_d_when_b_increase - 1;b++)
                {
                    //remove dupilcate & do pruning if b too small or too big
                    if((b>a+1 && nums[b] == nums[b-1])
                    || nums[a] + nums[b] + nums[Max_d_when_b_increase-1] + nums[Max_d_when_b_increase] < target) continue;
                    sum = nums[a] + nums[b]+nums[b+1];
                    if(sum + nums[b+2] > target) break;
    
    
                    //update Max_d_when_b_increase
                    while(sum+nums[Max_d_when_b_increase]>target) Max_d_when_b_increase--;
    
    
                    solution[1] = nums[b];
                    c = b+1;
                    d = Max_d_when_b_increase;
                    sum = nums[a] + nums[b];
                    while(c < d)//this are the same as two sum
                        if(sum + nums[c] + nums[d] == target)
                        {
                            solution[2]=nums[c];
                            solution[3]=nums[d];
                            result.push_back(solution);
    
    
                            do{c++;}while(c < d && nums[c] == nums[c-1]);
                            do{d--;}while(c < d && nums[d] == nums[d+1]);
                        }
                        else if(sum + nums[c] + nums[d] < target) 
                            do{c++;}while(c < d && nums[c] == nums[c-1]);
                        else do{d--;}while(c < d && nums[d] == nums[d+1]);
                }
            }
            return result;
        }
    };

    里面具体用了什么剪枝策略就不说了,大家花三分钟就能读懂。
    再来说一下,2-2sum策略,这种策略是将4sum问题分解,先求出所以数字两两之和,然后存储到一个容器里,再对这个容器进行2sum的搜索。我们来分析一下这种策略的时间复杂度和实现难点,首先计算两两之和的复杂度为O(n^2),这个没有什么难处,就是要多花一些存储空间。然后是2sum搜索之前的排序问题,这个就比较难搞了,如果人工排序的话那么会时间复杂度为O(n^2logn),而且这其中还要记录下标,因为不能重复使用同一个数字组成的和,这样一来的话时间就不容易控制了,要想获得好的效果可能需要一些特殊的数据结构来帮忙了,我们这里就不再探讨了,不过这种分部解决的思路是值得我们学习的,只是本题比较特殊实现起来不太容易。
    总结一下,一方面通过这一题各位最好掌握剪枝和分部解决的思想。再遇到类似的问题可以用得上我们学过的算法思路才是我们真正的目的。另一方面这一题可以推广的nsum问题,算是对这一种题型一个一般性的解决思路总结吧。
    中秋节一个人过,有些凄凉,心情也不太好,写的一般大家凑合看吧,祝大家中秋节快乐,但愿人长久,千里共婵娟。



    cs