计数

基本概念

容斥

容斥解决满足多条件的组合方案数问题,将各个条件集合的形式表达,即可套用容斥原理。

  • |U| :全集。

  • |Si| :满足条件 Si 的方案数。

  • |i=1nSi| :满足任一条件的方案数。

  • |i=1nSi| :满足全部条件的方案数。

两个公式:

  • |i=1nSi|=j=1n(1)j1×ai<ai+1|i=1jSai|

  • |i=1n|Si=|||i=1nSi¯|


二项式反演

二项式反演一般用于转化恰好至多(少)之间的数量关系。

常见形式:

  • fi=i=0n(1)i×Cni×gigi=i=0n(1)i×Cni×fi

  • fn=i=0nCni×gign=i=0n(1)ni×Cni×fi

高维二项式反演:

  • fx,y=i=xnj=ymCixCjy×gi,jgx,y=i=xnj=ym(1)i+jxyCixCjy×fi,j

  • 剩下以此类推。

恰好与至多的转化

fk 表示至多满足 k 个条件,gk 满足恰好 k 个条件,由二项式反演,得:

fk=i=0kCki×gigk=i=0k×(1)ki×Cki×fi

恰好与至少的转化

fk 为至少满足 k 个条件,gk 满足恰好 k 个条件,由二项式反演,得:

fk=i=knCik×gigk=i=kn(1)ik×Cik×fi

如果题目要求恰好的数量,要么直接求(DP)很好求,要么间接求再用二项式反演求很好求。

错排问题

问题:某人写了 n 封信和 n 个信封,如果所有的信都装错了信封。求所有信都装错信封共有多少种不同情况。

证明略。递推式如下:

f1=0,f2=1fn=(n1)×(fn1+fn2)(n3)


整除分块

一般解决形如一下的问题:

i=0nni

可以证明,在第一个使 ni 取不同值的 i 时,

inni

时,ni 的取值相等。

又可以证明,这样的块不超过 2n 个,于是可以在根号时间复杂度内算出值。

题目选做

数论相关

1. H(n)

整除分块模板题。

#include <cstdio>
#include <iostream>
using namespace std;
typedef long long LL;
int T;
LL n;
int main () {
cin >> T;
while(T --) {
cin >> n;
LL ans = 0;
for (LL l = 1, r; l <= n; l = r + 1) {
r = min(n, n / (n / l));
ans += (n / l) * (r - l + (LL)1);
}
cout << ans << endl;
}
return 0;
}

2. Calculating

由算数基本定理可知,f(x) 就是 x 的约数个数。将要求的式子转化为:

i=0rf(i)i=0l1f(i)

其中,i=0rf(i) 等价于 i=0rni ,可以使用整除分块。

3.[POI2007]ZAP-Queries

求以下式子:

x=1ay=1b(gcd(x,y)==d)

转化为:

a=ad,b=bdx=1ay=1b(gcd(x,y)==1)

正难则反,可以用容斥的思想做,转化为:

a1b1a2b2a3b3...+a6b6...

即:

i=1min(a,b)μ(i)aibi

莫比乌斯函数即为容斥系数。

因为这个式子就是整除分块的基本式,所以将 μ(i) 做一个前缀和就可以用整除分块。

莫比乌斯函数预处理过程:

void prework() {
mul[1] = 1;
for (int i = 2; i <= M; i ++) {
if (!f[i]) {
prime[++tot] = i;
mul[i] = -1;
}
for (int j = 1; j <= tot && i * prime[j] <= M; j ++) {
LL k = i * prime[j];
f[k] = 1;
if (i % prime[j] == 0) {
mul[k] = 0;
break;
}
else mul[k] = mul[i] * -1;
}
}
for (int i = 1; i <= M; i ++) sum[i] = sum[i - 1] + mul[i];//前缀和
}

4. [HAOI2011]Problem b

求下列式子的值:

x=aby=cd(gcd(x,y)==k)

通过简单容斥,转化为:

K=(gcd(x,y)==k)x=1by=1dKx=1by=1c1Kx=1a1y=1dK+x=1a1y=1c1K

类似于矩阵前缀和的形式,可以画出矩阵更加直观的理解。

那么问题就转化成了求

x=1ny=1m(gcd(x,y)==k)

的值,就是上一题

5.四元组统计

正难则反。所有四元组有 Cn4 个,用容斥减去不合法的方案数,也就是 gcd!=1 的方案数,设 f(i) 表示至少 gcd=i 的四元组数,答案即为:

Ans=i=1maxμ(i)×f(i)

考虑求出 f(i) ,可以统计因数 i 在数列每个数中是否出现,记 sum(i) 表示因数 i 出现在数列中数字的个数,那么:

f(i)=Csum(i)4

考虑求出 sum(i) ,暴力一点的想法就是直接枚举每个数的因数,时间复杂度为 O(Tna),极限数据恰好是1e8,可以通过此题。

类似于素数筛法,可以利用筛法的思想将每一个数的因数筛出来,这样可以大大提高代码运行效率。

Ps:双倍经验:MSKYCODE - Sky Code

