计数
基本概念
容斥
容斥解决满足多条件的组合方案数问题,将各个条件用集合的形式表达,即可套用容斥原理。
-
\(\left| U \right|\) :全集。
-
\(\left| S_i \right|\) :满足条件 \(S_i\) 的方案数。
-
\(\left| \bigcup_{i=1}^{n} S_i \right|\) :满足任一条件的方案数。
-
\(\left| \bigcap_{i=1}^{n}S_i \right|\) :满足全部条件的方案数。
两个公式:
-
\[\left| \bigcup_{i=1}^{n} S_i \right|=\sum_{j=1}^{n}(-1)^{j-1}\times \sum_{a_i<a_{i+1}}\left| \bigcap_{i=1}^{j}S_{a_i} \right| \]
-
\[\left| \bigcap_{i=1}^{n}\right| S_i=\left| \bigcup \right|-\left| \bigcup_{i=1}^{n}\bar{S_i} \right| \]
二项式反演
二项式反演一般用于转化恰好和至多(少)之间的数量关系。
常见形式:
-
\[f_i=\sum_{i=0}^{n}(-1)^i\;\times \;C_{n}^{i}\;\times\;g_i\iff g_i=\sum_{i=0}^{n}(-1)^i\;\times \;C_{n}^{i}\;\times\;f_i \]
-
\[f_n=\sum_{i=0}^{n}C_n^i\times g_i \iff g_n=\sum_{i=0}^{n}(-1)^{n-i}\times C_n^i\times f_i \]
高维二项式反演:
-
\[f_{x,y}=\sum_{i=x}^{n}\sum_{j=y}^{m}C_i^xC_j^y\times g_{i,j}\iff g_{x,y}=\sum_{i=x}^n\sum_{j=y}^m(-1)^{i+j-x-y}C_i^xC_j^y\times f_{i,j} \]
-
剩下以此类推。
恰好与至多的转化
设 \(f_k\) 表示至多满足 \(k\) 个条件,\(g_k\) 满足恰好 \(k\) 个条件,由二项式反演,得:
恰好与至少的转化
设 \(f_k\) 为至少满足 \(k\) 个条件,\(g_k\) 满足恰好 \(k\) 个条件,由二项式反演,得:
如果题目要求恰好的数量,要么直接求(DP)很好求,要么间接求再用二项式反演求很好求。
错排问题
问题:某人写了 \(n\) 封信和 \(n\) 个信封,如果所有的信都装错了信封。求所有信都装错信封共有多少种不同情况。
证明略。递推式如下:
整除分块
一般解决形如一下的问题:
可以证明,在第一个使 \(\left\lfloor \frac{n}{i} \right\rfloor\) 取不同值的 \(i\) 时,
时,\(\left\lfloor \frac{n}{i} \right\rfloor\) 的取值相等。
又可以证明,这样的块不超过 \(2\sqrt{n}\) 个,于是可以在根号时间复杂度内算出值。
题目选做
数论相关
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\) 的约数个数。将要求的式子转化为:
其中,\(\sum_{i=0}^{r}f(i)\) 等价于 \(\sum_{i=0}^{r}\left\lfloor \frac{n}{i} \right\rfloor\) ,可以使用整除分块。
3.[POI2007]ZAP-Queries
求以下式子:
转化为:
正难则反,可以用容斥的思想做,转化为:
即:
莫比乌斯函数即为容斥系数。
因为这个式子就是整除分块的基本式,所以将 \(\mu(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
求下列式子的值:
通过简单容斥,转化为:
类似于矩阵前缀和的形式,可以画出矩阵更加直观的理解。
那么问题就转化成了求
的值,就是上一题
5.四元组统计
正难则反。所有四元组有 \(C_{n}^{4}\) 个,用容斥减去不合法的方案数,也就是 \(gcd\;!=1\) 的方案数,设 \(f(i)\) 表示至少 \(gcd=i\) 的四元组数,答案即为:
考虑求出 \(f(i)\) ,可以统计因数 \(i\) 在数列每个数中是否出现,记 \(sum(i)\) 表示因数 \(i\) 出现在数列中数字的个数,那么:
考虑求出 \(sum(i)\) ,暴力一点的想法就是直接枚举每个数的因数,时间复杂度为 \(O(Tn \sqrt a)\),极限数据恰好是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\) ,将 \(\frac{n}{x}\) 拆分成 \(f\) 份,那么这样拆分出的 \(a_i\) 必然满足有 \(x\) 这个因数。
然后考虑容斥,在根号时间内枚举 \(n\) 的因数,然后用莫比乌斯函数做容斥系数。即:
8.SQFREE - Square-free integers
直接容斥,求式子:
9. List Of Integers
先来考虑这样一个问题:求 \(1\sim x\) 有多少个数与 \(p\) 互质。
问题可以转化为求如下式子的值:
这个式子是我们的老熟人了,直接套容斥。即求如下式子的值:
回到原问题,我们可以二分要求的数,那么 \(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]分特产
正难则反。
\(f_i\) 表示钦定(至少)前 \(i\) 个人没有分到特产,其他的放任自流的方案数。
\(g_i\) 表示恰好 \(i\) 个人没有分到特产的方案数。
易得,\(g_0\) 就是答案,由二项式反演,得:
考虑如何求 \(f\) 数组。
要求 \(f_i\) ,只需考虑剩下 \(n-i\) 个人的取特产情况,考虑将每个特产依次分给这 \(n-i\) 个人,由于不一定每个人都要分到,所以问题就等价于允许有空盒的不同盒子放相同球的模型,于是得到如下式子:
然后再用上面的式子求出 \(g_0\) 即为答案。
2.Cheerleaders
正难则反。
\(f_i\) 表示钦定 \(i\) 条边没有人占,其余放任自流的方案数。
容易得出全集即为 \(f_0=\dbinom{n\times m}{k}\)
考虑求出 \(f\) 数组,
当 \(i=1\) 时,除去一条边不能占,那么就有 \(m\times(n-1)\) 或者 \(n\times (m-1)\) 的区域是爱占不占的,于是:
当 \(i=2\) 时,除去两条边不能占,那么就有 \(n\times(m-2)\) 或 \(m\times(n-2)\) 或 \((n-1)\times (m-1)\) 的区域是爱占不占的,于是:
同理可得,当 \(i=3\) 和 \(i=4\) 的方案数:
套用容斥可知答案为:
3. [JSOI2015]染色问题
正难则反。
咱这有个不需要脑子的二项式反演做法。。。
\(f_{i,j,k}\) 表示钦定 \(i\) 行,\(j\) 列一个没染,\(k\) 个颜色一个没用,其余放任自流的方案数。
\(g_{i,j,k}\) 表示恰好 \(i\) 行,\(j\) 列一个没染,\(k\) 个颜色一个没用的方案数。
易得:
再由高维二项式反演得:
由于答案就是 \(g_{0,0,0}\) ,所以:
若直接求这个式子的值,时间复杂度为 \(O(nmc\times log(nm))\) ,非常悬。这类柿子一般用二项式定理优化,发现 \((c-k+1)\) 的指数 \((n-i)(m-j)\) 与 \(k\) 一点关系没有,于是将 \(\sum_{k=0}^{c}\) 拉到最外面枚举,得:
由二项式定理,得:
时间复杂度降为 \(O(nc\times log(m))\)
4.Sky Full of Stars
正难则反。。。
咱又有一个不需要脑子的高维二项式反演做法。。。
\(f_{i,j}\) 表示钦定 \(i\) 行 \(j\) 列颜色相同,其余的放任自流的方案数。
\(g_{i,j}\) 表示恰好 \(i\) 行 \(j\) 列颜色相同的方案数。
那么 \(Ans=\left| \bigcup \right|-g_{0,0}\) ,\(\left| \bigcup \right|=3^{n\times n}\) 。
由二维二项式反演可得:
考虑求出 \(f\) 数组。
有一个显然的性质:如果至少有 \(1\) 行 和 \(1\) 列满足题目要求,那么满足要求的这些行列一定是同种颜色的。如果只有行满足要求,那么行之间就不一定要同种颜色;列也是同理。
于是很快可以求出:
由于 \(f_{i,0}\) 和 \(f_{0,j}\) 都可以在线性时间内求出来,而 \(f_{i,j}\) 不行,所以将 \(f_{i,j}\) 单独拎出来考虑。
注意到 \(f_{0,0}=3^{n\times n}\) ,所以 \(Ans=-3\times \sum_{i=1}^{n}(-1)^i\times\dbinom{n}{i}\times ((3^{n-i}-1)^n-(3^{n-i})^{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:\(f_{i,0}\) 也可以进行二项式反演优化,但是我懒。。。