Acwing 第四章模板及详解(数学知识)

news/2024/10/30 21:29:44/

一、质数
二、约数
三、欧拉函数
四、快速幂
五、扩展欧几里得算法
六、中国剩余定理
七、高斯消元
八、组合计数
九、容斥原理
十、简单博弈论

一、质数

质数
质数,在大于1的整数中,有且只有1和他本身两个因数的数,也叫做素数

试除法判定质数

1、sqrt(x),函数计算的时间比较高

bool is_prime(int x) 
{// 质数大于1if(x < 2) return false;for(int i = 2; i <= sqrt(x); i++) {if(x % i == 0) return false;}return true;
}

2、i × i ≤ x

bool is_prime(int x) 
{// 质数大于1if(x < 2) return false;for(int i = 2; i * i <= x; i++){if(x % i == 0) return false;}return true;
}

3. 推荐此方法i ≤ n / i 

bool is_prime(int x) 
{// 质数大于1if(x < 2) return false;for(int i = 2; i <= x / i; i++) {if(x % i == 0) return false;}return true;
}

试除法判定质数 

bool is_prime(int x)
{if (x < 2) return false;for (int i = 2; i <= x / i; i ++ )if (x % i == 0)return false;return true;
}
试除法分解质因数 
void divide(int x)
{for (int i = 2; i <= x / i; i ++ )if (x % i == 0){int s = 0;while (x % i == 0) x /= i, s ++ ;cout << i << ' ' << s << endl;}if (x > 1) cout << x << ' ' << 1 << endl;cout << endl;
}

(1)朴素筛法求素数 

朴素筛法O(nlogn)
算法核心:把每个数的所有倍数筛掉

调和级数:1 + 1/2 + 1/3 +…+ 1/n = lnn + c(c欧拉常数=0.577)

算法时间复杂度:最外层遍历整个数组是n(其实不用管,只用看内部总次数即可),内部循环总次数是n/2,n/3,n/4…1,累加得n(1/2 + 1/3 + 1/4 +…+1/n)=nlnn=nlogn
 

int primes[N], cnt;     // primes[]存储所有素数
bool st[N];         // st[x]存储x是否被筛掉void get_primes(int n)
{for (int i = 2; i <= n; i ++ ){if (st[i]) continue;primes[cnt ++ ] = i;for (int j = i + i; j <= n; j += i)st[j] = true;}
}
//void get_prime(int x)
{for (int i = 2; i <= n; i ++ ){if (!st[i]) primes[cnt ++] = i;for(int j = i + i ; j <=  n ; j += i) st[j] = true;}
}

(2)埃式筛法O(nloglogn)

算法核心:把每个质数的所有倍数筛掉

质数定理:1~n中由n/logn个质数

算法时间复杂度:由(1)可得:O(nlonglongn)当数据不是足够大时与O(n)接近