6. NGM2 - Another Game With Numbers

很容易联想到容斥原理,可以用 dfs 搜出所有的组合,然后做容斥。

code

#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL n, k, ans;
int f[20], b[20];
vector<int> a;
inline LL gcd(LL a, LL b) { return b == 0?a:gcd(b, a%b); }
inline LL lcm(LL a, LL b) { return a / gcd(a, b) * b; }
inline void dfs(int nd, int m, LL s, int p) {
if (s > n) return;
if (nd == m + 1) {
ans += n / s * p;
return ;
}
for (int i = b[nd - 1] + 1; i <= k; i ++) {
LL lll = lcm(s, a[i - 1]);
b[nd] = i;
dfs(nd + 1, m, lll, p);
}
}
int main() {
ios::sync_with_stdio(0);
cin >> n >> k;
for (int i = 1; i <= k; i ++) {
int x;
cin >> x;
a.push_back(x);
}
int p = 1;
for (int i = 0; i < k; i ++) {
memset(b, 0, sizeof(b));
dfs(0, i, 1, p);
p *= -1;
}
cout << n - ans << endl;
return 0;
}

7. Devu and Birthday Celebration

考虑枚举 n 的因数 x ,将 nx 拆分成 f 份,那么这样拆分出的 ai 必然满足有 x 这个因数。

然后考虑容斥,在根号时间内枚举 n 的因数,然后用莫比乌斯函数做容斥系数。即:

Ans=x|nμ(x)×(nxf)

8.SQFREE - Square-free integers

直接容斥,求式子:

i2nμ(i)×ni2

9. List Of Integers

先来考虑这样一个问题:求 1x 有多少个数与 p 互质。

问题可以转化为求如下式子的值:

i=1x(gcd(i,p)==1)

这个式子是我们的老熟人了,直接套容斥。即求如下式子的值:

a|nμ(a)×ai

回到原问题,我们可以二分要求的数,那么 check 函数就是如上问题。

code

#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
#define R register
const int N = 1e6 + 10;
const int MAX = 1e7;
const int M = 1e6;
typedef long long LL;
int T;
LL x, p, k;
int f[N];
LL cnt, ys[N];
LL mul[N], prime[N], tot;
inline void init() {
mul[1] = 1;
for (R int i = 2; i <= M; i ++) {
if (!f[i]) {
prime[++tot] = i;
mul[i] = -1;
}
for (R int j = 1; j <= tot && i * prime[j] <= M; j ++) {
LL t = i * prime[j];
f[t] = 1;
if (i % prime[j] == 0) {
mul[t] = 0;
break;
}
mul[t] = mul[i] * -1;
}
}
}
inline LL check(LL k, LL p) {
LL ans = 0;
for (R int i = 1; i <= cnt; i ++) {
ans += mul[ys[i]] * (k / ys[i]);
}
return ans;
}
int main() {
ios::sync_with_stdio(0);
init();
cin >> T;
while (T --) {
cin >> x >> p >> k;
cnt = 0;
for (R int i = 1; i * i <= p; i ++) {
if (p % i == 0) {
ys[++cnt] = i;
if (i * i != p) ys[++cnt] = p / i;
}
}
LL ans_x = check(x, p);
LL l = x, r = MAX, ans = 0;
while (l <= r) {
LL mid = l + r >> 1;
if (check(mid, p) - ans_x >= k) ans = mid, r = mid - 1;
else l = mid + 1;
}
cout << ans << endl;
}
return 0;
}

DP相关

1. [JSOI2011]分特产

正难则反。

fi 表示钦定(至少)前 i 个人没有分到特产,其他的放任自流的方案数。

gi 表示恰好 i 个人没有分到特产的方案数。

易得,g0 就是答案,由二项式反演,得:

g0=i=0n(1)i0×(ni)×fi

考虑如何求 f 数组。

要求 fi ,只需考虑剩下 ni 个人的取特产情况,考虑将每个特产依次分给这 ni 个人,由于不一定每个人都要分到,所以问题就等价于允许有空盒不同盒子相同球的模型,于是得到如下式子:

fi=j=1m(aj+ni1ni1)

然后再用上面的式子求出 g0 即为答案。

2.Cheerleaders

正难则反。

fi 表示钦定 i 条边没有人占,其余放任自流的方案数。

容易得出全集即为 f0=(n×mk)

考虑求出 f 数组,

i=1 时,除去一条边不能占,那么就有 m×(n1) 或者 n×(m1) 的区域是爱占不占的,于是:

f1=2×(m×(n1)k)+2×(n×(m1)k)

i=2 时,除去两条边不能占,那么就有 n×(m2)m×(n2)(n1)×(m1) 的区域是爱占不占的,于是:

f2=(n×(m2)k)+(m×(n2)k)+2×((n1)×(m1)k)

同理可得,当 i=3i=4 的方案数:

f3=2×((n1)×(m2)k)+2×((n2)×(m1)k)f4=((n2)×(m2)k)

套用容斥可知答案为:

Ans=f0f1+f2f3+f4

