You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1110 lines
44 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 容斥原理专题
### 一、模板题
#### [$AcWing$ $890$. 能被整除的数](https://www.cnblogs.com/littlehb/p/15389237.html)
**题意**:给定一个质数序列,求$1 \sim n$中被质数序列中 **至少一个整除** 的整数有多少个。
**解题思路**
① 至少被一个质数整除,正向思考就是:被$1$个除,被$2$个除,被$3$个除,...,
用测试用例举栗子:
```cpp {.line-numbers}
10 2
2 3
```
表示$n=10$,质数序列为`2 3`,那么:
$2$能够被质数$2$整除
$3$能够被质数$3$整除
$4$能够被质数$2$整除
$6$能够被质数$2,3$整除
$8$能够被质数$2$整除
$9$能够被质数$3$整除
$10$能够被质数$2$整除
看了一下,感觉$6$有点特殊:别人都是被$1$个质数整除,只有它是被$2$个质数整除。
为什么呢?思考一下,原来是因为$6=2\times 3$,也就是$6$中同时有$2$和$3$两个质数因子,而其它的只有一个质数因子。
② 这么一个个的检查怕是不行,因为$n,p_i<=10^9$,这样遍历一次就$TLE$了,需要有一个快速些的算法。那有什么样的办法能够做到快点计算出一个区间内有多少个$2$,$3$,$x$...的倍数呢?这是一个小学数学问题,遇到事决,小学数学:
<font color='red' size=4><b>$[1 \sim n]$中被$p$整除的数个数= $⌊\frac{n}{p}⌋$ </b></font>
> **注**:这里可不是说质数,比如我们想计算$10$以内$6$的倍数,就是$10/6=1$,这个$1$需要减去~
有了这个办法,就可以快速计算了,但问题马上又来了:
因为$6$即能被$2$整除,也能被$3$整除,这样就会被计算$2$次!
$⌊\frac{10}{2}⌋+⌊\frac{10}{3}⌋=5+3=8$
而我们用纸笔一个个查出来的是$7$个,这样算的结果多出来$1$个,很显然,就是因为$6$被记录了两次,需要把记录两次的扣除掉一次,噢,这是容斥原理啊!如果某个数是三个质因子的倍数,那么还需要加上它,....
② **容斥原理算法步骤**
质因子站好排后,使用二进制数位枚举来每个质因子是不是选择了,这样可以穷举所有的可能 ,需要注意的是二进制需要从$1$开始枚举,一直到$2^m-1$。
> **注**$m$为质因子的个数,$2^m-1$就是共$m$位,每个位置上都是数字$1$,表示全部选中。根据选择了因数数量的奇偶性,决定它的加或减。
> 举栗子:
> $2^2-1=4-1=3=(11)_2$
> $2^3-1=8-1=7=(111)_2$
> $2^4-1=16-1=7=(1111)_2$
> $2^5-1=32-1=7=(11111)_2$
> **注意**:在枚举过程中,如果出现$2\times 3 \times 7 \times 11=462$,如果$n=100$,那么这组解无效,因为超过了极限值嘛,所以此时组合选择需要放弃掉。
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
int n, m; // n:质数个数m:1~m的数字中有多少个可以被质数序列中至少一个整数整除。
// 注意代码里的n,m与模板题目中的含义相反一定要注意!!!!!!!!!!!!
vector<int> p; // 质数数组
signed main() {
cin >> m >> n; // 与m互质,n个质数
// 读入n个质数,为了使用vector<int>,读入时确实不太方便
for (int i = 0; i < n; i++) {
int x;
cin >> x;
p.push_back(x);
}
// ① 枚举从1到 2^n-1,每个数字,代表一种状态,每个状态代表一种质数的挑选办法
// 当然这些整数值的乘积可能大于n,大于的没用只要小于等于n的
int s = 0;
for (int i = 1; i < 1 << p.size(); i++) {
int t = 1, cnt = 0; // 累乘积,质因子个数
// ② 在对应的整数值确定后,枚举此数值的每一个数位
for (int j = 0; j < p.size(); j++)
if (i >> j & 1) { // ③判断当前数位是不是1,是1表示当前数位选中
if (t * p[j] > m) { // 乘积不能超过最大值m控制在[1~m]范围内
t = 0; // s=0代表本次挑选的组合失败无效
break; // 由于i是由小到大遍历的前面的都无效了后面的肯定更大更无效不用继续了
}
cnt++; // 选择的质因子个数
t *= p[j]; // 累乘积
}
if (t) { // 超过范围的s=0,所以,现在代表只讨论在范围内的
if (cnt & 1) // 质数因子数量,奇数加
s += m / t; // 引理内容代表m里面有多少个这个数字s的倍数
else // 偶数减
s -= m / t;
}
}
cout << s << endl;
}
```
### 二、题单
### [$HDU4135$ $Co-prime$](https://acm.hdu.edu.cn/showproblem.php?pid=4135)
**题意:**
给三个数$a、b$ 和 $m$,求区间$[a,b]$中与$m$互质的数的个数。
$1 <= a <= b <= 10^{15}$$1 <=m <= 10^9$。
**解析**
这道题与模板题不太一样,加入了一些变化,分析一下:
先从这句话下手:**与$m$互质**
答:就是把$m$分别质因数后,形成一个质数序列$p_1,p_2,...,p_x$,然后我们可以根据模板题,
① 求出来$1 \sim a-1$之间的所有数被$p_1,p_2,...,p_x$中的至少一个数整除的整数有多少个。
② 求出来$1 \sim b$之间的所有数被$p_1,p_2,...,p_x$中的至少一个数整除的整数有多少个。
二者之差(**类似前缀和**)就是这个区间$[a,b]$之间与$m$不互质的数的个数
互质的个数等于总数减去不互质的个数就行啦~
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
vector<int> p; // 质数数组
int cal(int n, int m) { // 返回1-m中与n互素的数的个数
p.clear(); // 多组测试数据,清空
// 分解质因数
for (int i = 2; i * i <= n; i++) { // 对n分解质因数
if (n % i == 0) {
p.push_back(i);
while (n % i == 0) n /= i;
}
}
if (n > 1) p.push_back(n); // 最后一个大因子,也加入
int s = 0; // 1到m中与n不互素的数的个数
// 枚举子集不能有空集所以从1开始
for (int i = 1; i < 1 << p.size(); i++) { // 1枚举到(2^素因子个数)
int cnt = 0;
int t = 1;
for (int j = 0; j < p.size(); j++) { // 枚举每个素因子
if (i & (1 << j)) { // 有第i个因子
cnt++; // 计数
t *= p[j]; // 乘上这个质因子
}
}
// 容斥原理
if (cnt & 1) // 选取个数为奇数,加
s += m / t;
else // 选取个数为偶数,减
s -= m / t;
}
return m - s; // 返回1-m中与n互素的数的个数
}
signed main() {
int T, ca = 0;
cin >> T;
while (T--) {
int m, a, b;
cin >> a >> b >> m; // 求区间[a,b]中与m互素的数字个数
// 计算[1,a-1]之间与m互素的个数
// 计算[1, b]之间与m互素的个数
int ans = cal(m, b) - cal(m, a - 1);
printf("Case #%d: %lld\n", ++ca, ans);
}
}
```
### [$HDU2841$ $Visible$ $Trees$](http://acm.hdu.edu.cn/showproblem.php?pid=2841)
### 题意
给一个$n*m$的矩阵,左下角为$(1,1)$,右上角为$(n,m)$,问农民在$(0,0)$点可以看到多少个点。
### 分析
**解题思路**
如果$(0,0) \rightarrow (x,y)$和$(0,0)\rightarrow(x,y)$两个向量共线,即$(0,0),(x,y),(x,y)$三点共线,那后面的那个点$(x,y)$就被挡住看不到了。
如果三点共线,那么向量$(x,y)$一定可以表示成$(x'=kx,y'=ky)$(其中$k \in Z^+$且$k>1$,$kx<=n,ky<=m$),因此对于一个数对$(xy)$,如果它们存在公因数,那么**一定可以化简成最简,即互质的形式**,那么这个互质的数对构成的向量应该是和原向量共线的,因此我们 <font color='red'><b>只能看到最前面那个互质的数对构成的点,其它不互质的都会被它前面的某个互质的挡住</b></font>。
#### 因此题目转变成求区间$[1,m],[1,n]$之间互质数的**对数**
**求解办法**
选取一个区间(**为了优化选取小区间**)比如说选取$[1,n]$,枚举$n$里面的数$i$,然后对于每个数$i$我们看它在$[1,m]$区间内能找到多少互质的数,**把这些结果全部累加起来即可**。也就是枚举一个小区间中的所有数,然后转化为: <font color='red' size="4"><b>求$1\sim n$中与$m$互素的数的个数</b></font>
> **注**:通常情况下,小的循环在外层,大的循环在内层会更有利于性能。这是因为内存访问模式对性能有重要影响,将内存中的数据按照连续的顺序访问会更高效。当小的循环在外层时,内存中的数据更有可能按照连续顺序被访问,这有助于提高缓存的命中率,从而提高性能。
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int N = 1e6 + 10;
// 返回1-m中与n互素的数的个数
vector<int> p;
int cal(int n, int m) {
p.clear();
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
p.push_back(i);
while (n % i == 0) n /= i;
}
}
if (n > 1) p.push_back(n); // 求n的素因子
int s = 0; // 1到m中与n不互素的数的个数
// 枚举子集不能有空集所以从1开始
for (int i = 1; i < 1 << p.size(); i++) { // 从1枚举到(2^素因子个数)
int cnt = 0;
int t = 1;
for (int j = 0; j < p.size(); j++) { // 枚举每个素因子
if (i & (1 << j)) { // 有第i个因子
cnt++; // 计数
t *= p[j]; // 乘上这个质因子
}
}
// 容斥原理
if (cnt & 1) // 选取个数为奇数,加
s += m / t;
else // 选取个数为偶数,减
s -= m / t;
}
return m - s; // 返回1-m中与n互素的数的个数
}
signed main() {
int T;
cin >> T;
while (T--) {
int a, b;
cin >> a >> b;
int res = 0;
// 从1~n(b现在就是n)之间找到所有与m(m现在就是i)互质的数字个数
for (int i = 1; i <= a; i++) res += cal(i, b);
printf("%lld\n", res);
}
}
```
### [$HDU1695$ $HDU$ $1695$ $GCD$(容斥原理)](https://acm.hdu.edu.cn/showproblem.php?pid=1695)
**题意**
给了 $a、b、c、d、k$ 五个数 求$gcd(x,y)=k$的 **对数**, 其中 $a<=x<=b ,c<=y<=d$ 并且所有数据的$a$和$c$都是$1$。
**分析**
$gcd(x,y)=k \rightarrow gcd(x/ky/k)=1 (1<=x<=b/k, 1<=y<=d/k)$ 也就是求互质的对数
那么,我们可以去枚举$x$的范围去算$y$中与$x$互质的个数,但 **为了避免重复的情况** 比如`1 3` 和`3 1`算一对 那么我们就假定$x<y$ 所以我们算的是
<font color='red' size=4><b>大于$x$,小于等于$y$,并且与$x$互质的个数</b></font>
用容斥原理算出大于$x$小于等于$y$的数中是$x$的质因数的倍数的个数$sum$, 然后$y-x-sum$就是$x$与$1 \sim d/k$中互质的对数。
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
// 返回1-m中与n互素的数的个数
vector<int> p;
int cal(int n, int m) {
p.clear();
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
p.push_back(i);
while (n % i == 0) n /= i;
}
}
if (n > 1) p.push_back(n); // 求n的素因子
int s = 0; // 1到m中与n不互素的数的个数
// 枚举子集不能有空集所以从1开始
for (int i = 1; i < 1 << p.size(); i++) { // 1枚举到(2^素因子个数)
int cnt = 0;
int t = 1;
for (int j = 0; j < p.size(); j++) { // 枚举每个素因子
if (i & (1 << j)) { // 有第i个因子
cnt++; // 计数
t *= p[j]; // 乘上这个质因子
}
}
// 容斥原理
if (cnt & 1) // 选取个数为奇数,加
s += m / t;
else // 选取个数为偶数,减
s -= m / t;
}
return m - s; // 返回1-m中与n互素的数的个数
}
int T, ca;
signed main() {
cin >> T;
while (T--) {
int a, b, c, d, k;
cin >> a >> b >> c >> d >> k;
// k=0时需要特判因为我们想要 x'=x/k ,y'=y/k,不能随意除,需要判断
if (k == 0) { // k=0时表示gcd(x,y)=0
/*
如果两个数的最大公约数是0这意味着这两个数中至少有一个数是0。
因为最大公约数是两个数共有的最大因子而0没有最大因子所以0
与任何数的最大公约数都是0。
而a<=x<=b,c<=y<=d,a=c=1,所以k=0是不可能存在gcd(x,y)=0的应该返回0对
*/
printf("Case %lld: 0\n", ++ca);
continue;
}
b /= k, d /= k;
// 因为 (13)与 (3,1)算1个所以要限制x<y
// a=c=1
if (b > d) swap(d, b);
int ans = 0;
// d>b
for (int i = 1; i <= d; i++) // 枚举大区间
// c(n,m): 返回1-m中与n互素的数的个数
// 拿大区间[1~d]中的每个数字i去 [1~b]中找与其互质的数
// 但是,这样做的话,会出现 [1,3],[3,1]这样的情况,为了防止这样的事情发生
// 我们需要控制区间的范围也就是小于等于i,同时也要考虑i与b的大小关系保证i<=b
// 也就是 min(i,b)
ans += cal(i, min(i, b));
printf("Case %lld: %lld\n", ++ca, ans);
}
}
```
#### [$AcWing$ $214$. $Devu$和鲜花](https://www.cnblogs.com/littlehb/p/16378751.html)
> **思路脉络**
① 隔板法,每组内允许为$0$个,也就是隔板法扩展
② 但每个分组中最高的数量有限制!
③ 正难则反,先用隔板法扩展求一下每个小组可以为$0$的所有方案数
④ 总的方案数减去每个盒子中不满足条件的并不是答案,因为可能减多了,联想到容斥原理。
⑤ $S_i$代表每组中至少取出$a_i+1$朵花,那就先拿走这些,然后考虑$m-(a_i+1)$朵花中分成$n$组、并且每组数量可以为$0$的方案数,这是经典的隔板扩展法,然后再把拿走的$a_i+1$朵花拿回来到$i$组中,有原来的情况一致。
⑥ 与上一题的区别在于:上一题必须保证有一个及以上的因子,也就是`for (int i = 1;i < 1 << n; i++)`,本题可以一个都不选,即:`for (int i = 0;i < 1 << n; i++)`
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int N = 20, mod = 1e9 + 7;
int A[N];
int n, m;
// 快速幂
int qmi(int a, int k) {
int res = 1;
while (k) {
if (k & 1) res = res * a % mod;
a = a * a % mod;
k >>= 1;
}
return res;
}
int C(int a, int b) {
if (a < b) return 0;
int up = 1, down = 1;
for (int i = a; i > a - b; i--) up = i % mod * up % mod;
for (int i = 1; i <= n - 1; i++) down = i * down % mod; //(n-1)! % mod
down = qmi(down, mod - 2); // 费马小定理求逆元
return up * down % mod; // 费马小定理
}
signed main() {
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> A[i]; // 第i个盒子中有A[i]枝花,限制条件
int res = 0;
for (int i = 0; i < 1 << n; i++) { // 容斥原理的项数0000 代表一个限制条件都没有, 0001代表第1个限制条件生效0011,代码第12个限制条件生效
int a = m + n - 1, b = n - 1; // 上限是m+n-1,下限不一样
int sign = 1; // 奇数个限制条件,需要减;偶数个限制条件,需要加。现在这种限制条件组合状态,是奇数个限制,还是偶数个限制?
for (int j = 0; j < n; j++) // 枚举状态的每一位
if (i >> j & 1) { // 如果此位是1
sign *= -1; // 符号位变号从1变为-1或者从-1变为1; 这个用法挺牛X的漂亮的实现了奇数次负号偶数次正号的维护
a -= A[j] + 1; // 拼公式
}
res = (res + C(a, b) * sign) % mod;
}
cout << (res + mod) % mod << endl;
}
```
## [$HDU3501$ $Calculation$ $2$](http://acm.hdu.edu.cn/showproblem.php?pid=3501)
**等差数列**
是指从第二项起,每一项与它的前一项的差等于同一个常数的一种数列,常用$A、P$表示,这个常数叫做等差数列的公差,公差常用字母$d$表示。例如:$135792n-1$。通项公式为:$a_n=a_1+(n-1)*d$。首项$a_1=1$,公差$d=2$,前$n$项和公式为:
$$\large S_n=n*(a_1+a_n)/2 $$
> **注意**$n \ \in Z^+ $
**公式推导**
$\large S_n=a_1+a_2+a_3+...+a_n$。
把上式倒过来得:$\large S_n=a_n+a_{n-1}+a_2+a_1$。
将以上两式相加得:$\large 2S_n=(a_1+a_n)+(a_2+a_{n-1})+...+(a_n+a_1)$。
由等差数列性质:若$m+n=p+q$则$a_m+a_n=a_p+a_q$得
$$\large 2S_n=n(a_1+a_n)$$
> **注**:括号内其实不只是$a_1+a_n$满足只要任意满足下角标之和为$n+1$就可以两边除以$2$得$s_n=n(a_1+a_n)/2$ ①
**题意**
输入$n$,计算比$n$小的数中,和$n$不互质的数的和 $\%1000000007$
**解题思路**
求小于$N\ (1 \sim N-1)$的数字中与$N$ **不互质** 的数的 **加法和**。
与$N$不互质,就与$N$有相同因子,首先将$N$因式分解,找出所有的质因子。
对于累乘积因子$2$,有$2,4,6,8,……$
对于累乘积因子$3$,有$3,6,9,12,……$
对于累乘积因子$6$,有$6,12,18,……$
也就是需要加上$2$的那一坨,加上$3$的那一坨,再减去$6$的那一坨,...
而这些坨,都是等差数列,可以用 **等差数列求和公式** 求解。
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int mod = 1000000007;
signed main() {
int m;
while (cin >> m && m) {
if (m == 1) {
cout << "0" << endl;
// 边界需要特别注意本题的左边界是1
// 表示的含义是小于1并且与1不互质的数的加法和当然是0。
// 在做题时,先想正常思路,然后再思考一下边界是不是用特判。
continue;
}
int n = m; // 复制出来进行质因数分解
// 分解质因数
vector<int> p;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
p.push_back(i);
while (n % i == 0) n = n / i;
}
}
if (n > 1) p.push_back(n);
// 容斥原理
int s = 0;
for (int i = 1; i < (1 << p.size()); i++) {
int cnt = 0;
int t = 1;
for (int j = 0; j < p.size(); j++) {
if (i >> j & 1) {
cnt++;
t *= p[j];
}
}
/* t:质因子的累乘积
cnt:质因子的个数
举栗子:
枚举到的组合中只有数字2,那么246810....需要加上
枚举到的组合中只有数字3,那么3691215....需要加上
枚举到的组合中中6.有数字23,那么61218....需要减去
*/
int num = (m - 1) / t; // 题目要求比m小的数字中也就是[1~m-1]这些数字中有多少个数是t的倍数呢?是(m-1)/t个。
/* 这些数字,首个是t,第二个是t*2,最后一个是t*num
等差数列求和公式:(首项+末项)*项数/2
模板题是计数,这里不是计数,而是计算这些数加在一起是多少,不能傻傻的一个个累加,而是采用了数学中的等差数列求和公式,
否则聪明的高斯该生气了~
*/
int tmp = (t + t * num) * num / 2;
if (cnt & 1) // 奇数的加
s += tmp;
else // 偶数的减
s -= tmp;
}
cout << s % mod << endl; // 取模输出
}
}
```
### [$HDU$$ 1796$ $How$ $many$ $integers$ $can$ $you$ $find$](https://acm.hdu.edu.cn/showproblem.php?pid=1796)
**题意**
给你一个整数$N$。和$M$个整数的集合{$A_1、A_2、…、A_m$}。集合内元素为 **非负数** (包括零),求小于$N$的
正整数($1\sim N-1$)中,能被$M$个整数的集合中随意一个元素整除的正整数个数。
比如$N = 12,M = {23}$,在$1\sim N-1$中,能被$2$整除的数为{$2,4,6,8,10$},能被$3$整除的数为{$3,6,9$}。则所求集合为{$234。68910$},共$7$个,则答案为$7$。
**解题思路**
就是求$M$个集合的并集。先看上边的样例。能被$2$整除的数集合$S_1$为{$2,4,6,8,10$},能被$3$整除的数
集合$S_2$为{$3,6,9$}。而两者交集$S_{12}$为能被$LCM(23) = 6$整除的数为{$6$}。
则两者并集 $S = S_1 + S_2 - S_{12}$。
依据容斥定理得:若有$M$个数,则可求出$1~N-1$中能被不同组合的公倍数整除的个数。
$1\sim N-1$能被公倍数整除的个数为 $(N-1) / LCM$。然后依据奇数项加,偶数项减的原则得出答案个数。
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int N = 15;
vector<int> p;
// 最小公倍数
int lcm(int a, int b) {
return a * b / __gcd(a, b);
}
signed main() {
int n, m;
while (cin >> m >> n) { // n个数求[1~m)中有多少个数能被整除集合中的数字整除
m--; // small than m 注意细节
p.clear(); // 多组测试数据注意清0
for (int i = 0; i < n; i++) { // n个数字组成的序列
int x;
cin >> x;
if (x) p.push_back(x); // 排除掉数字0,0不能做除数
}
int s = 0;
for (int i = 1; i < (1 << p.size()); i++) {
int cnt = 0;
int t = 1;
for (int j = 0; j < p.size(); j++) {
if (i >> j & 1) {
cnt++;
t = lcm(t, p[j]); // 这里不是简单的相乘,而是求的最小公倍数
}
}
if (cnt & 1)
s += m / t;
else
s -= m / t;
}
cout << s << endl;
}
}
```
### [$HDU$ $2204$ $Eddy's$爱好]((https://acm.hdu.edu.cn/showproblem.php?pid=2204))
#### 题意
如果一个数能表示为$M^k$,那么这个数是好数,问你$1 \sim n$有几个好数。
#### 引理
$100$以内是某个数的$2$次方的数有多少个?怎么计算的?
对于这个问题,我们可以针对$100$以内的每个数进行计算,看其是否是某个数的$2$次方。但是,如果我们只是想要知道$100$以内有多少个数是某个数的$2$次方,我们可以 <font color='red' size=4><b> 先对$100$进行开方,然后向下取整</b></font>,得到的结果就是$100$以内的$2$次方数的个数。在这种情况下,$100$的开方是$10$,向下取整后得到$10$,这意味着$100$以内有$10$个数是某个数的$2$次方。
$100$以内是某个数的$2$次方的数包括:$0, 1, 4, 9, 16, 25, 36, 49, 64$, 和 $81$。
#### 思路
如果我们直接按这个计算,会发现有很多重复,比如有个数是$2$的次方,也有可能是$4$的次方,也有可能是$6$的次方,但是这些其实在$2$的次方中就已经计算了,而$48$是$2$的倍数,所以我们只需要 **管素数次方** 就行了。
那是不是这样就没有重复了?也不是,不同数之间有可能也有,就比如一个数是$6$的次方,他就被素数$2$次方和素数$3$次方同时给统计了,这个要 **用到容斥**,比如$27^2=729$和$9^3=729$会重复,是因为他们都能凑出指数$6$,也就是$27^2=3^{3^2}=3^6,9^3=3^{2^3}=3^6$所以我们需要减去指数$6$所得到的个数。
我们用$n$计算出指数为$6$的个数,$729^{1/6}=pow(729,1.0/6)=1$
答案就是先+指数为$2$的,再+指数为$3$的,最后减指数为$6$的
这就是容斥原理的经典套路了,加奇数个的,减偶数个的。
#### 指数范围
**$Q1$:最多多少个质数因子?**
**答** 因为$2$是最小的质数因子,它的$60$次方 = $2^{60}>10^{18}$,也就是说,即使是最小的质数,它的$60$次方都超过了题目数据上限,所以,指数最大为$60$足够所有情况使用,打表$60$以内的素数,然后容斥。
**$Q2$:最多容斥多少个质数?**
**答**$2*3*5*7>60$,我们挑选最小的$4$个质数因子,就会超过上限$60$!所以,最多只有三个质数因子,不会有$4$个及以上的质数因子个数。
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
// 60以内的质数列表
int p[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59};
signed main() {
int n;
while (cin >> n) {
int s = 1, t; // s=1表示最起码数字1符合要求
for (int i = 0; i < 17; i++) { // 三层循环遍历质数数组p枚举第一个因子
t = pow(n, 1.0 / p[i]) - 1; // 计算出指数=p[i]的数的个数,需要减去1见下面的注释解释
if (!t) break; // 扣除完1^2后没有了那太白扯了
/*
int n = 10;
cout << (int)pow(n, 1.0 / 2) << endl;
输出3理解一下
10以内可以描述为x^2的有1^2 2^2 3^2
这样就是说上面的算法把数字1记录在内了。
*/
s += t; // t个但数字1不能考虑
for (int j = i + 1; j < 17; j++) { // 三层循环,枚举第二个因子,避让第一个因子
t = pow(n, 1.0 / (p[i] * p[j])) - 1; // 计算出指数=p[i]*p[j]的数的个数
if (!t) break; // 扣除完1^2后没有了那太白扯了
s -= t; // 两个的要减
for (int k = j + 1; k < 17; k++) { // 三层循环,枚举第三个因子,避让第一、第二个因子
t = pow(n, 1.0 / (p[i] * p[j] * p[k])) - 1; // 计算出指数=p[i]*p[j]*p[k]的数的个数
if (!t) break; // 扣除完1^2后没有了那太白扯了
s += t; // 三个的要加
}
}
}
cout << s << endl;
}
}
```
### [$HDU$ $4407$ $Sum$](https://acm.hdu.edu.cn/showproblem.php?pid=4407)
#### 题意
给一个长度为$n$的序列,初始值为$1 \sim n$;
对序列有以下两种操作;
1.查询$[x,y]$内与$p$互素的数的和;
2.修改第$x$数为$c$;
共$m$,($1 \leq m \leq 1000$)次操作。
#### 解题思路
如果我们先忽略操作$2$带来的影响,可以通过求$calc(y)-calc(x-1)$这种类似于前缀的方法来得出答案,其中$calc(y)$的含义就是计算从$1\sim y$之间与$p$互质的数字和,办法当然就是用补集思想先算总和=$sum(1 \sim y)$,然后再计算出与$p$不互质的数字有哪些,用总和减去不互质的数字和,就是互质的数字和。
本题的数据比较特殊,是$[1\sim n]$序列,是一个等差数列。
经验告诉我们,需要对$p$先分解质因数,由于我们一般喜欢用`vector<int> p`来保存质因数序列,所以我管原来的输入$p$改名为$P$,小$p$变量名留给质因数序列数组。
一眼就看出来本题需要容斥原理+二进制枚举组合
利用二进制法枚举每种可能的质数因子组合,举栗子:$2,3,5$
本次枚举的是数字$3$,即$(11)_2$,得到质数组合$(2,3)$
取乘积 $2*3=6$
此时 $t=6,n/t = n/6$:计算$[1 \sim n]$里面$6$这个因子的倍数有多少个,比如有$3$个
即:$6,12,18$
我们需要把这$3$个数字和加到一起,$(6+12+18)$,就是$(1+2+3)*6$
即:$n * (n + 1) / 2 * d$;
能不能证明一下这个公式呢?
我们先看基础公式$S_n=(a_1+a_n)*n/2$
在本题中,$a_1=d,a_n=a1+(n-1)*d$
代入 $S_n=(d+d+(n-1)*d)*n/2=d*(2+n-1)*n/2=(n+1)*n/2*d$
**证毕**
**$Q$:修改怎么办?**
本来是一个$1\sim n$的序列,这题目挺坏啊,将第$x$位的值修改为$c$ ,这样,本来依靠等差数列求和之类的东西就无法使用了!
所以,办法就是 **先写基本盘****再做修改盘** 的补充
> **注**: 为$m$范围只到$1000$,所以暴力不超时
**步骤**
① 遍历每个查询范围内的修改,如果原值与$P$互质,则减掉该数值
② 如果修改的新值与$P$互质,则加上该数值。
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int N = 400010;
unordered_map<int, int> mp; // 能用Hash表不用红黑树
vector<int> p; // 将m拆分成的质数因子序列p
/*
等差数列求和=(首项+末项)*项数/2
本题中:首项=t,项数=n,公差=t
举栗子3,6,9 是一个等差数列,公差=3
我们可以这样看(1,2,3)*3,也就是先计算(1,2,3)的和再乘以3=(1+3)*3/2*3=18
*/
int get(int t, int n) {
return n * (n + 1) / 2 * t;
}
// 容斥原理+等差数列加法和
// 求解[1,n]中与c互素的所有数字的和
// = [1,n]的数字总和 - [1,n]中所有与c存在公共因子的数的和
int calc(int n) {
int sum = get(1, n); // [1,n]的数字总和
int s = 0;
for (int i = 1; i < (1 << p.size()); i++) {
int cnt = 0, t = 1;
for (int j = 0; j < p.size(); j++) {
if (i >> j & 1) {
cnt++;
t *= p[j];
}
}
if (cnt & 1)
s += get(t, n / t);
else
s -= get(t, n / t);
}
return sum - s; // 总数,减去不互质的,等于,互质的数字加法和
}
signed main() {
#ifndef ONLINE_JUDGE
freopen("HDU4407.in", "r", stdin);
#endif
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
int T; // T组测试数据
cin >> T;
while (T--) {
mp.clear(); // 注意此处,每次测试案例都要清零
int n, m;
cin >> n >> m; // 1~n 共n 个长度的序列
while (m--) { // m个询问
int choice;
cin >> choice; // 操作
int x, y, P;
if (choice == 1) { // 查询[x,y]内与c互素的数的和
cin >> x >> y >> P;
p.clear(); /// 初始化
// 分解质因数
int t = P; // 复制出来
for (int i = 2; i * i <= t; i++) {
if (t % i == 0) {
p.push_back(i);
while (t % i == 0) t = t / i;
}
}
if (t > 1) p.push_back(t);
int sum = calc(y) - calc(x - 1); // 类似于前缀和求[x~y]区间内的等差数列加法和
// 遍历已经录入的所有修改
for (auto it = mp.begin(); it != mp.end(); it++) {
int a = it->first, b = it->second;
// 如果修改的不存本次查询的范围内,改了也不影响我,没用,不理
if (a > y || a < x) continue;
// 本来互素,结果被你改了,那就需要减掉这个数值
if (__gcd(a, P) == 1) sum -= a;
// 修改后互素的要加上新数值
if (__gcd(b, P) == 1) sum += b;
}
cout << sum << endl;
} else {
cin >> x >> P;
mp[x] = P; // 修改序列内容
}
}
}
}
```
### [$UVA$ $11806$ $Cheerleaders$](https://vjudge.net/problem/UVA-11806)
<font color='red' size=4><b>考查了组合数公式、补集思想、容斥原理思想(不拘泥于质数+二进制枚举噢~</b></font>
#### 题意
给定$n、m、k$三个数,$n$代表行数,$m$代表列数,$k$代表人数。
$n*m$的表格,一共有$k$个人,要求:
① 每个小格只能容纳一个人,所有人必须都在表格中。
② **第一行、第一列、最后一行、最后一列必须站人**。
③ **若夹角处站人,则代表此行和列都已站人**。
问:有多少种方法?
#### 解题思路
利用容斥原理,设:
- $S$为总数。($n*m$中选$k$个位置,即$C_{nm}k$
- $A$为第一行没有站人。(少一行)
- $B$为最后一行没有站人。(少一行)
- $C$为第一列没有站人。(少一列)
- $D$为最后一列没有站人。(少一列)
文氏图如下:
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202312140842345.png)
采用 **补集思想**:所有可能的站法 $-$ 所有不可能的站法 $=$ 符合条件的所有站法。
我们要计算的就是,$S$减去$ABCD$覆盖部分。
容斥原理公式表示为:
$ans=S(A+B+C+D)+(AB+AC+AD+BC+BD+CD)(ABC+ABD+ACD+BCD)+(ABCD)$
记:
$s_1=(A+B+C+D)$
$s_2=(AB+AC+AD+BC+BD+CD)$
$s_3=(ABC+ABD+ACD+BCD)$
$s_4=(ABCD)$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int mod = 1000007;
const int N = 410;
int C[N][N];
int n, m, k, ans;
/*
Sample Input
2
2 2 1
2 3 2
Sample Output
Case 1: 0
Case 2: 2
*/
int main() {
#ifndef ONLINE_JUDGE
freopen("UVA11806.in", "r", stdin);
#endif
// 预处理出组合数结果数组
for (int i = 1; i < N; i++) {
C[i][0] = C[i][i] = 1;
for (int j = 1; j < i; j++)
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
}
int T, cas = 1;
int S, s1, s2, s3, s4;
cin >> T;
while (T--) {
ans = 0; // 多组测试数据,每次注意清零
cin >> n >> m >> k; // n行,m列,k个人
if (k == 0) { // 一定要注意边界情况比如0个人
printf("Case %d: 0\n", cas++);
continue;
}
S = C[n * m][k]; // n*m个格子中找出k个格子站人就是所有方案数
/*
S为总数
A为第一行没有站人
B为最后一行没有站人
C为第一列没有站人
D为最后一列没有站人
令:
s1 =(A+B+C+D)
s2=(AB+AC+AD+BC+BD+CD)
s3=(ABC+ABD+ACD+BCD)
s4=(ABCD)
*/
// A:第一行没人即C[(n-1)*m,k]
// B:最后一行没人,即C[(n-1)*m,k]
// C:第一列没人C[n*(m-1),k]
// D:最后一列没人C[n*(m-1),k]
s1 = 2 * (C[n * (m - 1)][k] + C[(n - 1) * m][k]) % mod;
// AB:第一行最后一行没有人行少了2行列不变即C[(n-2)*m,k]
// AC:第一行第一列没有人行少了1行列少了一列即C[(n-1)*(m-1),k]
// AD:第一行最后一列没有人即C[(n-1)*(m-1),k]
// BC:最后一行第一列没有人即C[(n-1)*(m-1),k]
// BD:最后一行,最后一列没有人,即C[(n-1)*(m-1),k]
// CD:第一列,最后一列没有人,即C[n*(m-2),k]
// 中间4个是一样的4*C[(n-1)*(m-1),k]
s2 = (C[(n - 2) * m][k] + 4 * C[(n - 1) * (m - 1)][k] + C[n * (m - 2)][k]) % mod;
// ABC:第一行、最后一行、第一列没有人行少了2行列少了1列即C[(n-2)*(m-1),k]
// ABD:第一行、最后一行、最后一列没有人行少了2行列少了1列即C[(n-2)*(m-1),k]
// ABC+ABD=2*C[(n-2)*(m-1),k]
// ACD:第一行、第一列、最后一列没有人行少了1行列少了2列即 C[(n-1)*(m-2),k]
// BCD:最后一行、第一列、最后一列没有人行少了1行列少了2列即 C[(n-1)*(m-2),k]
// ACD+BCD=2*C[(n-1)*(m-2),k]
s3 = 2 * (C[(n - 2) * (m - 1)][k] + C[(n - 1) * (m - 2)][k]) % mod;
// 第一行第一列最后一行最后一列都不能站那么剩下n-2行m-2列需要在(n-2)*(m-2)这么多的格子里找出k个格子
s4 = C[(n - 2) * (m - 2)][k] % mod;
ans = (S - s1 + s2 - s3 + s4) % mod; // 容斥原理
ans = (ans + mod) % mod; // 防止取余后出现负数
printf("Case %d: %d\n", cas++, ans); // 输出答案
}
return 0;
}
```
### [$ACM-ICPC$ $2018$ 沈阳赛区网络预赛$G-Spare$ $Tire$](https://blog.csdn.net/yz467796454/article/details/82531727)
#### 题意
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202312140922419.png)
**中文题意**
给出$a$的递推式,找出$1$到$n$中与$m$互质的数$i$,求$a[i]$和。
#### 解题思路
**结论**:递推式 $\large a_n = \frac{3a_{n-1} - a_{n-2}}{2} + n + 1$ 的通项公式为 $a_n = n^2 + n$。
可以使用数学归纳法来证明这个结论。
**初值条件**
当 $n = 1$ 时,$a_1 = 1^2 + 1 = 2$,符合通项公式 $a_n = n^2 + n$。
当 $n = 2$ 时,$a_2 = 2^2 + 2 = 6$,符合通项公式 $a_n = n^2 + n$。
**归纳假设**
假设对于所有的 $n \leq m$,递推式 $a_n = \frac{3a_{n-1} - a_{n-2}}{2} + n + 1$ 的通项公式成立,即 $a_n = n^2 + n$。
**归纳步骤**
我们需要证明当 $n = m+1$ 时,通项公式 $a_n = n^2 + n$ 仍然成立。
根据递推式 $a_n = \frac{3a_{n-1} - a_{n-2}}{2} + n + 1$,我们有:
$a_{m+1} = \frac{3a_m - a_{m-1}}{2} + m + 1+1 $
根据归纳假设,将 $a_m$ 和 $a_{m-1}$ 替换为 $(m^2 + m)$ 和 $((m-1)^2 + (m-1))$,得到:
$a_{m+1} = \frac{3(m^2 + m) - ((m-1)^2 + (m-1))}{2} + m + 1+1 \rightarrow$
$a_{m+1}=\frac{3m^2+3m-(m^2-2m+1)-m+1}{2}+m+1+1 \rightarrow$
$a_{m+1}=\frac{3m^2+3m-m^2+2m-1-m+1}{2}+m+1+1 \rightarrow$
$a_{m+1}=\frac{2m^2+4m}{2}+m+1+1 \rightarrow$
$a_{m+1} = m^2 + 2m +m+1+ 1 \rightarrow $
$a_{m+1} = m^2 + 2m +1+m+1 \rightarrow $
$a_{m+1}= (m+1)^2 + (m+1) $
因此,当 $n = m+1$ 时,通项公式 $a_n = n^2 + n$ 仍然成立。
综上所述,根据数学归纳法,递推式 $a_n = \frac{3a_{n-1} - a_{n-2}}{2} + n + 1)$ 的通项公式为 $a_n = n^2 + n$
费了牛劲,终于求得$a_n=n*n+n$,那么前$n$项和$S_n$是多少呢?
即$S_n=a_1+a_2+a_3+...+a_n \rightarrow$
$S_n=1^2+1+2^2+2+3^2+3+...+n^2+n$
拆开来看:
① $1,2,3,...,n$很显然是一个等差数列,
$$\large S_n'=(1+n)*n/2$$
② $1^2,2^2,3^2,...,n^2$这个序列的求和公式是多少呢?
这是自然数平方求和数列,有公式:
$$\large S_n''=n*(n+1)(2*n+1)/6$$
这个东西是怎么来的呢?
**推导过程**
$2³-1³=3×1²+3×1+1$
$3³-2³=3×2²+3×2+1$
$4³-3³=3×3²+3×2+1$
... ...
$(n+1)³-n³=3n²+3n+1$
以上$n$个式子相加,得
$(n+1)³-1=3(1²+2²+3²+...+n²)+3(1+2+3+...+n)+(1+1+1+...+1)$
即$(n+1)³-1=3(1²+2²+3²+...+n²)+3[n(n+1)/2]+n$
令$S''=1²+2²+3²+...+n²$
$3S''=(n+1)³-1-3n(n+1)/2-n$
$3S''=(n+1)³-3n(n+1)/2-(n+1)$
$3S''=(n+1)((n+1)^2-3n/2-1)$
$S''=(n+1)(n^2+2n+1-3/2n-1)/3$
$S''=(n+1)(n^2+n/2)/3$
$S''=(n+1)(2n^2+n)/6$
∴$S''=n(n+1)(2n+1)/6$。
> **更多推导方法,看 [《具体数学》学习笔记: $4$.四种方法推导平方和公式](https://blog.csdn.net/ajaxlt/article/details/86161674)**
我们直接求与$m$互质的数较难,所以我们可以换个思路,求与 $m$不互质的数,那么与$m$不互质的数,是取$m$的素因子的乘积(因为根据唯一分解定理,任意个数都可看作的素数积),那么我们将$m$分解质因数,通过容斥定理,就可以得道与$m$不互质的数,总和$sum$减去这些数对应的$a$的和就是答案了。
**代码细节**
如果我们利用两个质数$2,3$组成了一个数$t=6$,那么在$1\sim n$范围内,一共几个$6$的倍数呢? 以前学习过,是 $n/6=n/t$ 个,
即:$t,2t,3t,…n/t*t$
现在我们需要把$i \in [t,2t,3t,…n/t*t]$计算$\sum a[i]$
有公因子$t$,设$i=t*j$
我们观察每一项:
$a(i)=i^2+i=(t*j)^2+(t*j)=t^2*j^2+t*j$
在平方和公式前面,要乘一个系数$t$的平方,同时在等差数列求和公式前面要乘一个系数$t$。
根据通项公式,可以以$O(1)$时间快速计算出结果:
$$\large s_x=t^2*s_x'+t*s_x''$$
$Sample$ $Input$
```cpp {.line-numbers}
4 4
```
$Sample$ $Output$
```cpp {.line-numbers}
14
```
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
const int mod = 1e9 + 7; // 尽量这样定义mod ,减少非必要的麻烦
// 快速幂
int qmi(int a, int b) {
int res = 1;
a %= mod;
while (b) {
if (b & 1) res = res * a % mod;
b >>= 1;
a = a * a % mod;
}
return res;
}
vector<int> p; // 将m拆分成的质数因子序列p
signed main() {
#ifndef ONLINE_JUDGE
freopen("SpareTire.in", "r", stdin);
#endif
int n, m;
int Six = qmi(6, mod - 2); // 因为需要用到 % 1e9+7 下6的逆元用费马小定理+快速幂求逆元
int Two = qmi(2, mod - 2); // 因为需要用到 % 1e9+7 下2的逆元用费马小定理+快速幂求逆元
while (cin >> n >> m) {
// 所结果拆分为平方和公式,等差数列两部分
// 注意:现在求的是整体值,还没有去掉不符合条件的数字
int first = n * (n + 1) % mod * (2 * n + 1) % mod * Six % mod;
int second = n * (n + 1) % mod * Two % mod;
int res = (first + second) % mod;
// 对m进行质因子分解
int t = m; // 复制出来
for (int i = 2; i * i <= t; i++) {
if (t % i == 0) {
p.push_back(i);
while (t % i == 0) t = t / i;
}
}
if (t > 1) p.push_back(t);
/*
容斥原理
例如有3个因子那么1<<3=8(1000二进制)
然后i从1开始枚举直到7(111二进制i中二进制的位置1表式取这个位置的因子
例如i=3(11二进制) 表示取前两个因子i=5101表示取第1个和第3个的因子
*/
int s = 0;
for (int i = 1; i < (1 << p.size()); i++) {
int cnt = 0, t = 1;
for (int j = 0; j < p.size(); j++)
if ((i >> j) & 1) {
cnt++;
t *= p[j];
}
// 比如找到了s=6=2*3需要知道s是奇数个还是偶数个因子
// n/s:范围内6的倍数有多少个
int k = n / t;
int x = k * (k + 1) % mod * (2 * k + 1) % mod * Six % mod;
x = x * t % mod * t % mod; // 乘上t^2
// 还需要累加等差数列部分
// 首项是t,项数是k,末项 t*k
x = (x + k * (t + t * k) % mod * Two % mod) % mod;
if (cnt & 1)
s = (s + x) % mod;
else
s = (s - x + mod) % mod;
}
// 输出
cout << (res - s + mod) % mod << endl;
}
}
```
#### [$AcWing$ $215$. 破译密码](https://www.acwing.com/problem/content/217/)