void get_primes(int n)
{for (int i = 2; i <= n; i ++ ){if (st[i]) continue; //st如果是true 说明被筛过,那么它的倍数肯定被筛过,所以直接跳过//接下来对该质数的所有倍数进行筛选primes[cnt ++ ] = i;for (int j = i + i; j <= n; j += i)st[j] = true;}
}

(3)线性筛法求素数 

算法核心:x只会被它的最小质因数筛去,即每个数字只被筛选一次,因此是线性的n。

证明每个x都能被筛掉:
对于一个合数x,x一定存在一个最小质因子,假设pj是x 的最小质因子,当i枚举到x/pj时,x就被筛了,因为x只有一个最小质因数,因此每个数字只被筛选一次。

算法时间复杂杂度:因为每个数只被筛过一次,因此为O(n)

void get_prime(int x)
{for(int i = 2 ; i <= x ; i++){if(!st[i]) primes[cnt ++] = i;/**for循环判断语句中不需要j<cnt。分两种情况。1.i为合数,当primes[j]取到i的最小质因子时就break 此时 j<cnt2.i为质数,当primes[j]的值和i相等时就break 此时j == cnt-1**/for(int j = 0 ; primes[j] <= x / i ; j++){										st[primes[j] * i] = true; //筛去primes[j]的倍数/*针对st[primes[j] * i] = true;本质上分两种情况1.i%pj == 0, 因为primes[j]是顺序遍历,因此当当一次模为零时,primes[j]一定为i的最小质因子,primes[j]也一定为primes[j]*i的最小质因子2.i%pj != 0, 同样因为primes[j]是顺序遍历,primes[j]一定小于i的所有质因子所以primes[j]也一定为primes[j]*i最小质因子*/if(i % primes[j] == 0) break;//当primes[j]是i的最小质因数时break(为了  			   //遵守算法的核心,避免重复的筛选)。如果继续用primes[j+1]去筛选,此时,//primes[j+1]大于i的最小质因子,那么也同样不是primes[j+1]*i的最小质因子}}
}

 模板:

int primes[N], cnt;     // primes[]存储所有素数
bool st[N];         // st[x]存储x是否被筛掉void get_primes(int n)
{for (int i = 2; i <= n; i ++ ){if (!st[i]) primes[cnt ++ ] = i;for (int j = 0; primes[j] <= n / i; j ++ ){st[primes[j] * i] = true;if (i % primes[j] == 0) break;}}
}

二、约数
根据分摊分析,当数据最够多时,每个数的约数平均为logn个。
(第一个数是n个数的约数 , 第二个数是n/2个数的约数 , 以此类推第n个数是n/n个数的约数。
累加得n(1/1+1/2+1/3+…+1/n)=n(lnn+c)≈nlogn)

(1)试除法O(sqrt(n))
思路:从小到大枚举较小的约数即可

vector<int> get_divisors(int x)
{vector<int> res;for(int i = 1 ; i <= x / i ; i++) {if(x % i == 0) {res.push_back(i);if(i != x / i) res.push_back(x / i); //特判边界}}sort(res.begin() , res.end());return res;
}

(2)约数个数
题目:给定n个正整数ai,请你输出这些数的乘积的约数个数,答案对109+7取模。

算法思想:
基于算数基本定理:N = p1a1 * p2a2 * … * pkak
N的任意一项约数可以写成 d = q1b1+q2b2+…+qkbk

不同的b数组组合而成的约数d是不同(即因式分解不同)
同理不同的a数组组合而成的N也是不同的,

而a1有a1+1种选法,a2有a2+1种选法…ak有ak+1种选法
因此约数个数:(a1+1)(a1+1)(a3+1)…(ak+1)

#include <bits/stdc++.h>using namespace std;typedef long long ll;const int P = 1e9 + 7;
int main()
{unordered_map<int , int> primes; //利用哈希表存储int n;cin >> n; while(n --) //分解质因数{int x;cin >> x;    for(int i = 2 ; i <= x / i; i++){while(x % i == 0){primes[i]++;x /= i;}}if(x > 1) primes[x]++;}ll ans = 1;for(auto p : primes) //迭代一次,套用公式即可ans = ans * (p.second + 1) % P;  cout << ans << endl; return 0;
}

(3)约数之和
同样的某个数N可以展开为 N = p1a1 * p2a2 * … * pkak

约数之和为:(p10+p11+p12+…+p1a1) * (p20+p21+p22+…+p2a2) * …* (pk0+``pk1+pk2+…+pkak)
即排列问题,展开后就是每一个约数,且都不相等

//求质因数与上方代码相同
for(auto t : primes){int p = t.first , q = t.second;ll res = 1;while(q--)  res = (res * p + 1) % P; //这里有一个小技巧><ans = ans * res % P;}

(4) 最大公约数
(greast common divisor简称“gcd”)

递归版辗转相除法(欧几里得算法):核心是gcd(a , b) = gcd(b , a % b)
原理:
由基本定理得:当d能分别整除a,b时,d就能整除xa+yb。
显然a % b = a - c * b(其中c=a/b)。
即证:(a , b) = (b , a - c * b)
记d=(a,b),则d|a,d|b,所以根据基本定理得d|a-cb,所以d=gcd(b , a-cb)
记d=(b , a-cb) , 则d|b , d|a-cb ,所以根据基本定理得:d|(cb)+(a-cb) 即a|b ,所以d=(a , b)
所以(a , b) = (b , a-c*b)成立,说明(a,b)的公约数就是(b , a - c * b)的公约数,所以等号两边的最大公约数相同。
不断地辗转相除直到第二个参数为0,因为0模上任何一个非零的整数都是0,所以任何一个数都可以看作是0的约数,而b的最大约数是b,那取交集后,gcd(b,0)当然是b。
所以gcd(a , b) = gcd(b , a % b)成立
三、欧拉函数

欧拉函数 O(N)=1到N中所有与N互质的数的个数,如果可以把N写成它的分解质因数公式:

N=p1的a1次方 * p2的a2次方 * .....pk的ak次方

则它的欧拉函数等于N*(1-1/p1)*(1-1/p2)*...(1-1/pk),这是由容斥原理得到的(数学集合论),

我们将n减去所有它的因子的倍数,这其中有些数被减了两次,如p1,p2共同的倍数,我们再逐一加上每两个数的倍数,再减去每三个数的倍数...就能得到与N互质的数的个数了。 
1 ~ N 中与 N 互质的数的个数被称为欧拉函数

欧拉函数的证明
利用容斥原理,求1 ~N-1中与N互斥的数的个数s
拆分出N的质因子p1、p2、p3…
s = N - N/p1 - N/p2 - N/p3-…-N/pk
+N/(p1*p2) + N/(p1*p2) + … + N/(p1*pk) + … + N/(pk-1*pk)
- N/(p1*p2*p3) - N/(p1*p2*p4) - … -N/(pk-2*pk-1*pk)
+N/(p1*p2*p3*p4) + … + N/(pp-3*pp-2*pp-1*pp)
…(以此类推)

p1的倍数由N/p1个,p2的倍数有N/p2个…pk的倍数有N/pk个,因此要减去。
但是有的数同时是p1和p2的倍数,所以会被重复减去,因此在第二行加回。
但是有的数同时是p1、p2、p3的倍数,在第一行会被重复减去三次,而在第二行会重复加回三次,相当于没变,因此要在第三行减去。
以此类推。。。。。。。
最后欧拉函数的公式如下:s = N * (1-1/p1) * (1-1/p2) * (1-1/p3) … * (1-1/pk)

接下来用公式来求欧拉函数 O(n*sqrt(n))


#include <bits/stdc++.h>using namespace std;typedef long long ll;//数据范围较大记得用long long
const int N = 110;int n;int main()
{cin >> n;while(n--){int cnt = 0;ll primes[N];int x;cin >> x;ll ans = x;for(int i = 2 ; i <= x / i ; i++){if(x % i == 0){ans = ans / i * (i - 1);while(x % i == 0) x /= i;}}if(x > 1) ans = ans / x * (x - 1);cout << ans << endl;}return 0;
}


筛法求欧拉函数 

线性筛法求欧拉函数之和O(n)
核心:在找到质数和筛掉和筛掉合数时,利用欧拉函数的公式直接计得出其欧拉函数

int primes[N], cnt;     // primes[]存储所有素数
int euler[N];           // 存储每个数的欧拉函数
bool st[N];         // st[x]存储x是否被筛掉
void get_eulers(int n)
{euler[1] = 1;for (int i = 2; i <= n; i ++ ){if (!st[i]){primes[cnt ++ ] = i;euler[i] = i - 1;}for (int j = 0; primes[j] <= n / i; j ++ ){int t = primes[j] * i;st[t] = true;if (i % primes[j] == 0){euler[t] = euler[i] * primes[j];break;}euler[t] = euler[i] * (primes[j] - 1);}}
}

欧拉定理
定理:若a与n互质,则aΦ(n) ≡ 1(mod n)

证明:
假如1 ~ n中,所有与n互质的数是A数组:a1、a2、a3…aΦ(n)
设M数组:aa1、aa2、aa3…aaΦ(n)(同样与n互质)

下面先证明两个推理:
一、M数列中不存在两个数模n同余。
用假设法证明:
1.假如M数列存在ma与mb模n同余,即ma≡mb(mod n)
2.移项得mi - mj ≡ 0 (mod n)
3.即aai-aaj ≡ 0 (mod n)
4.提取公因式得:a(ai-aj) ≡ 0 (mod n)
5.因为a与n互质,所以要是等式左边与n互质,那么必然:(ai-aj) ≡ 0 (mod n)
6.则ai ≡ aj(mod n),这显然与A数组中的数的性质相违背(A数组中各个数都小于n,并且不同,因此不可能模n同余)。
7.所以假设不成立!即:M数列中不存在两个数模n同余!

二、M中的数对n的余数全部与n互质(即mi%n 与 n 互质)
证明:
1.已知a与n互质,ai与n互质,则aai也与n互质,也可得M数列中的数与n互质。
2.然后带入欧几里得算法中推导即可:gcd(aai , n) = gcd(n , aai%n ) = 1 , 即mi%n 与 n 的最大公约数是1
(a与n互质,pi与n互质,因此api与n互质)
3.得证:M中的数对n的模全部与n互质!

根据这两个推理,可以开始推导欧拉定理。

根据推理二,我们可以知道M中的数模n全部与n互质,而且M中不

存在两两对n取模相等的数,而且A、M数组长度相同,因此一个M中的数一定

可以在A中找到唯一一个数和它模n同余(即两个数组在模n的意义下是等价的),因此可得两个数列分别的乘积对n同余

根据A数组的含义,A数组模n后的数组就是A数组本身

即:m1m2m3m4…mΦ(n) ≡ a1a2a3…aΦ(n) (mod n)

将每项m展开的:a * a1 * a * a2 * a * a3…a * aΦ(n) ≡ a1a2a3…aΦ(n) (mod n)

化简得:aΦ(n)(a1a2a3…aΦ(n)) ≡ a1a2a3… aΦ(n) (mod n)

因A数列中的每一项都跟n互质,那么整个数列的乘积也和n互质,所以

可以两边同除a1a2a3…aΦ(n)得:aΦ(n) ≡ 1 (mod n)

证毕!

费马小定理
由欧拉定理可以马上推得费马定理。
当n为质数时,Φ(n) = n - 1
因此当p是质数时,ap-1≡ 1 (mod p) 即费马定理!
 

四、快速幂 

算法核心:将b用二进制表示,然后对每一位进行01判断,进而累乘即可。

求 m^k mod p,时间复杂度 O(logk)。int qmi(int m, int k, int p)
{int res = 1 % p, t = m;while (k){if (k&1) res = res * t % p;t = t * t % p;k >>= 1;}return res;
}

快速幂求逆元
题目:给定n组ai , pi,其中pi是质数,求ai模pi的乘法逆元,若逆元不存在则输出impossible。

逆元定义:若整数b,p互质,并且对于任意的整数 a,如果满足b|a=0,则存在一个整数x,使得a/b≡a∗x(mod p),则称x为b的模p乘法逆元,记为b−1(mod p)。

接下来进行推导:
若:a/b≡a∗x(mod p)
两边同除a,得:1/b ≡ x (mod p)
移项得:x*b ≡ 1 (mod p) ,即x满足该式子即可!

由费马小定理得:若p是质数,且b不是p的倍数, 则bp-1 ≡ 1 (mod p)
因此x = b p-2 就是解。
当b%p = 0时 ,x*b ≡ 1 (mod p)显然无解

//代码中的a是上面文字中的b
#include <iostream>using namespace std;typedef long long ll;
//求a的b次方
ll qmi(ll a , ll b , ll p)
{ll ans = 1;while(b){if(b & 1) ans = ans * a % p;b >>= 1;a = a * a % p;}return ans;
}int main()
{int n;cin >> n;while(n--){ll a , p;cin >> a >> p;ll t = qmi(a , p-2 , p);if(a % p) cout << t << endl;else puts("impossible");}return 0;
}

欧几里得算法 

int gcd(int a, int b)
{return b ? gcd(b, a % b) : a;
}
求欧拉函数
int phi(int x)
{int res = x;for (int i = 2; i <= x / i; i ++ )if (x % i == 0){res = res / i * (i - 1);while (x % i == 0) x /= i;}if (x > 1) res = res / x * (x - 1);return res;
}

五、扩展欧几里得算法 

题目:给定n对正整数ai , bi,对于每对数,求出一组xi,yi,使其满足ai∗xi+bi∗yi=gcd(ai,bi)。

裴蜀定理:若a,b是整数,且gcd(a,b)=d,那么对于任意的整数x,y,ax+by都一定是d的倍数,特别地,一定存在整数x,y,使ax+by=d成立。
显然因为d|a , d|b , 则d|ax+by。

有一个直接的应用就是 如果ax+by=1有解,那么gcd(a,b)=1;

根据裴蜀定理,我们知道ai∗xi+bi∗yi=gcd(ai,bi)一定存在解,要是想求出x,y就得用到扩展欧几里得算法。
需要找出递归关系
求解方程 ax+by=gcd(a,b)
当 b=0 时, ax+by=a 故而 x=1,y=0。
当b != 0 时,因为gcd(a,b)=gcd(b,a%b)
而gcd(a,b) = xa+yb , gcd(b,a%b) = yb+x(a%b) . 其中 a%b = a - (a/b)b
所以 gcd(b , a%b) = yb+x(a - (a/b)b) , 化简得:gcd(b , a%b) = xa+(y-(a/b)x)b
推得:xa+yb = xa+(y-(a/b)x)b
根据恒等定理得:y = (y-(a/b) * x) 这就是递归关系。

#include <bits/stdc++.h>using namespace std;void exgcd(int a , int b , int &x , int &y)
{if(!b)//终止条件{x = 1 , y = 0;return;}exgcd(b , a % b , y , x);//这里将y写前面,x写后面可以简化代码,否则要进行交换y -= a / b * x;/*这是不交换x,y的写法exgcd(b, a%b, x, y);int t = y;y = x - (a/b) * y;x = t;*/return ;
}int main()
{int n;cin >> n;while(n--){int a , b , x , y;cin >> a >> b;exgcd(a , b , x , y);printf("%d %d\n" , x , y);}return 0;
}

模板:

// 求x, y,使得ax + by = gcd(a, b)
int exgcd(int a, int b, int &x, int &y)
{if (!b){x = 1; y = 0;return a;}int d = exgcd(b, a % b, y, x);y -= (a/b) * x;return d;
}

通解 = 特解 + 齐次解
而齐次解即为方程 ax+by=0的解
通解为 x=x′+k∗b/d , y=y′−k∗a/d ,k∈z

利用扩展欧几里德算法求解线性同余方程
题目:给定n组数据ai,bi,mi,对于每组数求出一个xi,使其满足ai∗xi≡bi(mod mi),如果无解则输出impossible。
算法核心:将原方程进行等价代换:
1.ax≡b (mod m) 等价于 ax+my≡b (mod m)
2.即 (ax+my)%m=b
3.只要利用扩展欧几里德算法求出gcd(a,m),如果b是gcd(a,m)的倍数,那么显然可以通过乘积的方式,来使ax+my等于b,否则返回impossible。

#include <bits/stdc++.h>using namespace std;typedef long long ll;int exgcd(int a, int b , int &x , int &y )
{if(!b){x = 1 , y = 0;return a;}int d = exgcd(b , a % b , y , x);y -= a / b * x;return d;
}int main()
{int n;cin >> n;while(n--){int a , b , m , x , y;cin >> a >> b >> m;int d = exgcd(a , m , x , y);//求gcd(a,m)if(b % d) puts("impossible");else cout << (ll) x * b / d % m << endl;//如果b是最大公约数d的倍数,则可以通过乘上一个b/d来使求得的结果由d变成b}return 0;
}

六、中国剩余定理
描述:给定 2n 个整数a1,a2,…,an和m1,m2,…,mn,求一个最小的非负整数 x,满足∀i∈[1,n],x≡mi(mod ai)。

思路:
合并:将第一条和第二条合并,再合并进第三条……一直合并n-1次,就只剩一条了。
具体实现:1.由题意得x % a1 = m1 , x % a2 = m2,
2.即x = k1a1 + m1 , x = k2a2+m2,
3.取出两式等号右边得:k1a1 + m1 = k2a2+m2
4.移项得:k1a1-k2a2 = m2-m1.( * )
5.接下来利用扩展欧几里得算法求得k1的解和最大公约数d,判断d是否能整除m2-m1,若不行说明无解,否则继续
6.再将k1自乘上一个(m2-m1)/d才是( * )的一个解,可以知道k1的通解是k1+ka2/d,k2的通解是k2+ka1/d
7.为了防止数据溢出,我们需要求得k1的最小正整数解,在c++中,取模会得到负值,因此我们需要以下操作
8.令t=a2/d,k1 = (k1 % t + t) % t
9.将k1= k1+ka2/d,代回x = k1a1 + m1中得:x = k1a1+m1+ka1a2/d
10.发现此时x的表达式和原来的很像,一部分常量,一部分变量,所以我们重新赋值 , m1 = k1a1+m1,a1= abs(a1 / d * a2)
11.这样一来原来的两条式子就合并为一条x=k1a1+m1(此时a1和m1已经变了),继续合并即可。
12.最后出m1就是解,在10中可以发现m1=k1a1+m1就是x的值,不过在输出也要对其a1取模以输出最小的非负整数解。

#include <bits/stdc++.h>using namespace std;typedef long long ll;ll exgcd(ll a , ll b , ll &x , ll &y)
{if(!b){x = 1 , y = 0;return a;}int d = exgcd(b , a % b , y , x);y -= a / b * x;return d;
}int main()
{int n;cin >> n;ll a1 , m1;cin >> a1 >> m1;bool flag = true;for(int i = 0 ; i < n - 1 ; i++){ll a2 , m2;cin >> a2 >> m2;ll k1 , k2;ll d = exgcd(a1 , a2 , k1 , k2);if((m2 - m1) % d){flag = false;break;}k1 *= (m2 - m1) / d;//此时求出来的k1只是众多k1的解中的一个而已ll t = a2 / d;//防止溢出,要求得k1的最小正整数解,易得k1+k*(a2/d)是k1的通解,因此使其对t 取模k1 = (k1 % t + t) % t;//在c++中,取模的值可得负数,在这样的操作之后会变成同等意义的正数m1 = a1 * k1 + m1;a1 = abs(a1 / d * a2);}if(flag) cout << (m1 % a1 + a1) % a1 << endl;else puts("-1");return 0;
}

七、高斯消元 

描述:解一个包含n个方程n个未知数的线性方程组

在这里插入图片描述

算法流程:对每一列的系数进行如下操作
1.找到一列中系数绝对值最大的一条方程(不考虑已经移动过的方程)
2.将其移到最上方(同样不考虑移动过的方程)
3.将该系数变为1
4.将下面的方程同一列的系数消为0
5.得到一个倒三角形方程组,即可求出解

得出三种情况:
①完美在倒三角i选哪个 唯一解
②0 = 非0 无解
③0 = 0 无穷解

 

// a[N][N]是增广矩阵
int gauss()
{int c, r;for (c = 0, r = 0; c < n; c ++ ){int t = r;for (int i = r; i < n; i ++ )   // 找到绝对值最大的行if (fabs(a[i][c]) > fabs(a[t][c]))t = i;if (fabs(a[t][c]) < eps) continue;for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]);      // 将绝对值最大的行换到最顶端for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];      // 将当前行的首位变成1for (int i = r + 1; i < n; i ++ )       // 用当前行将下面所有的列消成0if (fabs(a[i][c]) > eps)for (int j = n; j >= c; j -- )a[i][j] -= a[r][j] * a[i][c];r ++ ;}if (r < n){for (int i = r; i < n; i ++ )if (fabs(a[i][n]) > eps)return 2; // 无解return 1; // 有无穷多组解}for (int i = n - 1; i >= 0; i -- )for (int j = i + 1; j < n; j ++ )a[i][n] -= a[i][j] * a[j][n];return 0; // 有唯一解
}


八、递归法求组合数

1.题目:给定n组询问,每组询问给定两个整数a,b,请你输出Cba mod (109+7)的值。1≤n≤10000,1≤b≤a≤2000. O(N2)
数据较小,共有2000*2000=4e6中形式,直接暴力,先预处理每一种形式的值,再查询即可。

利用这条关系式:Cba = Cba-1 + Cb-1a-1,打个比方:Cba就是从a个苹果中拿b个,所有的拿法可以分成包括某个特殊的苹果,和不包括某个特殊的苹果,包括的情况则Cb-1a-1,不包括则是Cba-1,加起来就是Cba。

// c[a][b] 表示从a个苹果中选b个的方案数
for (int i = 0; i < N; i ++ )for (int j = 0; j <= i; j ++ )if (!j) c[i][j] = 1;else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;


通过预处理逆元的方式求组合数 
首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]
如果取模的数是质数,可以用费马小定理求逆元

#include <bits/stdc++.h>using namespace std;const int N = 2010;
const int P = 1e9 + 7;
int t[N][N];int main()
{for(int i = 0 ; i < N ; i++)for(int j = 0 ; j <= i ; j++){if(!j) t[j][i] = 1;else t[j][i] = (t[j][i - 1] + t[j - 1][i - 1]) % P;//递推式}int n;cin >> n;while(n--){int a, b;cin >> a >> b;cout << t[b][a] << endl;}return 0;
}

2题目:1≤n≤10000, 1≤b≤a≤105 (NlonN)
1e5 * 1e5 = 1e10,此时情况就太多了,就不能用之前的预处理了。
还知道Cba = a!/(b!*(a-b)!),所以我们可以预处理每一个数的阶乘先,然后在查询即可。
因为a/b mod p != (a mod p)/(b mod p),所以我们要转换成乘法,就要用到逆元。
 

#include <bits/stdc++.h>using namespace std;typedef long long ll;
const int N = 100010;
const int P = 1e9 + 7;int fact[N] , infact[N];//infact[i]是fact[i]的逆元,因为P是质数,所以可以用费马小定理,快速幂求逆元。int qmi(int a, int b , int p)//求
{int res = 1;while(b){if(b & 1) res = (ll)res * a % p;b >>= 1;a = (ll)a * a % p;}return res;
}int main()
{fact[0] = infact[0] = 1;for(int i = 1 ; i < N ; i++){fact[i] = (ll)i * fact[i - 1] % P;infact[i] = (ll)qmi(i , P - 2 , P) * infact[i - 1] % P;}int n;cin >> n;while(n--){int a , b;cin >> a >> b;cout << (ll)fact[a] * infact[b] % P * infact[a - b] % P << endl;}return 0;
}

int qmi(int a, int k, int p)    // 快速幂模板
{int res = 1;while (k){if (k & 1) res = (LL)res * a % p;a = (LL)a * a % p;k >>= 1;}return res;
}// 预处理阶乘的余数和阶乘逆元的余数
fact[0] = infact[0] = 1;
for (int i = 1; i < N; i ++ )
{fact[i] = (LL)fact[i - 1] * i % mod;infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
}

3.题目:给定n组询问,每组询问给定三个整数a,b,p,其中p是质数,请你输出Cba mod p的值。1≤n≤20,1≤b≤a≤1018,1≤p≤105。

这里a和b的数据很大,要用到Lucas(鲁卡斯定理)
在这里插入图片描述

#include <bits/stdc++.h>using namespace std;typedef long long LL;int p;int qmi(int a, int b)//快速幂求逆元
{int res = 1;while(b){if(b & 1) res = (LL) res * a % p;b >>= 1;a = (LL) a * a % p;}return res;
}int C(int a , int b)
{if(a < b) return 0;int res = 1;for(int i = b , j = a ; i ; i-- , j--)//利用定义求出C{res = (LL) res * j % p;res = (LL) res * qmi(i , p - 2) % p;//利用逆元求}return res;
}int lucas(LL a, LL b)
{if(a < p && b < p) return C(a , b);return (LL)C(a % p , b % p) * lucas(a / p , b / p) % p;
}int main()
{int n;cin >> n;while(n--){LL a , b;cin >> a >> b >> p;cout << lucas(a , b) << endl;}return 0;
}

 4.题目:输入a,b,求Cba的值,1≤b≤a≤5000。
注意结果可能很大,需要使用高精度计算。

算法思路:
1.分解质因数,将Cba=a!/(b!*(a-b)!)分解质因数 p1k1 *p2k2 *p3k3 …pnkn ,求出最终每一个质因数的次数
2.高精度乘法

求a!中p的次数O(logp):

在这里插入图片描述

#include <bits/stdc++.h>using namespace std;const int N = 5010;int primes[N];
int cnt;
int sum[N];
bool st[N];int get(int n , int p)//返回n的阶乘中 ,p的次数
{int s = 0;while(n){s += n / p;n /= p; }return s;
}vector<int> mul(vector<int> a , int b)//高精度乘法
{vector<int> ans;int t = 0;for(int i = 0 ; i < a.size() || t ; i++){if(i < a.size())    t += a[i] * b;ans.push_back(t % 10);t /= 10;}return ans;
}void get_primes(int n)//线性筛质数
{for(int i = 2 ; i <= n ; i++){if(!st[i])   primes[cnt++] = i;for(int j = 0 ; primes[j] * i <= n ; j++){st[primes[j] * i] = true;if(i % primes[j] == 0) break;}}
}int main()
{int a, b;cin >> a >> b;get_primes(a);for(int i = 0 ; i < cnt ; i++)//求出最终每一个p的次数{int p = primes[i]; sum[i] = get(a , p) - get(b , p) - get(a - b , p);}vector<int> res;res.push_back(1);for(int i = 0 ; i < cnt ; i++)//累乘for(int j = 0 ; j < sum[i] ; j++)res = mul(res , primes[i]);for(int i = res.size() - 1 ; i >= 0 ; i--)  cout << res[i];cout << endl;return 0;
}

Lucas定理 
 

若p是质数,则对于任意整数 1 <= m <= n,有:C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p)int qmi(int a, int k, int p)  // 快速幂模板
{int res = 1 % p;while (k){if (k & 1) res = (LL)res * a % p;a = (LL)a * a % p;k >>= 1;}return res;
}int C(int a, int b, int p)  // 通过定理求组合数C(a, b)
{if (a < b) return 0;LL x = 1, y = 1;  // x是分子,y是分母for (int i = a, j = 1; j <= b; i --, j ++ ){x = (LL)x * i % p;y = (LL) y * j % p;}return x * (LL)qmi(y, p - 2, p) % p;
}int lucas(LL a, LL b, int p)
{if (a < p && b < p) return C(a, b, p);return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}

九、容斥原理

集合数为奇数——正好
偶数——负号

判断集合个数
在本题中,判断1-n中包含质数p1的个数为 n / p1,向下取整。
同时有可能存在同时是p1 和p2 的倍数,出现重复相加的情况,要减去,于是 n / (p1*p2)。
以此类推,每次要求的就是包含不同集合的情况下的个数。

程序设计思路
由于可能项较多,我们枚举所有可能项——一般用位运算来做
这样可以对应所有的选法,
从1- 2的n次方-1,将他们转换成二进制表示,0表示不选,1表示选,这样可以包含其中的所有情

判断第k位是不是1, i >> k & 1

题目:

给定一个整数 n 和 m 个不同的质数 p1,p2,…,pm。

请你求出 1∼n 中能被 p1,p2,…,pm 中的至少一个数整除的整数有多少个。

模板:

#include <bits/stdc++.h>
using namespace std;typedef long long LL;const int N = 20;int p[N];int main()
{int n, m;cin >> n >> m;for (int i = 0; i < m; i ++ ) cin >> p[i];int res = 0;for (int i = 1; i < 1 << m; i ++ ){int t = 1, s = 0;for (int j = 0; j < m; j ++ )if (i >> j & 1){if ((LL)t * p[j] > n){t = -1;break;}t *= p[j];s ++ ;}if (t != -1){if (s % 2) res += n / t;else res -= n / t;}}cout << res << endl;return 0;
}

容斥原理一般用来包含这多个集合的求个数
需要
1、集合构造
2、集合个数求解
3、对于重合部分的集合个数求解
这三步是看题目自己分析


分解质因数法求组合数 
当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:
    1. 筛法求出范围内的所有质数
    2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 n! 中p的次数是 n / p + n / p^2 + n / p^3 + ...
    3. 用高精度乘法将所有质因子相乘

int primes[N], cnt;     // 存储所有质数
int sum[N];     // 存储每个质数的次数
bool st[N];     // 存储每个数是否已被筛掉

void get_primes(int n)      // 线性筛法求素数
{for (int i = 2; i <= n; i ++ ){if (!st[i]) primes[cnt ++ ] = i;for (int j = 0; primes[j] <= n / i; j ++ ){st[primes[j] * i] = true;if (i % primes[j] == 0) break;}}
}int get(int n, int p)       // 求n!中的次数
{int res = 0;while (n){res += n / p;n /= p;}return res;
}vector<int> mul(vector<int> a, int b)       // 高精度乘低精度模板
{vector<int> c;int t = 0;for (int i = 0; i < a.size(); i ++ ){t += a[i] * b;c.push_back(t % 10);t /= 10;}while (t){c.push_back(t % 10);t /= 10;}return c;
}get_primes(a);  // 预处理范围内的所有质数for (int i = 0; i < cnt; i ++ )     // 求每个质因数的次数
{int p = primes[i];sum[i] = get(a, p) - get(b, p) - get(a - b, p);
}vector<int> res;
res.push_back(1);for (int i = 0; i < cnt; i ++ )     // 用高精度乘法将所有质因子相乘for (int j = 0; j < sum[i]; j ++ )res = mul(res, primes[i]);

卡特兰数( Cn2n / (n+1))

给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数的序列的数量为: Cat(n) = C(2n, n) / (n + 1)

举个例子:n=6时,就可以画成上图,假设向右是0向上是1,则在红线以下的路径是合法的,可以看出每一条从(0,0)走到(6,6)的非法路径做关于红线的对称,都对应一条(0,0)-(5,7)的路径;反之,每一条从(0,0)-(5,7)的路径都对应一条从(0,0)-(6,6)的非法路径,那么就可以利用(0,0)-(5,7)的路径数间接求出(0,0)-(6,6)的非法路径数。

算法核心:每一条从(0,0)走到(n,n)的非法路径都对应一条从(0,0)走到(n-1,n+1)的非法路径,因此合法路径就是
因此从(0,0)走到(6,6)的不合法路径数就是Cn-12n,即合法的是Cn2n-Cn-12n ,化简得 Cn2n / (n+1),
所以直接从定义出发求出Cn2n ,其中因为模上一个质数,可以用快速幂求逆元。

#include <bits/stdc++.h>using namespace std;typedef long long LL;const int N = 100010 , mod = 1e9 + 7;int qmi(int a, int k, int p)
{int res = 1;while (k){if (k & 1) res = (LL)res * a % p;a = (LL)a * a % p;k >>= 1;}return res;
}int main()
{int n;cin >> n;int ans = 1;for(int i = 2 * n ; i > n ; i--)    ans =  (LL) ans * i % mod;for(int i = n ; i >= 1 ; i--) ans = (LL) ans * qmi(i , mod - 2 , mod) % mod;ans = (LL) ans * qmi(n + 1 , mod - 2 , mod) % mod;cout << ans << endl;return 0;
}

十、博弈论

先手必胜和先手必败状态
定义P-position和N-position,其中P代表Previous,N代表Next。直观的说,上一次move的人有必胜策略的局面是P-position,也就是“后手可保证必胜”或者“先手必败”,现在轮到move的人有必胜策略的局面是N-position,也就是“先手可保证必胜”。更严谨的定义是:1.无法进行任何移动的局面(也就是terminal position)是P-position;2.可以移动到P-position的局面是N-position;3.所有移动都导致N-position的局面是P-position。

先手必胜状态(N-position)—— 可以走到某一个必败状态(存在我走的某一步,使得对方是必败状态)
先手必败状态(P-position)—— 走不到任何一个必败状态(我怎么走对方都是赢,对方走不到任何一个必败状态)

NIM游戏 —

给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。

我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。
NIM博弈不存在平局,只有先手必胜和先手必败两种情况。

定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0

题目

现在,有一个 n 级台阶的楼梯,每级台阶上都有若干个石子,其中第 i 级台阶上有 ai 个石子(i≥1)。

两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。

已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

模板

#include <bits/stdc++.h>using namespace std;const int N = 100010;int main()
{int n;scanf("%d", &n);int res = 0;for (int i = 1; i <= n; i ++ ){int x;scanf("%d", &x);if (i & 1) res ^= x;}if (res) puts("Yes");else puts("No");return 0;
}

公平组合游戏ICG

若一个游戏满足:

由两名玩家交替行动;
在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
不能行动的玩家判负;
则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。

有向图游戏
给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。
任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。

Mex运算
设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:
mex(S) = min{x}, x属于自然数,且x不属于S

SG函数
在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:
SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。

有向图游戏的和 —— 模板题 AcWing 893. 集合-Nim游戏
设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:
SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)

定理
有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。

使用Nim模型去求解问题的关键
在于简单判断当前是否为先手必胜状态还是先手必败状态
如果是先手必胜状态则,找到必胜策略。


http://www.ppmy.cn/news/836504.html

相关文章

蓝桥杯算法竞赛培训(二) 汉诺塔与STL

2023大厂真题提交网址(含题解): www.CodeFun2000.com&#xff08;http://101.43.147.120/&#xff09; 最近我们一直在将收集到的机试真题制作数据并搬运到自己的OJ上&#xff0c;供大家免费练习,体会真题难度。现在OJ已录入50道2023年最新大厂真题&#xff0c;同时在不断的更…

基于GPT4All的大型语言模型设计生态系统

GPT4All 一套专为强大、定制的大型语言模型设计的生态系统,能够在消费级CPU上本地运行。在GPT4All中,所使用的模型是一个3GB至8GB的文件,读者可以自行下载该文件,并将其插入到GPT4All的开源生态系统软件中。这一软件生态系统由Nomic AI提供支持并进行维护,其目的是确保系统…

《大掌门》欧阳刘彬--基于Cocos2d-x引擎开发经验分享

《大掌门》欧阳刘彬分享的内容同样是与Cocos2D-X和跨平台开发有关&#xff0c;在演讲中他详细分享了为什么会选择Lua。 欧阳刘彬&#xff1a;首先感谢CocoaChina的邀请&#xff0c;跟大家分享一下我们《大掌门》在游戏开发过程当中使用Cocos2D所开发的一些经验。刚才凌聪讲的…

js-二维子矩阵的和

// 给定一个二维矩阵 matrix&#xff0c;以下类型的多个请求&#xff1a; // 计算其子矩形范围内元素的总和&#xff0c;该子矩阵的左上角为(row1, col1) &#xff0c;右下角为(row2, col2) 。// 实现 NumMatrix 类&#xff1a;// NumMatrix(int[][] matrix) 给定整数矩阵 matr…

【Mybatis-Plus】 Mapper 接口使用总结

本文主要内容是 Mapper接口的使用 示例。 注意&#xff1a;我们需要方法 执行的SQL 是什么样的&#xff0c;以及返回值。 Mapper CRUD 接口 新增 insert方法 > Preparing: INSERT INTO user ( user_id, user_name, create_time ) VALUES ( ?, ?, ? ) > Parameter…

mysql,对表的简单操作

一.创建表并插入数据 mysql> create table worker(-> department_id int(11) not null comment 部门号,-> worker_id int(11) primary key not null comment 职工号,-> worker_date date not null comment 工作时间,-> wages float(8,2) not null comment 工资…

SpringBoot快速实践 --Ⅰ

文章目录 启动一个SpringBoot项目如何替换内嵌容器玩转SpringBoot配置全局异常处理过滤器拦截器使用Lombok简洁代码使用IDEA HTTP Client进行接口调试 启动一个SpringBoot项目 如果你觉得使用官网来创建太慢了&#xff0c;那你直接把以前项目的依赖粘过来就行了&#xff1a; …

【sfu】追踪ms如何提供flv服务

media request : mrmedia source : msMediaSourceMgr 根据mr 创建ms std::shared_ptr<MediaSource> MediaSourceMgr::FetchOrCreateSource(MediaSource::Config& cfg,std::shared_ptr<MediaRequest> req)