1 教学目标
- 公式
, :以 为结尾的合法对数 - 本质不重不漏集合分类问题
- 固定结尾,用
map作为桶维护前面每种颜色的数量 - 把逐个结尾的贡献合并成组合数,理解
的来源
2 题目: 黑白气球
有一排黑白气球,你可以随机选两个球,问有多少种方法选到颜色不同的球?
- 第一行表示有多个气球
- 0表示白色
- 1表示黑色
6
0 1 1 0 0 1
输出
9
代码
//Author by [Rainboy](https://github.com/rainboylvx)
//date: 2024-05-03 17:22:44
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+5;
int n,m;
int a[maxn];
int ans;
int main (int argc, char *argv[]) {
std::cin >> n;
for(int i = 1;i <= n ;++i ) // i: 1->n
{
std::cin >> a[i];
}
//枚举以i为结尾
for(int i = 2;i <= n ;++i ) // i: 1->n
{
// 枚举对应的起点
for(int j = 1;j <= i-1 ;++j ) // j: 1->i
{
if( a[j] != a[i])
ans++;
}
}
std::cout << ans << "\n";
return 0;
}
3 解释
本质这是一个不重不漏的集合分类问题
先给每个气球进行编号
0 1 1 0 0 1
1 2 3 4 5 6
那么答案的
| 编号 | 对 |
|---|---|
| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 |
3.1 证明这是一个集合问题:
原题目部我们: 1. 任何选一对 2. 且这一对的颜色不同
我们一步一步做,任选一对气球,可以得到集合
3.2 集合分类
如果我们可以把集合
显然按对数的结尾的数字,可以把集合
证明:
- 不重: 显然以
为结尾的元素不可能和以 为结尾的元素在同一个集合 - 不漏: 任意一个元素一定会以
为结尾
4 对数问题的公式:
把上面的做法抽象出来.设
那么答案就是把每个结尾位置的贡献加起来:
这就是对数问题的公式
4.1 公式为什么是对的
- 不漏: 任意一个合法数对
一定有一个右端点 ,所以它一定会被某个 统计到 - 不重: 数对
的右端点 是唯一的,所以它只会被 统计一次,不会在别的子集里再算一遍 时左边没有任何元素,所以 ;从 开始累加结果也一样
这正是前面说的按结尾分类加上分类加法原理: 把集合
4.2 难点不在求和,而在怎么求
公式本身只是"把每个结尾的答案加起来",真正的难点是如何快速算出一个
最直接的办法是再枚举一遍
// 暴力:以 i 为结尾,再枚举起点 j
for (int i = 2; i <= n; i++)
for (int j = 1; j < i; j++)
if (a[j] != a[i])
ans++; // 这里就是在算 s(i)
5 方法一: 固定结尾,用 map 作为桶
固定第
用 map<int, long long> cnt 作为桶:cnt[color] 表示当前位置之前,颜色为 color 的气球数量.这里的键表示颜色,值表示数量;没有出现过的颜色,通过 cnt[color] 访问时会得到初始值
处理第
算完贡献以后,再把当前气球放进桶里,供后面的气球使用.
map<int, long long> cnt;
long long ans = 0;
for (int i = 1; i <= n; i++) {
ans += (i - 1) - cnt[a[i]]; // 只统计前面与当前气球异色的气球
cnt[a[i]]++; // 当前气球加入桶,供下一个结尾使用
}
对题目中的例子,整个计数过程如下.表中的桶数量都是加入当前气球之前的数量.
| 结尾 |
当前颜色 |
前面的白球数 | 前面的黑球数 | 本次贡献 |
|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 0 |
| 2 | 1 | 1 | 0 | 1 |
| 3 | 1 | 1 | 1 | 1 |
| 4 | 0 | 1 | 2 | 2 |
| 5 | 0 | 2 | 2 | 2 |
| 6 | 1 | 3 | 2 | 3 |
把最后一列相加,得到
先统计、再入桶,保证桶里始终只有前面的气球.如果把两行交换,当前气球会被当成前面的同色气球减掉,每个结尾都会少算
如果共有 std::map 每次访问需要 long long cnt[2] = {}; 作为桶,总时间为
桶里应当保存什么,取决于当前气球需要什么样的配对对象:
| 题目 | 合法条件 | 桶里存什么 | |
|---|---|---|---|
| 黑白气球 | 每种颜色在前面出现了几次 | ||
| 快乐数对 | 每个数值在前面出现了几次 |
6 方法二: 合并结尾的贡献,用组合数计数
继续从固定第
把所有结尾的贡献加起来,可以拆成两部分:
这里的
先看所有气球对.每个结尾前面的气球数依次为
再看某一种颜色.假设这种颜色共有
这也可以理解为: 从这
固定一个结尾时,我们只需从前面选一个气球;把一整组气球的所有结尾贡献加起来,才得到从这一组中选两个的组合数.
设白球共有
样例中
long long cnt[2] = {};
for (int i = 1; i <= n; i++)
cnt[a[i]]++;
long long total = n;
long long ans = total * (total - 1) / 2;
for (int color = 0; color < 2; color++)
ans -= cnt[color] * (cnt[color] - 1) / 2;
读入并统计颜色需要 long long,避免乘法溢出.
把公式展开,还能得到
两种方法计算的是同一个答案: 用桶时,每遇到一个结尾就累加贡献;用组合数时,先统计每种颜色的总数,再把这些贡献一次合并.
7 总结
有条件的一对组合(任取两个元素)问题,我们称为对数类问题.
从
- 对数类问题的通常解法: 按结尾位置把集合分类,答案就是
- 关键在于如何快速求出一个子集的数量
(通常用桶) - 同一组内任意两个元素都能配对时,可以把各个结尾的贡献合并为
;本题用总对数减去同色对数 - 这里用到了集合与分类加法原理
8 练习题目
建议按下面的顺序练习.先把本文的两种计数方法写熟,再练习从配对条件中找出桶的键.
8.1 巩固: 从颜色分类到组合数
1. atcoder abc206_c: [ABC206C] Swappable
给定一个整数序列,统计
尝试分别用“固定结尾、用桶统计”和“总对数减去同值对数”写两份程序.数值可达 map 记录次数;答案需要 long long.
2. atcoder abc159_d: [ABC159D] Banned K
对每个位置
训练点是组合数的局部变化: 先用
8.2 迁移: 根据配对条件查桶
统计满足
固定一个
4. codeforces 1520D: Same Differences
统计
尝试再次用本文的两种方法求解: 扫描时查询前面同类元素的数量,或者统计每一类的大小后累加组合数.这道题练习的是先变换条件,再分类计数.
8.3 进阶: 变换后的键与复合键
5. atcoder abc166_e: [ABC166E] This Message Will Self-Destruct in 5s
选择两个人,要求编号之差的绝对值等于两人的身高之和.固定编号较大的
把条件整理成
6. luogu P3405: [USACO16DEC] Cities and States S(USACO 原题)
两个城市的名称前两个字母,分别等于对方的州代码,且两个城市必须来自不同州.把每个城市表示成“名称前缀、州代码”这一对信息.
用 map<pair<string, string>, long long> 作为桶: 当前信息为
做每道题时,都先写清楚合法条件、固定一个对象后的贡献,以及桶的键和值分别表示什么.再判断这些贡献能否合并成组合数.
9 相关题解
| 题目 | OJ | 难度 |
|---|---|---|
| 找和为K的两个元素 | noi_openjudge | 入门 |
| [NOIP 2011 提高组] 选择客栈 | luogu | 普及 |
| Avoid K Partition | atcoder | 未知 |
| Number of Ways | codeforces | 1700 |