Leecode K数之和通用算法笔记(以四数之和为例)

Leecode K数之和通用算法笔记(以四数之和为例),第1张

Leecode K数之和通用算法笔记(以四数之和为例)

                                                       K数之和

题目描述:给你任意N个数组成的数组,给定一个目标值target,要求找出K个数,使得这K个数的总和为target,要求输出所有符合该条件的K位数的组合,组合不得重复

(以K=4为例)

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

如题所示,在明知K的情况,我们可以设置K层循环,对每种可能n1...nk,使得n1+..nk==target时保存,但很明显,K不明确的情况下我们没法设置循环,换言之,这种办法太冗余,而且针对题目的数据规模要求,这种暴力枚举必定超时

那么可以借助递归来完成此题,我们知道,解决规模为N的问题,需要借助N-1规模问题的解决方案,那么,假设我们需要四个数相加为target,假定一个值已经确定为i,那么我们只需要解决 三个数相加为target-i的问题,而后者与前者的解决方法是同态的,因此,我们可以编写函数进行递归

对于该问题,我们可以先进行排序,使得查找过程尽可能的短,排序后我们主要可以省去的是:当查找某个target时,使用双指针,分别指向最大最小值(严格来说应该是起始值),当发现和不等于target时,就令一方的指针移动,原理可以参考“前N大的数”,这样可以加快寻找的进度

从简单的方法说起,上述的方法可以直接实现,但重点是如何把“结果”进行储存?,我们应该借助一个vector数组来存放答案,每进入一层前就先放进去,然后进行所谓的递归,无论递归的结果最后找到没有,我们都应该把这一层放进去的东西popback掉(没找到得退出来,找到了也得退出来继续找),当我们不断pushback后,到一个临界条件,发现target此时为0,说明最后一个数已经找到了,我们就把这个vector数组放入一个vecotr>数组里,后者是专门储存答案的

简单版实现代码如下

void ksum(int k, int start, int target, int num[])//K为几位数字 start为开始位置
{                                        
    if (k == 0)
    {
        if (target == 0)
        {
            ans.push_back(tmp);
        }
        return;
    }
    else
    { 
        for (int i = start;i <=len-k;i++)  //如果需要找四位数,那么我们固定一位后,至少在
                                           //剩下的数组中有三位数能够让我们查找
                                      //不然固定一位后,剩下的都不足三位,又怎么能凑齐四位?
                                        //K个数同理。
        {   
            if (i > start && num[i] == num[i - 1])continue;
            tmp.push_back(num[i]);
            ksum(k - 1, i + 1, target - num[i], num);
            tmp.pop_back();
        }
    }


}

注意这里的if (i > start && num[i] == num[i - 1])continue;为什么要这么做,答案是去重,发现网上对该去重原理讲的不是特别深入,在此作详细解释:以 -2 -1 -1 0 1 为例,当我们固定了第一个-1,此时我们需要在-1 0 1中寻找 target-(-1),但当我们固定了第二个-1,此时我们需要在 0 1 中寻找 target -(-1),仔细观察,我们发现,如果0 1 是-1 0 1的子集,寻找全集的过程中其实就已经寻找过子集了!这也就是为什么不加该判断条件会重复的原因!假设我们需要的目标值为1,-1 0 1的答案是 0 1,0 1的答案也是0 1,但这两个搜索的过程固定的是两个不同位置的-1,但我们每搜索一次,就会丢进一个元素,这也就是会重复的来源,讲最明白一点, 子集中能找到,全集上肯定能找得到,但我们必定是越访问越短,全集已经找到了,子集就不必寻找了,重点是因为搜寻值相同。

运行结果如下:(K==4,target=0,num.length=6的情况下为例)

 当然这个简单版只是解决较少数字,放在LeetCode上必定会超时,我们发现,当找到最后一个数的时候,我们可以直接进行判定了,不必再递归一次,因此我们直接写入n==2的情况

进阶版代码如下:

