luogu-P1102 A-B 数对

luogu-P1102 A-B 数对

GESP C++ 五级练习题,二分查找考点应用,重点理解二分查找法和双指针法的应用。四-五级考生可以练习。题目难度⭐⭐☆☆☆,洛谷难度等级普及−

luogu-P1102 A-B 数对

题目要求

题目背景

出题是一件痛苦的事情!

相同的题目看多了也会有审美疲劳,于是我舍弃了大家所熟悉的 A+B Problem,改用 A-B 了哈哈!

题目描述

给出一串正整数数列以及一个正整数 CC,要求计算出所有满足 AB=CA - B = C 的数对的个数(不同位置的数字一样的数对算不同的数对)。

输入格式

输入共两行。

第一行,两个正整数 N,CN,C

第二行,NN 个正整数,作为要求处理的那串数。

输出格式

一行,表示该串正整数中包含的满足 AB=CA - B = C 的数对的个数。

输入输出样例 #1

输入 #1
4 1
1 1 2 3
输出 #1
3

说明/提示

对于 75%75\% 的数据,1N20001 \leq N \leq 2000

对于 100%100\% 的数据,1N2×1051 \leq N \leq 2 \times 10^50ai<2300 \leq a_i <2^{30}1C<2301 \leq C < 2^{30}


题目分析

题目本质是要求对于每一个数 BB,找到数组中有多少个 AA 满足 A=B+CA = B + C

如果直接使用两层循环枚举 AABB,在大数据量下(N2×105N \leq 2 \times 10^5)会超时(O(N2)O(N^2))。因此我们需要更高效的方法。 首先,将数组进行排序是所有高效解法的基础,这样可以将无序的查找转化为有序查找。

方法一:二分查找

思路:

  1. 排序数组。
  2. 遍历数组中的每一个数 a[i]a[i] 作为 BB
  3. 计算出目标值 target=a[i]+Ctarget = a[i] + C
  4. 在数组中通过二分查找找到等于 targettarget 的起始位置和结束位置。
    • std::lower_bound:找到第一个 target\ge target 的位置。
    • std::upper_bound:找到第一个 >target> target 的位置。
    • 两个迭代器/指针相减,即为等于 targettarget 的元素个数。
  5. 累加所有个数即为答案。 时间复杂度:排序 O(NlogN)O(N \log N),每次查找 O(logN)O(\log N),共 NN 次,总复杂度 O(NlogN)O(N \log N)

方法二:双指针法

思路:

  1. 排序数组。
  2. 定义两个指针 LLRR,初始都指向数组开头。
  3. 遍历数组中的每一个数 a[i]a[i] 作为 BB,对应的目标是 target=a[i]+Ctarget = a[i] + C
  4. 利用数组的单调性:随着 a[i]a[i] 增大,targettarget 也随之增大(或不变)。因此 LLRR 只需要向右移动,不需要回退。
    • 移动 LL 直到 a[L]targeta[L] \ge target
    • 移动 RR 直到 a[R]>targeta[R] > target
  5. 此时区间 [L,R)[L, R) 中的所有元素都等于 targettarget,数量为 RLR - L
  6. 累加答案。 时间复杂度:排序 O(NlogN)O(N \log N),双指针线性扫描 O(N)O(N),总复杂度 O(NlogN)O(N \log N)

注意事项

  • 题目数据范围较大,数字本身和最终统计的答案 ans 都有可能超过 int 上限,务必使用 long long
  • 本题中“不同位置的数字一样的数对算不同的数对”,上述两种方法都正确处理了这一点(通过计算数量而不是判断存在性)。

示例代码

方法一、二分查找

#include <algorithm>
#include <iostream>

// 使用 long long 防止溢出,C 的范围是 1 <= C < 2^30,A 和 B 同理
// 定义 long long 的别名 ll,方便编写
typedef long long ll;

// 全局数组,大小稍微开大一点防止越界
// N 最大 2*10^5
ll a[200005];

int main() {
    // 优化 I/O 操作速度
    std::ios::sync_with_stdio(false);
    std::cin.tie(NULL);

    int N;
    ll C;  // C 可能很大,用 ll
    std::cin >> N >> C;

    // 读取输入数组
    for (int i = 0; i < N; i++) {
        std::cin >> a[i];
    }

    // 排序是关键步骤。
    // A - B = C  =>  A = B + C
    // 排序后,对于确定的 B,我们可以二分查找 A (即 B + C)
    std::sort(a, a + N);

    ll ans = 0;
    // 遍历每一个数作为 B
    for (int i = 0; i < N; i++) {
        // 在 B 后面的位置寻找等于 B + C 的数的范围
        // lower_bound 返回第一个 >= (a[i] + C) 的位置
        // 注意:搜索范围 a + i + 1 到 a + N,因为 C >= 1,所以 A > B,A 一定在
        // B后面
        ll* start = std::lower_bound(a + i + 1, a + N, a[i] + C);

        // upper_bound 返回第一个 > (a[i] + C) 的位置
        ll* end = std::upper_bound(a + i + 1, a + N, a[i] + C);

        // 两个指针相减即为中间等于 a[i] + C 的元素个数
        // 如果找不到,start 和 end 会相等,结果为 0
        ans += (end - start);
    }

    // 输出结果
    std::cout << ans << std::endl;

    return 0;
}

方法二、双指针法

#include <algorithm>
#include <iostream>
#include <vector>

const int MAXN = 200005;
long long a[MAXN];

int main() {
    // 关闭同步,加速 IO
    std::ios::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n;
    long long c;
    std::cin >> n >> c;

    for (int i = 0; i < n; i++) {
        std::cin >> a[i];
    }

    // 1. 排序,为双指针或二分查找做准备
    std::sort(a, a + n);

    long long ans = 0;
    int l = 0, r = 0;

    // 2. 遍历每一个数作为 B (a[i])
    for (int i = 0; i < n; i++) {
        long long target = a[i] + c;  // 我们要找的目标 A

        // 寻找第一个 >= target 的位置 (l)
        // 随着 a[i] 增大,target 也增大,l 只会向右移动,不需要回退
        while (l < n && a[l] < target) {
            l++;
        }

        // 寻找第一个 > target 的位置 (r)
        // 同样,r 也只会向右移动
        while (r < n && a[r] <= target) {
            r++;
        }

        // 区间 [l, r) 中的所有元素都等于 target
        if (l < n && a[l] == target) {
            ans += (r - l);
        }
    }

    std::cout << ans << std::endl;

    return 0;
}

本文由coderli.com原创,按照CC BY-NC-SA 4.0 进行授权

所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code

luogu-”系列题目可在 洛谷题库 在线评测。

bcqm-”系列题目可在 编程启蒙题库 在线评测。

GESP/CSP认证交流QQ群: 688906745

GESP/CSP 认证学习微信公众号
GESP/CSP 认证学习微信公众号
最后更新于