配对问题
配对问题就是固定右端点 `j`,把所有合法左端点 `i < j` 按条件快速数出来。
一句话算法
配对问题就是固定右端点 j,把所有合法左端点 i < j 按条件快速数出来。
问题模型
给定一个序列,统计满足某种条件的下标对:
最直接的做法是两层循环枚举所有对,复杂度
配对问题的优化思路是:
- 从左到右枚举右端点
j。 - 把
j左边的信息维护在桶、哈希表、窗口或队列里。 - 对每个
j,直接查出有多少个i能和它配对。
通用形式:
其中 s(j) 表示“以 j 为右端点的合法配对数量”。
核心直觉
任意一对 (i,j) 都有唯一的右端点 j。所以按右端点分类,一定不重不漏。
黑白气球例子:
位置: 1 2 3 4 5 6
颜色: 0 1 1 0 0 1
当枚举到一个黑气球时,它能和左边所有白气球配对;当枚举到一个白气球时,它能和左边所有黑气球配对。
所以只需要维护左边有多少个 0 和多少个 1。
算法步骤
基础模板:
- 初始化一个维护历史信息的数据结构
state。 - 从左到右枚举当前位置
j。 - 先用
state计算s(j),把它加入答案。 - 再把
a[j]加入state,供后面的右端点使用。
“查询要在插入当前元素之前”
如果先插入当前元素,就可能把 j 自己当成左端点,破坏 i < j。
算法证明
关键不变量: 处理位置 j 前,state 中只包含下标 < j 的元素信息。
- 初始化时,
state为空,确实没有任何左端点。 - 处理
j时,先查询state,得到的都是i < j的配对。 - 查询后才插入
a[j],所以a[j]只会参与后续右端点的配对。 - 任意合法配对
(i,j)会且只会在枚举到右端点j时被统计一次。
所以按右端点统计既不会重复,也不会遗漏。
复杂度分析
不同模型取决于 state 的维护方式:
- 二值桶:
。 - 哈希表计数:期望
。 - 余数桶:
。 - 滑动窗口计数:期望
。 - 单调队列维护窗口最值:
。
空间复杂度通常是桶或哈希表大小。
代码实现
二值属性不同配对
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <bits/stdc++.h>
using namespace std;
// Count pairs (i, j), i < j, with different binary values.
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
long long cnt[2] = {0, 0};
long long ans = 0;
for (int i = 0; i < n; ++i) {
int x;
cin >> x;
ans += cnt[x ^ 1];
++cnt[x];
}
cout << ans << '\n';
return 0;
}
Two Sum 配对计数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <bits/stdc++.h>
using namespace std;
// Count pairs (i, j), i < j, with a[i] + a[j] == target.
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
long long target;
cin >> n >> target;
unordered_map<long long, long long> cnt;
long long ans = 0;
for (int i = 0; i < n; ++i) {
long long x;
cin >> x;
auto it = cnt.find(target - x);
if (it != cnt.end()) ans += it->second;
++cnt[x];
}
cout << ans << '\n';
return 0;
}
同余配对计数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <bits/stdc++.h>
using namespace std;
// Count pairs (i, j), i < j, with (a[i] + a[j]) % mod == 0.
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, mod;
cin >> n >> mod;
vector<long long> cnt(mod, 0);
long long ans = 0;
for (int i = 0; i < n; ++i) {
long long x;
cin >> x;
int r = (int)((x % mod + mod) % mod);
int need = (mod - r) % mod;
ans += cnt[need];
++cnt[r];
}
cout << ans << '\n';
return 0;
}
距离限制相等配对
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <bits/stdc++.h>
using namespace std;
// Count pairs (i, j), i < j, with a[i] == a[j] and j - i <= k.
// The map stores values inside the current left window [j-k, j-1].
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<long long> a(n);
unordered_map<long long, long long> cnt;
long long ans = 0;
for (int j = 0; j < n; ++j) {
cin >> a[j];
if (j > k) {
long long expired = a[j - k - 1];
auto it = cnt.find(expired);
if (it != cnt.end()) {
--it->second;
if (it->second == 0) cnt.erase(it);
}
}
auto it = cnt.find(a[j]);
if (it != cnt.end()) ans += it->second;
++cnt[a[j]];
}
cout << ans << '\n';
return 0;
}
距离限制最大配对和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <bits/stdc++.h>
using namespace std;
// Given a[0..n-1], find max a[i] + a[j] with i < j and j - i <= k.
// Monotonic deque keeps candidate i with maximum a[i] in the current window.
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
vector<long long> a(n);
for (long long &x : a) cin >> x;
deque<int> q;
long long ans = LLONG_MIN;
for (int j = 0; j < n; ++j) {
while (!q.empty() && q.front() < j - k) q.pop_front();
if (!q.empty()) {
ans = max(ans, a[q.front()] + a[j]);
}
while (!q.empty() && a[q.back()] <= a[j]) q.pop_back();
q.push_back(j);
}
cout << ans << '\n';
return 0;
}
测试用例
黑白气球输入:
6
0 1 1 0 0 1
输出:
9
Two Sum 输入:
6 10
1 5 3 4 9 7
输出:
2
距离限制相等配对输入:
6 2
1 2 1 3 1 2
输出:
2
应用分类详解
配对问题的本质是“右端点枚举 + 左侧信息查询”。
一、属性配对
典型模式: 两个元素的类别满足相同、不同、互补等条件。
识别信号: 黑白、颜色、性别、类别、标签。
核心建模: 用桶记录左侧各类别数量。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 黑白气球 | 本页示例 | 当前是 0 就加左侧 1 的数量,当前是 1 就加左侧 0 的数量 |
| 同色配对 | luogu-P1311 | 统计同色左端点,并处理额外有效性条件 |
二、数值配对
典型模式: 要求
识别信号: “两数之和”“差为 K”“找一对数”。
核心建模: 枚举 a[j],在哈希表中查需要的另一半。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| Two Sum 计数 | noiopenjudge ch0201-6184 | 查 target - a[j] |
| A-B=C 数对 | luogu-P1102 | 固定一个端点,查另一个值出现次数 |
三、同余配对
典型模式: 两数和能被 k 整除。
识别信号: “整除”“余数”“mod k”。
核心建模: 当前余数为 r,需要左侧余数 (k-r)%k。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 和为 k 的倍数 | 同余配对基础题 | 用余数桶 |
| 分组配对 | 数论基础题 | 先按余数分类再计数 |
四、距离限制配对
典型模式: 配对还要求 j-i <= k。
识别信号: “距离不超过”“最近 k 个”“窗口内”。
核心建模: 只维护 [j-k, j-1] 这个窗口中的历史信息,过期元素要移除。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 窗口内同值配对 | 本页示例 | add/remove/query 动态维护桶 |
| 选择客栈 | luogu-P1311 | 条件有效后,把历史同色数量转为可配对数量 |
五、窗口最值配对
典型模式: 配对不是计数,而是求窗口内最大/最小值。
识别信号: “距离不超过 k 的最大两数和”“滑动窗口最大值”。
核心建模: 计数桶不适合维护最值,使用单调队列保留窗口内最强候选。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 滑动窗口最值 | luogu-P1886 | 单调队列维护窗口最大/最小 |
| 距离限制最大配对和 | 本页模板 | 对每个 j,取窗口内最大 a[i] |
经典例题
- luogu-P1102 A-B 数对:数值配对计数。
- luogu-P1311 选择客栈:属性配对叠加区间有效性。
- luogu-P1886 滑动窗口:单调队列维护窗口最值。
- noiopenjudge ch0201-6184 找和为 K 的两个元素:Two Sum 基础题。