LeetCode 2824.统计和小于目标的下标对数目

给你一个下标从 0 开始长度为 n 的整数数组 nums 和一个整数 target ,请你返回满足 0 <= i < j < n 且 nums[i] + nums[j] < target 的下标对 (i, j) 的数目。

示例 1:

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

输出:3

解释:总共有 3 个下标对满足题目描述:

  • (0, 1) ,0 < 1 且 nums[0] + nums[1] = 0 < target
  • (0, 2) ,0 < 2 且 nums[0] + nums[2] = 1 < target
  • (0, 4) ,0 < 4 且 nums[0] + nums[4] = 0 < target
    注意 (0, 3) 不计入答案因为 nums[0] + nums[3] 不是严格小于 target 。
    示例 2:

输入:nums = [-6,2,5,-2,-7,-1,3], target = -2

输出:10

解释:总共有 10 个下标对满足题目描述:

  • (0, 1) ,0 < 1 且 nums[0] + nums[1] = -4 < target
  • (0, 3) ,0 < 3 且 nums[0] + nums[3] = -8 < target
  • (0, 4) ,0 < 4 且 nums[0] + nums[4] = -13 < target
  • (0, 5) ,0 < 5 且 nums[0] + nums[5] = -7 < target
  • (0, 6) ,0 < 6 且 nums[0] + nums[6] = -3 < target
  • (1, 4) ,1 < 4 且 nums[1] + nums[4] = -5 < target
  • (3, 4) ,3 < 4 且 nums[3] + nums[4] = -9 < target
  • (3, 5) ,3 < 5 且 nums[3] + nums[5] = -3 < target
  • (4, 5) ,4 < 5 且 nums[4] + nums[5] = -8 < target
  • (4, 6) ,4 < 6 且 nums[4] + nums[6] = -4 < target

提示:

1 <= nums.length == n <= 50

-50 <= nums[i], target <= 50

法一:直接模拟:

cpp 复制代码
class Solution {
public:
    int countPairs(vector<int>& nums, int target) {
        int sz = nums.size();
        int ans = 0;
        for (int i = 0; i < sz; ++i)
        {
            for (int j = i + 1; j < sz; ++j)
            {
                if (nums[i] + nums[j] < target)
                {
                    ++ans;
                }
            }
        }
        return ans;
    }
};

如果nums中有n个元素,此算法时间复杂度为O(n 2 ^{2} 2),空间复杂度为O(1)。

法二:双指针法,先对nums排序,再用两个指针分别指向nums的首尾,当两指针指向的数字和小于target时,说明两指针中间的所有数字和首指针指向的数字的和都小于target,将小于target的数对的数目加到结果上,此时我们有两个选择,一是自增首指针,二是自减尾指针,由于我们把首尾指针之间所有数字和首指针指向的数字的数对的数目都加上了,相当于自减尾指针的结果也已经计算过了,因此要自增首指针;如果两指针指向的数字和大于等于target,那么首尾指针之间的所有数字和尾指针指向的数字的和也都大于target,因此要自减尾指针:

cpp 复制代码
class Solution {
public:
    int countPairs(vector<int>& nums, int target) {
        int begin = 0;
        int end = nums.size() - 1;
        quickSort(nums, begin, end);

        int res = 0;
        while (begin < end)
        {
            if (nums[begin] + nums[end] < target)
            {
                res += end - begin;
                ++begin;
            }
            else
            {
                --end;
            }
        }

        return res;
    }

private:
    void quickSort(vector<int> &nums, int begin, int end)
    {
        if (begin >= end)
        {
            return;
        }

        int mid = (begin + end) / 2;
        swap(nums[mid], nums[end]);
        int bound = begin - 1;
        for (int i = begin; i <= end - 1; ++i)
        {
            if (nums[i] < nums[end])
            {
                ++bound;
                swap(nums[bound], nums[i]);
            }
        }

        swap(nums[bound + 1], nums[end]);

        quickSort(nums, begin, bound);
        quickSort(nums, bound + 2, end);
    }
};

此算法时间复杂度为O(nlogn),空间复杂度为O(logn)。

法三:二分法,先对nums排序,之后遍历nums中的每个元素,对于每个元素i,二分法找该元素后面的符合题目的最大元素m,m和i之间所有数字都可以与i组成符合题意的数对:

cpp 复制代码
class Solution {
public:
    int countPairs(vector<int>& nums, int target) {
        int begin = 0;
        int end = nums.size() - 1;
        quickSort(nums, begin, end);

        int res = 0;
        for (int i = 0; i <= end; ++i)
        {
            res += binarySearch(nums, i, target - nums[i]) - i;
        }

        return res;
    }

private:
    int binarySearch(vector<int> nums, int begin, int subtraction)
    {
        int end = nums.size() - 1;
        int result = begin;
        while (begin <= end)
        {
            int mid = begin + (end - begin) / 2;
            if (nums[mid] < subtraction)
            {
                result = mid;
                begin = mid + 1;
            }
            else
            {
                end = mid - 1;
            }
        }

        return result;
    }

