发布于 ,更新于 ,文章内容可能已经过时

计数杂题

部分题目来自这个题单

Luogu P6075 [JSOI 2015] 子集选取

首先,发现可以把元素单独拎出来考虑,假设对于一个元素来说有 种放置方案,则答案为

继续转化题意。

对于 ,它必须是它左边集合的子集,也得是上边集合的子集。
也就是说,如果 包含元素 ,则在它正上方、左上方、左侧的所有集合都包含 。(形式化来说,即 均包含

然后想象一种合法方案,容易发现每种方案就相当于把整个三角形分成两半,左上一半右下一半。

每两种方案不同当且仅当左上右下的分界线不同。

于是我们就能很轻松地求方案数了,即一个分界线从左下走到右上的方案。每一步可以向左或者向右走,即

快速幂即可。

Luogu P6146 [USACO 20 FEB] Help Yourself G

首先为了划分未求解和已求解的部分,我们需要把线段按照左端点排序。

给一个不排序会遇到错误的例子:

1
2
---- 1  ------3
--------- 2
复制

用下面的做法做的话,先算 和先算 是不一样的,可以自己体会一下。

然后设 为前 个线段的答案。为什么只有一维?因为容易发现子集这个东西很难用来划分阶段。另外前面的排序已经使得这种状态划分是唯一的了。

考虑如何推出

的答案由两部分组成:

  • 个线段的答案
  • 前面所有子集加上第 条线段的答案。

发现后者实际上也是分两部分的:

  • 新增的连通块(复杂度)
  • 除去新增,也就是原有的复杂度

好了,那么新增的连通块怎么算?实际上就是与第 条线段不相交的前 条线段的子集的数量。为啥呢?
那我们可以把前 条线段的子集分两类:与 相交与不相交。显然相交的子集不会贡献答案,不相交的子集会且仅会贡献 的答案。

然后问题就转化为:对前 条线段与线段 不相交的子集计数。
容易发现就是与线段 不相交的所有线段集合 的子集数量,计数就是

得到递推式:

的大小我们可以预处理出来:统计每一个右端点出现的次数 ,然后做一个前缀和,最后 就是不覆盖点 𝑖 的线段数量。

用快速幂优化,复杂度 O(𝑛log𝑛 +𝑛log𝑛),如果用光速幂可以干到 O(𝑛log𝑛 +𝑛 +𝑛)

P6008 [USACO 20 JAN] Cave Paintings P

首先发现答案就是把每个连通块的方案数乘起来。

那对于一个连通块怎么算答案?一个比较蛋疼的事实是,即使是同一个连通块,我们也不能保证它每个部分的水位是一样的,比如下面这个例子:

1
2
3
4
5
6
########
### ###
# #
# ## #
# ## #
########
复制

显然左右侧在分割开的时候可以水位高度不同。

那我们能不能对分割开的两块分别算呢?答案是不行,因为连通器原理。

1
2
3
4
5
6
########
# ## #
# ## #
# #
# #
########
复制

初中物理可知,左右两侧水位必须一样高。

所以我们需要动态维护这个关系,用啥呢?并查集。

初始化每个格子答案为 1,然后对于每个连通块从下往上涨水,过程如下:

  • 把这一行相邻的能放水的位置连在一起,表示他们必须是一个水位的。
  • 遍历该行每一个空位,如果这个位置下面也是空位且二者目前没有相连,那么当前位置方案数 *= 正下方一格方案数,同时把两个格子相连,表示两个连通块接在一起了。
  • 遍历该行每一个空位,每遇到一个连通块(find(x) == x)就把该格方案数加一。

最后用前面提到过的判断连通块的方式把答案乘起来就好了。

Luogu P1350 车的放置

同样是蓝题,但是比上一道水多了。

每一行每一列都只能放一个棋子,那直接从上往下遍历行,同时枚举放几颗棋子递推计数即可。

具体来说,设 𝑓𝑖,𝑗 为前 𝑖 行放了 𝑗 个棋子的方案数量,则转移方程如下:

𝑓𝑖,𝑗=𝑓𝑖1,𝑗+𝑓𝑖1,𝑗1(𝑙𝑒𝑛𝑗+1)

其中 𝑙𝑒𝑛 是行长度。

记得把 𝑓0𝑏+𝑑,0 设为 1

Luogu P3223 [HNOI 2012] 排队

基本上是纯数学题。

大概计数题可以分两种大方向思考:正推、容斥。

而容斥也大概分两种:手动容斥和套式子反演。

想直接拆分算,感觉是一个非常恶心难想的分类讨论。

递推,设 𝑓𝑛,𝑚𝑛 男生 𝑚 女生的方案数,发现推不了。

那就手动容斥。
容易发现把老师的限制扔掉之后是可以轻松算出来答案的:𝐴𝑚𝑚𝐴𝑛+2𝑛+2(𝑛+3𝑚),即把老师当成男生随便放置,然后在他们中间的空隙中选出来 𝑚 个用来放女生。

然后不合法方案,即两个老师靠在一起的方案,把两个老师绑在一起当一个男生求出来所有合法方案即可。别忘了两个老师顺序可以变,所以加一个 𝐴22

最终答案:𝐴𝑚𝑚𝐴𝑛+2𝑛+2(𝑛+3𝑚) 𝐴𝑚𝑚𝐴𝑛+1𝑛+1(𝑛+2𝑚)𝐴22

为啥说基本上是纯数学题而不是完全的数学题?因为这题需要打高精。

然后我用 python 写的,python 整数除法返回浮点值,所以要强制整除,即使用 //

举个例子,算 (𝑛𝑚)

1
2
3
4
5
6
7
import math

def binom(n, m) -> int:
if n < m:
return 0
else:
return math.factorial(n)//math.factorial(m)//math.factorial(n-m)
复制

[CSP-S 2019] Emiya 家今天的饭

挺难的题。

首先我们可以把选菜看成从方阵里面选数,每行只能选一个,每列最多选 𝑘2 个。

如果我们分列来考虑,发现合法的列有很多,但是不合法的列只会出现一个。(显然选的数超过总数一半的列只会有一个吧)
换句话说,如果我们正着算,记录合法方案总数,是一件非常困难的事情,因为每一列的合法情况我们都要记录。但是相对的,对于不合法方案计数,我们可以枚举不合法的列进行递推。当我们保证这一列不合法时,其他列一定合法,这样我们就不用记太多状态了。

枚举到第 𝑐𝑜𝑙 行,设 𝑓(𝑐𝑜𝑙)𝑖,𝑗,𝑘 为第 𝑐𝑜𝑙 列为不合法列,前 𝑖 行,第 𝑐𝑜𝑙 行选了 𝑗 个数,其他行选了 𝑘 个数的方案数量,𝑠𝑖 为第 𝑖𝑎 的总和,则有转移方程:

𝑓𝑖,𝑗,𝑘=𝑓𝑖1,𝑗,𝑘+𝑓𝑖1,𝑗1,𝑘𝑎𝑖,𝑐𝑜𝑙+𝑓𝑖1,𝑗,𝑘1(𝑠𝑖𝑎𝑖,𝑐𝑜𝑙)

然后不合法方案数即 𝑗>𝑘𝑓(𝑐𝑜𝑙)𝑛,𝑗,𝑘

复杂度 Θ(𝑚𝑛3),想办法减少状态。

根据刚才不合法方案数的计算,我们发现,一个方案不合法,当且仅当有一列选的数大于其他列选的数之和。

换句话说,刚才对不合法方案数的统计,基于 𝑗 >𝑘 这个条件,那我们是不是可以只记录 𝑗 𝑘 这个东西呢?

𝑓(𝑐𝑜𝑙)𝑖,𝑗 为第 𝑐𝑜𝑙 列为不合法列,前 𝑖 行,当前行选的数比别的行多 𝑗 个的总方案数,转移方程和上面的差不多:

𝑓𝑖,𝑗=𝑓𝑖1,𝑗+𝑓𝑖,𝑗1𝑎𝑖,𝑐𝑜𝑙+𝑓𝑖,𝑗+1(𝑠𝑖𝑎𝑖,𝑐𝑜𝑙)

不合法方案数:𝑗>0𝑓(𝑐𝑜𝑙)𝑛,𝑗

容易发现这个东西可能是负数,所以实现的时候要给 𝑗 这一维加一个 𝑛

然后是计算总方案数,设 𝑔𝑖,𝑗 为前 𝑖 行选了 𝑗 个数的方案数,转移方程:

𝑔𝑖,𝑗=𝑔𝑖1,𝑗+𝑔𝑖1,𝑗1𝑠𝑖

最终答案:𝑛𝑖=1𝑔𝑛,𝑖 𝑚𝑐𝑜𝑙=1𝑛𝑗>0𝑓(𝑐𝑜𝑙)𝑛,𝑗

Luogu P3214 [HNOI 2011] 卡农

题意要求选取子集有三条限制:
- 非空 - 选出的子集两两不同 - 所有元素的出现次数为偶数

转化 1:将“子集”转化为“二进制数”,然后选取子集变成从 0,12𝑛 1 个数里面选出 𝑚 个,然后限制就变成了:
- 这些数不等于 0 - 这些数异或和为 0 - 这些数两两不同

转化 2:答案是“无序”的方案数,我们发现这个 𝑚 个数无序的方案数乘一个排列 𝐴𝑚𝑚 就是有序的方案数,显然后者比较容易划分阶段,所以我们可以算出有序的方案数再除以 𝐴𝑚𝑚

然后我们发现这上面几条限制挺难同时记录并递推的,那我们可以考虑一下排除不合法方案。

𝑓𝑖 为选 𝑖 个数的方案数,先考虑第二条限制。在这条限制下,当 1 𝑖 1 这些数选完之后,第 𝑖 个数是确定的。换句话说,满足第二条限制的方案数,即为前 𝑖 1 个数随意选择的方案数(非空):𝐴𝑖12𝑛1

然后考虑第一条限制,发现第 𝑖 个集合为空且总异或和为 0,把第 𝑖 个数(空)去掉依然合法。不满足第一条限制的方案数即 𝑓𝑖1

最后考虑第三条性质:假设第 𝑖 个数与前面第 𝑗 个数相同了,那么这个第 𝑖 个数有 2𝑛 1 (𝑖 2) 种取值,这个第 𝑗 个数的位置有 𝑖 1 种可能性。除此以外的地方合法(前面已经保证异或和为 0 了,现在再异或两个相同的数结果不变)且长度为 𝑖 2。因此不满足这个要求的方案数有 𝑓𝑖2(2𝑛 1 (𝑖 2))(𝑖 1)

然后就可以递推求解了,𝑓𝑖 =𝐴𝑖12𝑛1 𝑓𝑖1 𝑓𝑖2(2𝑛 1 (𝑖 2))(𝑖 1),答案是 𝑓𝑚

𝐴𝑖12𝑛1 可以预处理,𝐴12𝑛1 =2𝑛 1,剩下的挨个乘就好了。预处理到 𝑚 即可。

CF 932 E

模拟赛写不动题了过来写这玩意挺合理的吧

原题意是给定 |𝕊|,𝑘,求 𝕋𝕊|𝕋|𝑘

然后我们发现这个东西只与 𝕊,𝕋 的大小有关,所以改为枚举大小,u设 𝑛 =𝕊,原式化为:

𝑛𝑖=1(𝑛𝑖)𝑖𝑘

𝑖𝑘 用组合意义干掉。发现这玩意就是 𝑘 个不同小球放进 𝑖 个不同盒子的方案数。

继续组合意义分析,上面那个式子相当于是:从 𝑛 个盒子里选出 𝑖 个盒子,然后把 𝑘 个小球放在这 𝑖 个盒子里面的方案数。

继续发现发现这 𝑘 个小球最多只能放满 𝑘 个盒子,所以我们转头去枚举“哪些盒子有小球”。设 𝑓𝑖,𝑗 为从 𝑛 个盒子选出 𝑗 个,𝑖 个小球放入这 𝑗 个盒子,盒子非空的方案数,然后我们要求的东西就变成了:

𝑘𝑖=1𝑓𝑘,𝑖2𝑛𝑖

(确定 𝑗 个盒子要放入小球之后,剩下 𝑛 𝑗 个小球可能会被选入最初选的 𝑖 个盒子中也有可能不被选入,所以有一个 2𝑛𝑖)。𝑘 5 ×103Θ(𝑘2) 能过。

𝑖 个小球放入 𝑗 个盒子,盒子非空的方案数,相当于 𝑖 个元素被划分成 𝑗 个非空子集的方案数,联想到第二类斯特林数。又因为选出来的 𝑗 个盒子不一定是哪几个,而且两两不同,我们还得乘进去一个排列。即 𝑓𝑘,𝑖 ={𝑘𝑖}𝐴𝑖𝑛,最终答案即为:

𝑘𝑖=1{𝑘𝑖}𝐴𝑖𝑛2𝑛𝑖

第二类斯特林数可以 Θ(𝑘2) 预处理,递推式为 {𝑖𝑗} ={𝑖1𝑗1} +𝑗{𝑖1𝑗}𝐴𝑖𝑛 实际上就是 𝑛 的下降幂,这玩意也是直接预处理到 𝐴𝑘𝑛 就好。后面的 2𝑛𝑖 可以用快速幂或者光速幂。光速幂没啥太大必要,毕竟复杂度瓶颈不在这。

CF 1850 G

感觉这题没啥必要放,毕竟 CF 1500,但是我写的 Θ(𝑛log𝑛) 大概算是常数比较小的版本吧。

发现满足要求的两个点只有四种可能: - 横坐标相等 - 纵坐标相等 - 横纵坐标和相等 - 横纵坐标差相等

所以我们把所有点按照上面四个元素作为关键字排序四次,每次分别统计相等的个数 𝑥,让答案加上 𝑥(𝑥 1) 就好了。

感觉比他们写的 std::map 做法常数要小。

CF 1485 F

简化题意

给定数组 𝑏,对合法的 𝑎 计数。
合法的 𝑎𝑖 [1,𝑛],𝑏𝑖 =𝑎𝑖𝑏𝑖 =𝑖𝑗=1𝑎𝑗

我们一个一个数填,发现对于每一个 𝑎𝑖,有两种可能: - 𝑎𝑖 =𝑏𝑖 - 𝑎𝑖 =𝑏𝑖 𝑖1𝑗=1𝑎𝑖

然后设 𝑓𝑠 为当前和等于 𝑠 的方案数。然后上面两个可能就变成了: - 𝑓𝑥+𝑏𝑖 =𝑓𝑥 - 𝑓𝑏𝑖 =𝑗𝑓𝑗

发现,第一个操作就是把整个数组向右移动了 𝑏𝑖,所以我们只需要记录一下偏移量每次加一个 𝑏𝑖 就完成了 Θ(1) 转移。

第二个实际上等于一个位置前面所有元素的和,𝑓𝑥 变成了 𝑎𝑛𝑠,然后原来的 𝑓𝑥 没了,于是 𝑎𝑛𝑠 =𝑎𝑛𝑠 𝑓𝑥 +𝑎𝑛𝑠

值域很大,所以要用 std::map 或者 std::unordered_map,后者需要重写哈希函数。

示例代码
1
2
3
4
5
6
7
8
9
10
11
12
cin >> n;
for (i32 i = 1; i <= n; ++ i) cin >> b[i];
f.clear();

i32 ans = f[0] = 1, delta = 0;
for (i32 i = 1; i <= n; ++ i) {
delta += b[i];
i32 add = (ans - f[b[i] - delta] + mod) % mod;
f[b[i] - delta] = ans;
ans = (ans + add) % mod;
}
cout << ans << '\n';
复制

CF 449 D

简化题意

给定数组 𝑎,从 𝑎 选出非空子集使其按位与和结果为 0。求方案数

𝑓𝑠 为与和为 𝑠 的方案数,𝑔𝑠 为与和是 𝑠 的超集的方案数。

显然 𝑓 做高维后缀和就得到了 𝑔

但是发现这个 𝑓𝑔 都不好直接求,然后发现如果设 𝑠 为数列中 𝑠 的超集的数量,那么 𝑔𝑠 =2𝑠 1(从这些超集里面随便几个取并集,显然结果一定仍然是 𝑠 的超集)。

然后继续发现,𝑠 可以对 𝑠 计数之后直接高位后缀和得到。

总体流程:先对 𝑠 用桶计数,然后高维后缀和,然后 𝑔𝑠 =2𝑠 1,然后对 𝑔 反着(加法变减法)做一遍高维后缀和就好了。

示例代码
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
i32 f[N];
i32 n;
constexpr i32 mod = 1e9+7;
constexpr i32 s = (1 << 20) - 1;

void sos(const i64& x) {
for (i32 i = 0; i < 20; ++ i)
for (i32 j = s; j >= 0; -- j)
if((j & (1 << i)) == 0)
f[j] = (f[j] + f[j | (1 << i)] * x % mod + mod) % mod; // SOS DP
}

int main() {
std::ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);

i32 x;

cin >> n;
for (i32 i = 1; i <= n; ++ i) {
cin >> x;
++f[x];
}

sos(1);
for (i32 i = 0; i <= s; ++ i) f[i] = binpow(f[i]) - 1;
sos(-1);

cout << f[0];

return 0;
}
复制