3. [JSOI2015]染色问题

正难则反。

咱这有个不需要脑子的二项式反演做法。。。

fi,j,k 表示钦定 i 行,j 列一个没染,k 个颜色一个没用,其余放任自流的方案数。

gi,j,k 表示恰好 i 行,j 列一个没染,k 个颜色一个没用的方案数。

易得:

fi,j,k=(ni)(mj)(ck)×(ck+1)(ni)(mj)

再由高维二项式反演得:

fx,y,z=i=xnj=ymk=zc(ni)(mj)(ck)×gx,y,zgx,y,z=i=xnj=ymk=zc(1)i+j+kxyz×(ix)(jy)(kz)×fi,j,k

由于答案就是 g0,0,0 ,所以:

g0,0,0=i=0nj=0mk=0c(1)i+j+k×(n0)(m0)(c0)×fi,j,k=i=0nj=0mk=0c(1)i+j+k×(n0)(m0)(c0)×(ni)(mj)(ck)×(ck+1)(ni)(mj)=i=0nj=0mk=0c(1)i+j+k×(ni)(mj)(ck)×(ck+1)(ni)(mj)

若直接求这个式子的值,时间复杂度为 O(nmc×log(nm)) ,非常悬。这类柿子一般用二项式定理优化,发现 (ck+1) 的指数 (ni)(mj)k 一点关系没有,于是将 k=0c 拉到最外面枚举,得:

g0,0,0=k=0ci=0nj=0m(1)i+j+k×(ni)(mj)(ck)×(ck+1)(ni)(mj)g0,0,0=k=0c(1)k(ck)i=0n(1)i(ni)j=0m(1)j(mj)×(ck+1)(ni)(mj)

由二项式定理,得:

g0,0,0=k=0c(1)k(ck)i=0n(1)i(ni)×((ck+1)ni1)m

时间复杂度降为 O(nc×log(m))

4.Sky Full of Stars

正难则反。。。

咱又有一个不需要脑子的高维二项式反演做法。。。

fi,j 表示钦定 ij 列颜色相同,其余的放任自流的方案数。

gi,j 表示恰好 ij 列颜色相同的方案数。

那么 Ans=||g0,0||=3n×n

由二维二项式反演可得:

g0,0=i=0nj=0n(1)i+j×(i0)(j0)×fi,j=i=0nj=0n(1)i+j×fi,j

考虑求出 f 数组。

有一个显然的性质:如果至少有 1 1 列满足题目要求,那么满足要求的这些行列一定是同种颜色的。如果只有行满足要求,那么行之间就不一定要同种颜色;列也是同理。

于是很快可以求出:

fi,0=(ni)×3i×3n(ni)f0,j=(nj)×3j×3n(nj)fi,j=(ni)(nj)×3×3(ni)(nj)(i1,j1)

由于 fi,0f0,j 都可以在线性时间内求出来,而 fi,j 不行,所以将 fi,j 单独拎出来考虑。

s=j=1n(1)0+j×f0,j+i=1n(1)i+0×fi,0

g0,0=i=1nj=1n(1)i+j×fi,j+s+f0,0=i=1nj=1n(1)i+j×(ni)(nj)×3×3(ni)(nj)+s+f0,0=3×i=1n(1)i×(ni)×j=1n×(nj)×(3ni)nj×(1)j+s+f0,0=3×i=1n(1)i×(ni)×((3ni1)n(3ni)n)+s+f0,0

注意到 f0,0=3n×n ,所以 Ans=3×i=1n(1)i×(ni)×((3ni1)n(3ni)n)+s

code

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const LL MOD = 998244353;
const int N = 1e6 + 10;
const int M = 1e6 + 10;
LL n, sum1, sum;
LL fac[N];
LL ksm(LL a, LL b) {
LL sum = 1;
while (b) {
if (b & 1) sum = sum * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return sum;
}
void init() {
fac[0] = 1;
for (LL i = 1; i <= N - 10; i ++) fac[i] = fac[i - 1] * i % MOD;
}
LL C(LL n, LL m) {
if (n < m) return 0;
LL fz = fac[n], fm = fac[m] * fac[n - m] % MOD;
return fz * ksm(fm, MOD - 2) % MOD;
}
int main() {
init();
cin >> n;
int p = -1;
for (int i = 1; i <= n; i ++) {
sum1 += p * C(n, i) * ksm(3, i) % MOD * ksm(3, n * (n - i)) % MOD;
sum1 %= MOD;
p *= -1;
}
sum1 = (sum1 * 2 % MOD);
p = -1;
for (LL i = 1; i <= n; i ++) {
sum += p * C(n, i) * (ksm(ksm(3, n - i) - 1, n) - ksm(3, n * (n - i))) % MOD;
sum = (sum % MOD + MOD) % MOD;
p = p * -1;
}
sum = (3 * sum + sum1) % MOD;
cout << (-sum + MOD) % MOD;
return 0;
}

PS:fi,0 也可以进行二项式反演优化,但是我懒。。。

posted @   2017BeiJiang  阅读(98)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示