算法:采购方案(两数之和变化版)【双指针、二分法、计数法】

本文介绍了力扣杯竞赛中的一道算法题,题目要求在给定预算内找到购买两个零件的所有方案。文章详细解析了双指针法的三种实现:基本双指针、双指针配合二分查找以及计数法,并分析了它们的时间复杂度和空间复杂度。通过实例展示了如何优化算法,提高解题效率。
摘要由CSDN通过智能技术生成

本题来自力扣杯竞赛真题-2021春赛第一题: LCP 28.采购方案,难度为简单,考察双指针


题目

小力将 N 个零件的报价存于数组 nums。小力预算为 target,假定小力仅购买两个零件,要求购买零件的花费不超过预算,请问他有多少种采购方案。

注意:答案需要以 1e9 + 7 (1000000007) 为底取模,如:计算初始结果为:1000000008,请返回 1

示例

示例1:

输入:nums = [2,5,3,5], target = 6
输出:1
解释:预算内仅能购买 nums[0] 与 nums[2]。

示例2:

输入:nums = [2,2,1,9], target = 10
输出:4
解释:符合预算的采购方案如下:
nums[0] + nums[1] = 4
nums[0] + nums[2] = 3
nums[1] + nums[2] = 3
nums[2] + nums[3] = 10


提示

  • 2 <= nums.length <= 10^5
  • 1 <= nums[i], target <= 10^5

题解

这道题和两数之和很像,只不过 = 的条件换成了 <=
对于这种无序数组求元素组合比大小的问题,上来肯定就是咔咔一顿排序:)。

1.双指针

  双指针是最容易想到的解法。
  这是因为可以找到一个很简单的规律:nums 数组排序后,对于某个数 nums[i],如果存在一个数 nums[j] 使得 j > i 的同时满足 nums[i] + nums[j] < target,则 ij 之间的任意索引 k 都满足 nums[i] + nums[k] < target
  与此同时,对于 nums[i + 1],想要找到对应的右区间,该索引一定在 j 的左边。
  因此,解法可以抽象理解为,一个初始为 0 ~ nums.length 的窗口通过不断缩小,统计对数,直到左右相逢。

class Solution {
    public int purchasePlans(int[] nums, int target) {
        long ans = 0;
        long mod = 1000000007;
        Arrays.sort(nums);
        for (int left = 0, right = nums.length - 1; left < right; left++) {
            while (left < right && nums[left] + nums[right] > target) right--;
            if (left < right) ans = (ans + right - left) % mod;
        }
        return (int)ans;
    }
}
结果
  • 执行用时:32 ms
  • 内存消耗:48.2 MB
复杂度分析
  • 时间复杂度:O(n2)(最坏情况下)
  • 空间复杂度:O(1)

2.双指针+二分法

  从上面的双指针可得知,内循环每次 right 移动都是一个数一个数挪,在数据量很大的情况下并不合适,可以采用二分来快速定位到极限值

(1)憨憨二分:找恰好满足小于 target - nums[i] 的最后一个值

  对于有序且存在重复元素的二分查找,往往需要多一个“是否是满足条件的重复的元素的第一个或最后一个”的判断。
  由于一开始就这么想,结果写了个憨憨二分,效率明显不佳。

class Solution {
    public int purchasePlans(int[] nums, int target) {
        long ans = 0;
        long mod = 1000000007;
        Arrays.sort(nums);
        for (int left = 0, right = nums.length - 1; left < right; left++) {
            int l = left + 1, r = right;
            int temp = target - nums[left];
            if (temp < nums[l]) break;
            while (l <= r) {
                int mid = (l + r) >> 1;
                if (nums[mid] <= temp && (mid == nums.length - 1 || nums[mid + 1] > temp)) {
                    right = mid;
                    break;
                } else if (nums[mid] > temp) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            }
            if (left < right) ans = (ans + right - left) % mod;
        }
        return (int)ans;
    }
}
结果
  • 执行用时:38 ms
  • 内存消耗:48.3 MB
复杂度分析
  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(1)
(2)优化二分:找恰好满足大于 target - nums[i] 的第一个值

  在执行一个非常非常长的测试用例时,发现耗时明显优于前面两种。
  虽然但是,可能是测试用例组的原因,这种用例较少,不能体现出这里二分法对双指针的性能提升。

class Solution {
    public int purchasePlans(int[] nums, int target) {
        long ans = 0;
        long mod = 1000000007;
        Arrays.sort(nums);
        for (int left = 0, right = nums.length - 1; left < right; left++) {
            int l = left + 1, r = right;
            int temp = target - nums[left];
            if (temp < nums[l]) break;
            while (l <= r) {
                int mid = (l + r) >> 1;
                if (nums[mid] > temp) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            }
            right = r;
            if (left < right) ans = (ans + right - left) % mod;
        }
        return (int)ans;
    }
}
结果
  • 执行用时:35 ms
  • 内存消耗:48.1 MB
复杂度分析
  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(1)

3.计数法

  其实本人第一个写的解法就是计数法。别问,问就是题目条件的 1 ~ 10^5 实在是太靓了,让我情不自禁蠢蠢欲动~
  咳咳,事实上,对于这种题,用计数法有点白痴,一是效果不佳,二是确实没必要哈哈,但还是顺便讲一下。
  这种解法的思想来源于计数排序,经典思路是使用一个 count 数组来统计以 nums[i] 作为索引的匹配值,以达到空间换时间使时间复杂度降到O(N)。他唯一的条件就是必须满足 nums 的值的取值范围是固定的,而且最好范围不要太大(像 10^5 其实已经算很大了)。
  在本题中,count 用于记录 0 ~ 100001 的命中次数,除此之外还涉及前缀和的思想,使 count[i] 包含 count[i - 1] 的值:
  对于 count 的每个索引 i

  • 如果 inums 中,count[i] = count[i - 1] + nums[i]命中次数
  • 如果 i 不在 nums 中,count[i] = count[i - 1]

  因此,从左边开始,对于每个 nums[i],都可以在 count 中找到 target - nums[i] 的前缀和即匹配的个数,结合从左往右遍历会有重复的配对,需要去重,只需要减去 count[num] 再减自身的 1 即可。
  然而,由于存在重复的 nums[i],采用这种解法将无法识别。因此还需要另一个额外的一维空间 repeat 来记录重复次数,同时将上面 “减去 count[num] ”改为“减去 count[num - 1] ”,便能解决这个问题。

class Solution {
    public int purchasePlans(int[] nums, int target) {
        long ans = 0;
        long mod = 1000000007;
        Arrays.sort(nums);
        long[] count = new long[100001];
        int[] repeat = new int[100001];
        for (int num : nums) {
            count[num]++;
        }
        for (int i = 1; i < 100001; i++) {
            count[i] += count[i - 1];
        }
        for (int num : nums) {
            if (num * 2 > target) break;
            ans = (ans + count[target - num] - count[num - 1] - repeat[num] - 1) % mod;
            repeat[num]++;
        }
        return (int)ans;
    }
}
结果
  • 执行用时:45 ms
  • 内存消耗:47.4 MB
复杂度分析
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

总结

虽然是道简单题,但既然出现在力扣杯春季赛里,说明有其独特之处,可以发现有很多解题的角度不妨试一试,练习一下算法基本功。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值