    void quickSort(vector<int> &nums, int begin, int end)
    {
        if (begin >= end)
        {
            return;
        }

        int mid = (begin + end) / 2;
        swap(nums[mid], nums[end]);
        int bound = begin - 1;
        for (int i = begin; i <= end - 1; ++i)
        {
            if (nums[i] < nums[end])
            {
                ++bound;
                swap(nums[bound], nums[i]);
            }
        }

        swap(nums[bound + 1], nums[end]);

        quickSort(nums, begin, bound);
        quickSort(nums, bound + 2, end);
    }
};

此算法时间复杂度为O(nlogn),空间复杂度为O(logn)。

法四:前缀和,可将nums中的元素作为key,元素出现的数量为value,组合出一个数字出现频率的数组,然后计算该数组的前缀和,此时该前缀和数组中某一元素的含义变为,小于等于该元素的key的数字的个数,然后遍历nums,找出小于目标数字的个数。对于一对符合题意的数对,此方法会计数两次,因此最后要将结果除2:

cpp 复制代码
class Solution {
public:
    int countPairs(vector<int>& nums, int target) {
        vector<int> prefixSum(101);
        for (int num : nums)
        {
            ++prefixSum[num + 50];
        }
        for (int i = 1; i < prefixSum.size(); ++i)
        {
            prefixSum[i] += prefixSum[i - 1];
        }

        int ans = 0;
        for (int num : nums)
        {
            // 如果目标数字小于0,就跳过,因为这时说明要找的数字小于-50,不存在
            if (target - num + 49 < 0)
            {
                continue;
            }
			// 如果目标数字大于100,按最大的数算,因为下标最多只到100
            if (target - num + 49 > 100)
            {
                ans += prefixSum[100];
            }
            else
            {
                ans += prefixSum[target - num + 49];
            }
            // 如果num本身也符合目标数字,则去掉自身
            if (num * 2 < target)
            {
                --ans;
            }
        }
        return ans / 2;
    }
};

如果nums的长度为n,每个元素的范围为m,此方法时间复杂度为O(n+m),空间复杂度为O(m)。

法五:树状数组可以更高效地计算前缀和,树状数组每次更新前缀和的时间复杂度为O(lgn),计算前缀和的时间复杂度也是O(lgn),原理是按二进制的每一位来计算前缀和,比如前11个数字的前缀和,11的二进制为1011,我们就可以把1010-1011区间的前缀和、1000-1010区间的前缀和、0000-1000区间的前缀和加起来,这样最多是对数级别的时间复杂度:

cpp 复制代码
class Solution {
public:
    int countPairs(vector<int>& nums, int target) {
        tree.resize(151);
        int ans = 0;
        for (int num : nums)
        {
            ans += query(target - num + 49);
            add(num + 50, 1);
        }
        return ans;
    }

private:
    vector<int> tree;

    int query(int n)
    {
        ++n;
        int ans = 0;
        for (int i = n; i > 0; i -= lowbit(i))
        {
            ans += tree[i];
        }
        return ans;
    }

    void add(int n, int add)
    {
        ++n;
        for (int i = n; i <= tree.size(); i += lowbit(i))
        {
            tree[i] += add;
        } 
    }

    // 求x二进制表示中,最后一位二进制1表示的值
    int lowbit(int x)
    {
        return x & (-x);
    }
};

如果nums的长度为n,target和nums的范围之和为m,此方法时间复杂度为O(nlgm),空间复杂度为O(m)。

相关推荐
吟安安安安3 分钟前
适合短期冲刺的学习工作流(针对算法)
学习·算法
科研前沿8 分钟前
什么是时空融合技术?
大数据·人工智能·数码相机·算法·重构·空间计算
AI科技星12 分钟前
全域数学本源公理:0、1、∞ 三者核心关系 (典籍定稿版)
人工智能·算法·数学建模·数据挖掘·量子计算
AI科技星17 分钟前
全域数学·第卷:场计算机卷(场空间计算机)【乖乖数学】
java·开发语言·人工智能·算法·机器学习·数学建模·数据挖掘
Deepoch19 分钟前
数学模型驱动:Deepoc 低幻觉数学大模型助力发动机全周期智能优化
人工智能·算法·机器学习·deepoc·数学大模型·低幻觉
嘻嘻哈哈樱桃28 分钟前
牛客经典101题解题集--贪心算法+模拟
java·python·算法·贪心算法
AKDreamer_HeXY29 分钟前
QOJ 12255 - 36 Puzzle 题解
数据结构·c++·数学·算法·icpc·qoj
AI科技星38 分钟前
《全域数学》第一部 数术本源 第三卷 代数原本第14篇 附录二 猜想证明【乖乖数学】
人工智能·算法·数学建模·数据挖掘·量子计算
Wect1 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
憨波个1 小时前
【说话人日志】DOVER-Lap:overlap-aware diarization 输出融合算法
人工智能·深度学习·算法·音频·语音识别