void kksum(int k, int start, int target, int num[])
{   
    if (k > 2)
    {
        for (int i = start;i <= len - k;i++)
        {
            if (i > start && num[i] == num[i - 1])continue;
            else
            {
                tmp.push_back(num[i]);
                kksum(k - 1, i + 1, target - num[i], num);
                tmp.pop_back();
            }
        }
    }
    else if(k==2)
    {
        int left = start,right=len-1;
        while (left < right)
        {
            if (num[left] + num[right] == target)
            {
                tmp.push_back(num[left]);
                tmp.push_back(num[right]);
                ans.push_back(tmp);
                tmp.pop_back();
                tmp.pop_back();
                while (left < right && num[left] == num[left + 1])left++;
                while (left < right && num[right] == num[right - 1])right--;
                        left++;
                        right++;
            }
            else if (num[left] + num[right] > target)
            {
                do
                {
                    right--;
                } while (left 

值得注意的是,当我们的结果与target不同时,指针不必移动后就马上结束,我们判断下,指针移动后指向的值是否相同,如果相同,即便是下一次while判断,结果也是一样的,所以我们要两个do while循环,使得left和right指针不断移动,直到指向的值不等于先前指向的值

其二,我们找到结果后,不只移动一方,如果只移动一方,由于数组有序,两个加数必定有一个数比之前的小或者大,无论怎么单向移都是不符合target的,那么我们同时移动即可,这样就保证一个变大一个变小,当然你单方面移动也可以,多进行一次while而已,有n个答案就要多进行n个while。

原理已经明确,将代码改装进leetcode,通过了,但是效率还是有点低

 当然我们既然已经知道了K=4,可以直接写四层for或者while循环

代码如下图所示:

 vector> fourSum(vector& nums, int target) {

    
    vector>ans;
    if (nums.size() < 4)return ans;
    sort(nums.begin(), nums.end());
    int len = nums.size();
    for (int i = 0;i <= len - 4;i++)
    {   
     
        if (i > 0 && nums[i] == nums[i - 1])continue;
        tmp.push_back(nums[i]);
        for (int j = i + 1;j <= len - 3;j++)
        {    
          
            if (j > i+1 && nums[j] == nums[j - 1])continue;
            tmp.push_back(nums[j]);
            int left = j + 1, right = len - 1;
            while (left < right)
            {
                if (nums[left] + nums[right] == (target-nums[i]-nums[j]))
                {
                    tmp.push_back(nums[left]);
                    tmp.push_back(nums[right]);
                    ans.push_back(tmp);
                    tmp.pop_back();
                    tmp.pop_back();
                    while (left < right && nums[left] == nums[left + 1])left++;
                    while (left < right && nums[right] == nums[right - 1])right--;
                    left++;
                    right--;
                }
                else if (nums[left] + nums[right] > (target - nums[i] - nums[j]))
                {
                    do
                    {
                        right--;
                    } while (left < right && nums[right] == nums[right + 1]);
                }
                else if (nums[left] + nums[right] < (target - nums[i] - nums[j]))
                {
                    do
                    {
                        left++;
                    } while (left < right && nums[left] == nums[left - 1]);
                }
            }
            tmp.pop_back();

        }
        tmp.pop_back();
    }
    return ans;

这样子在空间上能更加开阔一点,无需再一步步进入函数,提交结果如下:

 

优化部分

 

由于数组有序,所以一些不必要的判断和递归可以直接省去,例如本题,我们需要一个固定值,但不是每一个数都可以当这个固定值的,假设这个数是固定值,那么就还要再选三个数,如果我们将这个数和前三位最大的数相加,仍然小于target,那么就可以直接continue到下一个i了,因为连最大的三位都没办法把这个数拉到target,更何况比这些数小的数,相反而言,如果target被平均分成了k份,连这个固定值都大于这个平均值了,更何况后面比固定值大的,加起来肯定也会大于target的因此有以下两个优化

1.当取K个数满足target时,若当前值与前K-1大的数相加仍然小于target,直接continue开启下一轮

2.当当前值小于target的平均K值时,说明该数往后相加必然大于target,直接break掉此层循环。

代码部分略

优化后的结果:(递归版)

(for循环版)

 

 for循环进行优化后就是这个解法下几乎最优的版本了,当然还可以做的更细致一点,对异常行为直接return,能够提高程序运行效率

希望这篇文章能帮助到您,文章中有不妥当、错误的地方,还请您不吝赐教!

欢迎分享,转载请注明来源:内存溢出

原文地址:https://www.54852.com/zaji/5714752.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-12-17
下一篇2022-12-17

发表评论

登录后才能评论

评论列表(0条)

    保存