## 容斥原理专题 ### 一、模板题 #### [$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$...的倍数呢?这是一个小学数学问题,遇到事决,小学数学: $[1 \sim n]$中被$p$整除的数个数= $⌊\frac{n}{p}⌋$ > **注**:这里可不是说质数,比如我们想计算$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 using namespace std; #define int long long #define endl "\n" int n, m; // n:质数个数,m:1~m的数字中有多少个可以被质数序列中至少一个整数整除。 // 注意:代码里的n,m与模板题目中的含义相反!一定要注意!!!!!!!!!!!! vector p; // 质数数组 signed main() { cin >> m >> n; // 与m互质,n个质数! // 读入n个质数,为了使用vector,读入时确实不太方便 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 using namespace std; #define int long long #define endl "\n" vector 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$),因此对于一个数对$(x,y)$,如果它们存在公因数,那么**一定可以化简成最简,即互质的形式**,那么这个互质的数对构成的向量应该是和原向量共线的,因此我们 只能看到最前面那个互质的数对构成的点,其它不互质的都会被它前面的某个互质的挡住。 #### 因此题目转变成求区间$[1,m],[1,n]$之间互质数的**对数** **求解办法**: 选取一个区间(**为了优化选取小区间**)比如说选取$[1,n]$,枚举$n$里面的数$i$,然后对于每个数$i$我们看它在$[1,m]$区间内能找到多少互质的数,**把这些结果全部累加起来即可**。也就是枚举一个小区间中的所有数,然后转化为: 求$1\sim n$中与$m$互素的数的个数 > **注**:通常情况下,小的循环在外层,大的循环在内层会更有利于性能。这是因为内存访问模式对性能有重要影响,将内存中的数据按照连续的顺序访问会更高效。当小的循环在外层时,内存中的数据更有可能按照连续顺序被访问,这有助于提高缓存的命中率,从而提高性能。 #### $Code$ ```cpp {.line-numbers} #include using namespace std; #define int long long #define endl "\n" const int N = 1e6 + 10; // 返回1-m中与n互素的数的个数 vector 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/k,y/k)=1 (1<=x<=b/k, 1<=y<=d/k)$ 也就是求互质的对数 那么,我们可以去枚举$x$的范围去算$y$中与$x$互质的个数,但 **为了避免重复的情况** 比如`1 3` 和`3 1`算一对 那么我们就假定$x大于$x$,小于等于$y$,并且与$x$互质的个数 用容斥原理算出大于$x$小于等于$y$的数中是$x$的质因数的倍数的个数$sum$, 然后$y-x-sum$就是$x$与$1 \sim d/k$中互质的对数。 ```cpp {.line-numbers} #include using namespace std; #define int long long #define endl "\n" // 返回1-m中与n互素的数的个数 vector 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; // 因为 (1,3)与 (3,1)算1个,所以要限制x 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 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,代码第1,2个限制条件生效 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$表示。例如:$1,3,5,7,9,2n-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 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 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,那么2,4,6,8,10,....需要加上 枚举到的组合中只有数字3,那么3,6,9,12,15,....需要加上 枚举到的组合中中6.有数字2,3,那么6,12,18,....需要减去 */ 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 = {2,3}$,在$1\sim N-1$中,能被$2$整除的数为{$2,4,6,8,10$},能被$3$整除的数为{$3,6,9$}。则所求集合为{$2,3,4。6,8,9,10$},共$7$个,则答案为$7$。 **解题思路** 就是求$M$个集合的并集。先看上边的样例。能被$2$整除的数集合$S_1$为{$2,4,6,8,10$},能被$3$整除的数 集合$S_2$为{$3,6,9$}。而两者交集$S_{12}$为能被$LCM(2,3) = 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 using namespace std; #define int long long #define endl "\n" const int N = 15; vector 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$次方,我们可以 先对$100$进行开方,然后向下取整,得到的结果就是$100$以内的$2$次方数的个数。在这种情况下,$100$的开方是$10$,向下取整后得到$10$,这意味着$100$以内有$10$个数是某个数的$2$次方。 $100$以内是某个数的$2$次方的数包括:$0, 1, 4, 9, 16, 25, 36, 49, 64$, 和 $81$。 #### 思路 如果我们直接按这个计算,会发现有很多重复,比如有个数是$2$的次方,也有可能是$4$的次方,也有可能是$6$的次方,但是这些其实在$2$的次方中就已经计算了,而$4,8$是$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 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 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 using namespace std; #define int long long #define endl "\n" const int N = 400010; unordered_map mp; // 能用Hash表不用红黑树 vector 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) 考查了组合数公式、补集思想、容斥原理思想(不拘泥于质数+二进制枚举噢~) #### 题意 给定$n、m、k$三个数,$n$代表行数,$m$代表列数,$k$代表人数。 $n*m$的表格,一共有$k$个人,要求: ① 每个小格只能容纳一个人,所有人必须都在表格中。 ② **第一行、第一列、最后一行、最后一列必须站人**。 ③ **若夹角处站人,则代表此行和列都已站人**。 问:有多少种方法? #### 解题思路 利用容斥原理,设: - $S$为总数。($n*m$中选$k$个位置,即$C_{n∗m}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 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 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 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=5(101)表示取第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/)