数学基础
质数
定义:对于从2开始的的整数如果只包含1和本身这两个约数,就被称为质数/素数
质数的判定模板:根据定义(试除法)判断质数 O(sqrt(n))
bool is_prime(int x)
{
if (x < 2) return false;
for (int i = 2; i <= x / i; i ++ )//不推荐sqrt,因为每次都要算,不推荐i*i,因为可能溢出成负数。
if (x % i == 0)
return false;
return true;
}
分解质因数模板:试除法 O(sqrt(n))
void divide(int x)
{
for (int i = 2; i <= x / i; i ++ )//优化:x中最多包含一个大于sqrt(x)的质因子
if (x % i == 0)
{
int s = 0;
while (x % i == 0) x /= i, s ++ ;//把x的质因子除干净,所以对x的除法是直接永久改变x在这个函数里的值
cout << i << ' ' << s << endl;
}
if (x > 1) cout << x << ' ' << 1 << endl;//如果还剩一个数字,那么这个数字一定大于sqrt(n),而且是它的质因数且只有一个。
cout << endl;
}
/*推理过程:
x被除i之后,x的所有质数仍然是初始x的所有质数
x可以整除初始x
while循环结束后,当前的i不可能再能整除x,i已不是剩余x的质数
x是质数,即在sqrt(x)的范围内找不到i能整除x
x是初始x的质因数
初始x大于sqrt(x)的质因数最多只有一个
x是初始x质因数,且为1次方
*/
阶乘的质因数分解
n! 中质数p的次数是 [n / p] + [n / p^2] + [n / p^3] + ...
原理:1~a中p的倍数+p^2的倍数+...得到p所有的倍数之和,也就是质因数分解后p是多少次方。详见《算法竞赛进阶指南》P138。
思路:线性筛法得一张素数表,素数表的范围要够得上n,然后从素数表里一个一个取质数p,每个p的次方数就是 [n / p] + [n / p^2] + [n / p^3] + ...
具体实现见”求组合数IV“,其中用到了相关思想。
朴素筛法求质数 O(nloglogn) (埃氏筛:只要现在的i不是之前数字的倍数,那么i就是质数,同时也只要筛去质数的倍数即可,这里面已经包含了非质数的质数)
int primes[N], cnt; // primes[]存储所有素数,cnt就是count计数
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;
}
}
线性筛法求质数
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 ++ )//从质数表primes第0位开始,找到第一个大于n/i的质数之前的所有质数
{
st[primes[j] * i] = true;//那么这个质数乘i便筛掉了一个数字
if (i % primes[j] == 0) break;//到了i的最小质因子,就停下来
}
}
}
/*
n只会被最小质因子找到并筛去
1. i % primes[j] == 0说明prime[j]一定是i的最小质因子,同时也是primes[j]*i的最小质因子
2. i % primes[j] != 0说明prime[j]一定小于i的所有质因子,同时也是primes[j]*i的最小质因子
对于任意一个合数x,假设primes[j]是x的最小质因子,当i枚举到x/primes[j]的时候一定存在j,在for(j)循环的过程中可以把x筛掉
没有必要让第二层for循环增加一个条件j<cnt
*/
判断两个数是否互质
看他们是否是倍数关系,即假设a大的情况下a%b是否等于0,a或b中有1除外,1一定是任何一个正整数的倍数,同时又与他们互质。
约数
试除法求所有约数
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;
}
约数个数和、约数之和:通过质数个数来反映约数
如果 \(N = p1^c1 * p2^c2 * ... *pk^ck (质因数分解)\)
约数个数:
\(
(c1 + 1) * (c2 + 1) * ... * (ck + 1)
\)
原理:约数一定是p1^b1 * p2^b2 * ... *pk^bk,而bi一定在0~ci之间,即bi的取值有ci+1种,那总共的取值个数(约数)就是刚刚的公式
约数之和:
\(
(p_1^0 + p_1^1 + ... + p_1^c1) * ... * (p_k^0 + p_k^1 + ... + p_k^ck)
\)
原理:展开后的每一个单项,都是N的约数 实际计算利用
\(
p_1^0 + p_1^1 + ... + p_1^c1=1+p_1✖(1+p_1✖(...))
\)
即\(t = (t * a + 1)\)不断循环ci次得到k项里的每一项,然后相乘得到约数之和
应用范例
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;
typedef long long LL;
const int mod = 1e9 + 7;
int main()
{
int n;
scanf("%d",&n);
unordered_map<int,int> primes;
while(n--)
{
int x;
scanf("%d",&x);
for(int i=2;i<=x/i;i++)
{
while(x%i==0)
{
primes[i]++;
x/=i;
}
}
if(x>1) primes[x]++;
}
LL res=1;
for(auto p:primes)
{
LL a=p.first,b=p.second;
LL t=1;
while(b--)
{
t = (t * a + 1) % mod;
}
res = res * t % mod;
}
cout<<res;
return 0;
}
求最大公约数:欧几里得算法/辗转相除法
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
原理:数对(a,b)与数对(b,b%a)的最大公约数数量大小完全一样。证明,可以将假设左边数对一个公约数d,能推出来d一定是右边数对的公约数,反之亦然。
欧拉函数
定义
互质:若N个整数的最大公因数是1,则称这N个整数互质。
计算公式
将n分解质因数:
则有:
或:(注意先除后乘防止数据溢出)
计算公式原理:容斥原理:n减去所有n/pk,n/pk意味着比n小的pk的倍数的数量,这些数字都与n有同一个公约数pk,故一定不是互质的数,数字总数n要减去这些数字,同时再把某些既是pk1倍数又是pk2倍数的公共部分加回来,因为前一步有重复计算,然后再把减去重复的补,再去重,最后因式分解。
公式法求欧拉函数:求n的欧拉函数 O(sqrt(n))
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;
}
筛法求欧拉函数:求1~n中每一个数的欧拉函数 O(n)模仿线性筛法
int primes[N], cnt; // primes[]存储所有素数
int euler[N]; // 存储每个数的欧拉函数
bool st[N]; // st[x]存储x是否被筛掉
void get_eulers(int n)//如果要储存/返回1~n欧拉函数结果之和应用long long
{
euler[1] = 1;//初始化第一种情况euler[1]
for (int i = 2; i <= n; i ++ )
{
if (!st[i])//当i是质数时
{
primes[cnt ++ ] = i;
euler[i] = i - 1;//当i是质数时,1~i-1都与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)//primes[j]是i的质因子时
/*
primes[j]*i与i都进行质因数分解后,区别无非就是i的质数primes[j]上次方-1,根据欧拉公式,得出euler[t] = euler[i] * primes[j];
*/
{
euler[t] = euler[i] * primes[j];//具体解释见公式1
break;
}
//primes[j]不是i的质因子时
/*
primes[j]*i对于i而言,不仅拥有i的所有质数,同时还多一个质数primes[j]
*/
euler[t] = euler[i] * (primes[j] - 1);//具体解释见公式2
}
}
}
公式1:(其中primes[j]缩写为pj)
由于primes[j]*i与i的质数pk完全相同
故euler[t] = euler[i] * primes[j];
公式2:(其中primes[j]缩写为pj)
由于primes[j]*i对于i而言,不仅拥有i的所有质数,同时还多一个质数primes[j]
故euler[t] = euler[i] * (primes[j] - 1);
欧拉定理
定理内容:
跟若a与n互质,则有:
证明:
1~n中与n互质的数一共有 φ(n)个,设它们为
由于a与n也互质,故
中每一个数字也与n互质
所以②每个数字对n取余的结果与①的数字完全相同,只是数字顺序可能不同,因为①包含了1~n所有与n互质的数,而②内所有数字取余后互不相等(反证法)且与n互质,那么②中每个数字都能对应上①中的一个数字。
故此时再取①中每一项相乘,②中每一项取余相乘,再两边同时取余可得同余,即
化简即
费马定理(可看为欧拉定理特殊情况)
再欧拉定理的前提下,特别地,当n为质数时,1n中1n-1全部都与n互质,即φ(n)=n-1,此时有:
快速幂
模板:求 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;
}
乘法逆元
若整数b,m互质,并且对于任意的整数a,如果满足b|a(b能整除a),则存在一个整数x,使得
化简即(两遍同时乘b、除以a):
则称x为b的模m乘法逆元,记为:(这是标记而不是运算符号,也就是x不是数学运算来的,而是我们就是这么记/写)
注:
通解:
b与m互质时,根据欧拉定理
即
得b模m乘法逆元
特殊情况:且当模数m为质数时
即为b的乘法逆元
证明:由费马定理可知,若b与m互质,且m为质数时,有
故
--->注意:若b与m不互质,即b与m是倍数关系,有b%m==0,x一般也会等于0(m=2除外,此时m-2=0,0次方特殊),这个特点可说明乘法逆元x不存在
递归求乘法逆元:详见求组合数2:推导1
扩展欧几里得算法:求二元一次方程整数解
裴蜀定理/贝祖定理
定理内容
对于gcd(a,b)=d,对于任意的两个整数x,y,ax+by都一定是d的倍数,特别地,一定存在整数x,y,使ax+by=d成立。换句话说,只要d不是gcd(a,b)的倍数,就无解。
推论
a,b互质的充分必要条件是存在整数x,y使ax+by=1
模板
算法目的:给定a,b,求整数x,y,使得ax + by = gcd(a, b),同时返回最大公约数
// 求一组解x, y,使得ax + by = gcd(a, b),解可能不唯一。
int exgcd(int a, int b, int &x, int &y)
{
if (b==0)//若b等于0,也就是求ax+0y=gcd(a,0)=a的解,即x=1,y=0;
{
x = 1; y = 0;
return a;
}
int d = exgcd(b, a % b, y, x);//递归求解,a与b位置互换了,x与y互换
//此时存在关系by+(a mod b)x=d,即by+{a-[a/b]b}x=d
//即b(y-[a/b]x)+ax=d,此时构造了一个新的y,即y-[a/b]x
//注:[]为向下取整符号
y -= (a/b) * x;
return d;//返回值是最大公约数
}
通解:由已知解\(x_0,y_0\),易得等式:
通解即:
线性同余方程
解关于x的方程(解不唯一):
思想:等式变形为ax=b+m的倍数,设m的倍数是-y,即ax=b-my,有ax+my=b,那么先解 ax+my=gcd(a,m) 故有解的条件是b为gcd(a,m)的倍数,如果不是则无解,若是则化归为扩展欧几里得算法。
注:若解出来的值过大可以mod m。
注:题目的取模是一般意义的,但负数取模各个语言不一样
中国剩余定理
内容:给定k个两两互质的数字m1,m2,...,mk,解关于x的方程组
令
记Mi^-1为Mi模mi的逆元,写作x
其中x
则解为
解的正确性很好验证,以方程组第一个等式为例,由M1M1^-1的性质可得,取模m1后为1,第一项剩a1,此后的每一项,由于Mi里都含有m1,故每一项都可整除,取模后都等于0,故有 x = a1 (mod m1) 。
不满足两两互质时:数学归纳法总结规律。
//以acwing 204题为例
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL exgcd(LL a,LL b,LL &x,LL &y)
{
if(b==0)
{
x=1,y=0;
return a;
}
LL d = exgcd(b,a%b,y,x);
y = y - a/b*x;
return d;
}
int main()
{
int n;
cin>>n;
bool flag=true;
LL a1,m1;
cin>>a1>>m1;
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 = k1 * (m2-m1)/d;//另一个系数k2不需要就不记了
LL t = a2/d;//通解=k1+k*t,k属于Z
k1 = (k1%t+t)%t;//通过通解找到最小特解,根据c++取模方法这样取到最小非负的k1+k*t,k属于Z
m1 = k1*a1+m1;
a1 = abs(a1*a2/d);
}
if(flag)
{
cout<<(m1%a1+a1)%a1;
}
else puts("-1");
return 0;
}
高斯消元:解n元线性方程组 O(n^3)
原理
线性代数,经过初等行列变换,高斯消元,如果能化成完美阶梯型,就有唯一解,如果本来应该是0的位置不是0,那就无解,如果有一行系数全是0,就无穷多个解。
过程
枚举每一列,假设现在在处理第c列:
找到绝对值最大的一行
将该行换到最上面(但在已经固定的行下面)
将该行第一个数约成1
利用该行将该行下面所有第c列消成0
模板
// a[N][N]是增广矩阵,一般double型,同时0不一定是0,而是一个足够小的数eps = 1e-6
//最后答案就是这个增广矩阵的最后一列
//函数返回值表示解的情况
int gauss()
{
int c, r;//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;//如果这一列全都是0,直接看下一列。
for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]); // 将绝对值最大的行换到最顶端(最顶端的第r行与第t行所有元素交换)
for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c]; // 将当前行的首位变成1,倒着来,把a[r][c]最后变成1
for (int i = r + 1; i < n; i ++ )// 用当前行将下面所有的列消成0
if (fabs(a[i][c]) > eps)//如果不是0
for (int j = n; j >= c; j -- )//也要倒着来,这行打头的数字代表了r行放大的倍数,所以要最后动它。
a[i][j] -= a[r][j] * a[i][c];
r ++ ;//不走这个r++唯一的可能性就是走了上面的continue,这意味着行没有处理就走了下一个列,然后接着从这行开始,这样最后的阶梯矩阵从第r行(从0开始数,这里的r是这个大循环走完最后的r)开始,系数全都是0,这将给我们判断解的情况做铺垫。
}
if (r < n)//如果
{
for (int i = r; i < n; i ++ )
if (fabs(a[i][n]) > eps)//如果在系数全为0的方程里发现等号右边(即从0开始数的增广矩阵第n列)不为0,则无解
return 2; // 无解
//没有发现则意味着某些未知数无论去什么值都满足方程,因为他们的系数是0,所以有无穷多组解。
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];
/*上面这段循环的注释
从最后一行开始,第i+1列开始(第i行第i列已经是1了,现在要把其他的消成0),给这行最后一个数字a[i][n],逐个减去a[i][j]倍的第j行最后一个数字a[j][n]
我们是按照化为对角矩阵的思路来的,但系数矩阵最后不是对角矩阵,我们只改动了最后一列,在我们大脑里已经默认了处理过的行是0...010...0,不必真的体现到储存矩阵的数组上
*/
return 0; // 有唯一解
}
//主函数用for循环读入增广矩阵的时候别忘了列比行多一个,要+1.
高斯消元解异或线性方程组
形式:相比于普通的线性方程组,异或线性方程组把加减变成了异或符号,同时未知数的系数只为0或1,增广矩阵最右侧一列数字只为0或1.
异或可以理解为不进位加法,所以方程组是线性的,可以模仿常规高斯消元来写,只不过加减变成了异或。
1.消成上三角矩阵
1.1.枚举列
1.2.找非零行
1.3.交换
1.4.下面消0(通过异或计算)
2.判断解的情况
示例
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n,a[N][N];
int gauss()
{
int c,r;
for(c=0,r=0;c<n;c++)
{
int t;
for(t=r;t<n;t++)
if(a[t][c]==1) break;
if(t==n) continue;
for(int i=c;i<=n;i++) swap(a[t][i],a[r][i]);
for(int i=r+1;i<n;i++)
if(a[i][c])
for(int j=c;j<=n;j++)
a[i][j] = a[i][j]^a[r][j];
r++;
}
if(r<n)
{
for(int i=r;i<n;i++)
if(a[i][n])//等式左边全是0,如果最右侧不是0,则无解
return 2;
return 1;
}
for(int i=n-1;i>=0;i--)
for(int j=i+1;j<=n;j++)
if(a[i][j]) a[i][n]^=a[j][n];
return 0;
}
int main()
{
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n+1;j++)
cin>>a[i][j];
int t = gauss();
if(t==0)
{
for(int i=0;i<n;i++)
printf("%d\n",a[i][n]);
}
else if(t==1) puts("Multiple sets of solutions");
else puts("No solution");
return 0;
}
求组合数
关键是根据数据范围选择求组合数的方法
基本公式
基本模板
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;//xmodp/ymodp并不等于(x/y)modp,但等于xmodp*y乘法逆元modp
}
递归法求组合数
数据范围:10万次询问,1<=b<=a<=2000 O(n^2)
原理:递推式
推理过程(数学也可证):假设从a个苹果里面选b个 等于 给定一个苹果,
情况一:不选这个苹果,从a-1个里面选b个;
情况二:选这个苹果,从a-1个里面选b-1个。
两种情况之和即结果。
模板
// 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;//当b为0时组合数为1
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
通过预处理逆元的方式求组合数
数据范围:10万次询问,1<=b<=a<=1e5 O(nlogn)
原理:预处理好所有阶乘,fact[i]表示i的阶乘模p,然后利用公式计算
由于两个数取模相除不等于相除再取模
故利用逆元,infact[i]存储的是i的阶乘的逆元
逆元的性质:一个数字除以一个数字b,同余于乘上逆元
综上所述
模板
//首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]
//如果取模的数是质数,可以用费马小定理求逆元,也就是b^(m-2),要用快速幂
//事实上如果模数p不是质数,那有的数字可能不存在乘法逆元,就不能用这个方法写,因为乘法逆元存在的条件是这个数字fact[i]与模数p互质。
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;//infact[0] = 1请这样理解:一个数除以fact[0](=1)等于乘1
for (int i = 1; i < N; i ++ )
{
fact[i] = (LL)fact[i - 1] * i % mod;//n!=(n-1)!*n
infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
//这步解释看接下来的 推导1 ,infact[i]=qmi(fact[i],mod-2,mod);也对但计算量大。
}
推导1:p为质数,可以用费马小定理时:
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, mod = 1e9 + 7;
int fact[N], infact[N];
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()
{
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;
}
int n;
scanf("%d", &n);
while (n -- )
{
int a, b;
scanf("%d%d", &a, &b);
printf("%d\n", (LL)fact[a] * infact[b] % mod * infact[a - b] % mod);
}
return 0;
}
Lucas定理求组合数
**数据范围:20次询问,1<=b<=a<=1e18,1<=p<=1e5 **
Lucas定理内容:(若p是质数,则对于任意整数 1 <= b <= a)
思路,启动lucas(),(写一个计算组合数的函数C),计算C(a%p,b%p),然后对于C([b/p],[a/p]),若b/p,a/p后仍然大于p,则递归lucas()继续处理C([b/p],[a/p]),直到小于p可以被C()计算。
Lucas定理证明:(在Lucas定理所要求条件成立时)
a、b可化为
因为
由于C(p,k)的分子是p!,所以在没约掉分子p的情况下,模p一定等于0,故
此时
对比两边x^b项系数(用二项式定理及p进制数性质可得)
故得证
模板
//若p是质数,则对于任意整数 1 <= m <= n,有:
//C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p)
//n/p与m/p可能仍然大于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;//xmodp/ymodp并不等于(x/y)modp,但等于xmodp*y乘法逆元modp
}
int lucas(LL a, LL b, int p)//LL别忘了
{
if (a < p && b < p) return C(a, b, p);//在a,b都小于p的情况下直接算
return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}
/*
主函数调用lucas(a,b,p),返回值即为C(a,b)%p;
*/
分解质因数法求组合数
数据范围:一次计算 1<=b<=a<=5000 a与b很大同时又不取模,要用到高精度乘法
当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:
筛法求出范围内的所有质数
通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数(以p为例,即a!中p的次数减去b!与(a-b)!次数,即这个式子的质因子p次数)。 n! 中质数p的次数是 [n / p] + [n / p^2] + [n / p^3] + ...
原理:1~a中p的倍数+p^2的倍数+...得到p所有的倍数之和,也就是质因数分解后p是多少次方。(利用“阶乘的质因数分解”)
用高精度乘法将所有质因子相乘
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);//res的初始值是1,然后逐个把每个质因子次方乘上去
for (int i = 0; i < cnt; i ++ ) // 用高精度乘法将所有质因子相乘
for (int j = 0; j < sum[i]; j ++ )//sum[i]记录了primes[i]多少次方,快速幂可能会溢出,还是老老实实n次方就是乘n次计算。某个质数不是它的质因数也没关系,因为这个时候sum[i]=0会直接跳过。
res = mul(res, primes[i]);//大整数乘上某个质因子
问题的方案数:卡特兰数
给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数的序列的数量为: Cat(n) = C(2n, n) / (n + 1)
思路:做一张xy轴图像,x+1代表序列末尾添一个1,y+1代表序列末尾添一个0,要满足任意前缀中0的个数都不少于1的个数,那么这个xy图像一定处处y<=x,也就是在y=x+1这条线之下(或在y=x这条线之上或之下),那么满足条件的序列的个数=总个数-经过y=x+1不满足条件的路径的个数。
总个数=C(2n, n),这很显然,xy轴图像路径由2n次移动形成,其中选n次延x轴前进,那么C(2n, n)便是2n次中选择n次延x轴前进的组合方案总数,也就是总移动方案数。
经过y=x+1不满足条件的路径的个数=C(2n,n-1),由于x与y总数相等,所以最终一定停在y=x上,如果这条路径不满足条件(y<x+1),那么对从路径中不满足条件的点开始的线段做关于y=x+1轴对称,那终点必定落在(n-1,n+1),同时所有终点在(n-1,n+1)的路径都能做到对称后终点在(n,n)且对称后的路径不满足y<x+1要求,两个总数相同,所以”经过y=x+1不满足条件的路径“的个数为C(2n,n-1)。
容斥原理
可以用二进制状态压缩来储存每个集合是否被选上,二进制数1的个数可以反应(-1)的多少次方。(比如一共有3个集合,S1∩S3表示为101)
由于没有顺序要求且是遍历,所以二进制状态压缩可以由循环完成比如是0001~1111就相当于是从1开始,然后小于1<<5
例.AcWing890 能被整除的数
给定一个整数 $n$ 和 $m$ 个不同的质数 $p_1, p_2, …, p_m$。
请你求出 $1 \sim n$ 中能被 $p_1, p_2, …, p_m$ 中的至少一个数整除的整数有多少个。
输入格式
第一行包含整数 $n$ 和 $m$。
第二行包含 $m$ 个质数。
输出格式
输出一个整数,表示满足条件的整数的个数。
数据范围
$1 \le m \le 16$,
$1 \le n,p_i \le 10^9$
输入样例
10 2
2 3
输出样例
7
题解
//1~n中p的倍数的个数:[n/p],原理:n是由n个1相加而成,n/p代表有多少个完整的p个1,这些都是p的1、2..倍数
#include <iostream>
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;
}
博弈论
Nim游戏:问题的核心是谁最终会沦为面对全0无可动的(先手)必输局面,然后从结果反向出发,结合NIM定理。
给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。
我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。
NIM博弈不存在平局,只有先手必胜和先手必败两种情况。
定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0
^为异或运算,相同为0,相异为1。(不进位加法)
先手必胜状态:可以走到某一个必败状态
先手必败状态:走不到任何一个必败状态//必败状态的主语是对方
/*
定理证明:
1.如果所有堆石子数量都是0,那么0^0^...^0=0,谁是先手谁输,因为先手没有办法行动
,故0^0^...^0=0是目标状态。
2.假设说现在是a1^a2...^an=x,假设说x不是0,假设说x最高位1是第k位,那么这堆石子一定存在某个数字ai,它的第k位也是1(如果不存在这么一个数字,不会最后异或出来x第k位是1),又因为ai>ai^x(必然,因为ai第k位变成0,整个数字变小),那一定可以从这堆里拿(ai-(ai^x))个石子,让ai减少为ai^x,那么这个时候,a1^a2^...^ai^...^an=x就变为了a1^a2^...^ai^x^...^an=x^x=0,这个时候后手行动,又会破坏这个异或结果为0,先手继续把它变为0,最后一定能转化为0^0^...^0=0的状态并交给后手,后手输。
*/
台阶-Nim游戏:普通Nim变种
例题:
现在,有一个 n 级台阶的楼梯,每级台阶上都有若干个石子,其中第 i 级台阶上有ai个石子(i≥1)。
两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。
已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
思路:
/*
先手必败思路:所有的奇数台阶都是0^0...^0=0的状态,偶数台阶无论怎么动,后手只要把先手动的石子再移至下一台阶/地面,最终先手一定无石子可动,同时后手无论何时都有可以移动的石子。比如2号台阶有3个石子,先手无论移动多少到1号台阶,后手转手就把他们移到地面,最后先手输;
先手必胜思路:所有奇数台阶石子个数异或结果不为0,这个时候一定可以移动某个奇数台阶一定数量的石子到偶数台阶上,使所有奇数台阶石子个数异或结果为0,此时后手将面临刚刚先手必败思路中先手的情形。
*/
公平组合游戏ICG
若一个游戏满足:
1.由两名玩家交替行动;
2.在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
3.不能行动的玩家判负;
则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。
集合-Nim游戏
有向图游戏
给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。
Mex运算
设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:
mex(S) = min{x}, x属于自然数,且x不属于S
如mex({0,1,3,4})=2.
SG函数
在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:
特别地,整个有向图游戏:图G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。
一个图时,SG(x)==0必败态,SG(x)!=0必胜态,开局状态即决定胜负
解释:
(a)如果SG(x)!=0,意味着x的下一个状态必有0,否则SG(x)=0,所以这步把状态从x移动到0后,对方无法行动,必赢;
(b)如果SG(x)==0,意味着下一步无论如何都到不了0,因为下一个状态如果有0则SG(x)不会有0,所以下一步绝对能到一个状态y,且G(y)!=0,这时轮到对手来操作y状态至下一个状态,根据(a),对手必赢,故此状态是必败态。
有向图游戏的和:多个图,可以选一个图进行操作(改变状态x)时
输的条件:任何一个图都操作不了
设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:
SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)
所有图总SG为 每个图现在状态的SG值异或和 判断现在这个局面操作者必败还是必胜的依据就是这个值是否等于0,与一个图时类似,等于0必败,不等于0(大于0)必胜。
定理:
有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。
/*
定理证明:类比Nim游戏证明
1.所有图目前状态x,都有SG(x)=0,0^0^...^0=0,目前操作者必败
2.SG(G)=z,其中z!=0,那么一定有一个状态Xi,SG(Xi)第k位是1,k就是z的最高位,并且SG(Xi)^z<SG(Xi),根据SG函数定义(Xi下一个状态不能取到的最小自然数,所以Xi下一个状态SG(Xi)^z是能取到的),状态Xi一定可以走到一个新的状态Yi使得SG(Yi)=SG(Xi)^z,这个时候又变回了所有SG值异或为0,往复操作,目前操作者一定可以把状态移动至目标状态0^0^...^0=0,必胜。
*/
应用:如果相较于普通Nim,操作存在限制条件,每次拿只能拿规定数量,那么就把每堆的情况分别列出来,每堆各自有一个有向图表示本堆状态的改变过程。(n堆石子即n个有向图)
注:多个图情况下,SG值也不用重复计算,只要在某个图里计算过一次就行,因为相同的情况,相同的限制条件,状态改变的方法一定一样,结果一定一样
例题:acwing893:m个数字,表示能选取的石子数,n个数字,表示每堆石子数量。
/*
输入样例:
2
2 5
3
2 4 7
输出样例:
Yes
*/
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110,M = 10010;
int n,m;
int s[N],f[M];//s能选的石子个数,f是SG值,SG值不用重复计算,只要在某个图里计算过一次就行,因为相同的情况,相同的限制条件,状态改变的方法一定一样,结果一定一样
int sg(int x)
{
if(f[x]!=-1) return f[x];
unordered_set<int> S;//存目前状态x所有子状态的sg值,而x子状态即目前的石子数x-能被选作减数的s[i]
for(int i=0;i<m;i++)
{
int t=s[i];
if(x>=t) S.insert(sg(x-t));
}
for(int i=0;;i++)//sg(x)=mex(sg(y1),sg(y2),...),而sg(y1)之流具体数字都已经记录到S中,只要挨个从最小自然数检索,找到最小自然数即可。
if(!S.count(i))
return f[x]=i;//同时记得保存这个f[x],记忆化搜索。
}
int main()
{
cin>>m;
for(int i=0;i<m;i++)
cin>>s[i];
cin>>n;
memset(f,-1,sizeof f);//记忆化搜索
int res=0;
for(int i=0;i<n;i++)
{
int x;
cin>>x;
res^=sg(x);
}
if(res) puts("Yes");
else puts("No");
return 0;
}
拆分-Nim游戏:集合-Nim游戏变种
例题
给定 n 堆石子,两位玩家轮流操作,每次操作可以取走其中的一整堆石子,然后放入两堆规模更小的石子(新堆规模可以为 0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
思路
/*
求每一堆石子状态的SG值,每堆石子的状态就是该堆石子的数量,然后最后异或在一起,之后的操作与集合-Nim游戏相同
*/
性质
当一种状态a1转移成另一种状态的时候,如果接下来的状态(b1,b2)由两个有向图构成,那么SG(b1,b2)=SG(b1)^SG(b2)
比如a1的下一个状态有两种(b1,b2)、(c1,c2),那么有:
具体代码(模仿集合-Nim游戏)
#include <algorithm>
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 110;
int f[N];
int sg(int x)
{
if(f[x]!=-1) return f[x];
unordered_set<int> S;
for(int i=0;i<x;i++)//枚举子状态
for(int j=0;j<=i;j++)//避免重复,如(1,2)与(2,1)
S.insert(sg(i)^sg(j));
//mex操作
for(int i=0;;i++)
if(S.count(i)==0)//从最小的自然数开始枚举,找到不存于S(子状态集合)的最小的自然数
return f[x]=i;
}
int main()
{
int n;
cin>>n;
memset(f,-1,sizeof f);
int res=0;
for(int i=0;i<n;i++)
{
int x;
cin>>x;
res^=sg(x);
}
if(res) puts("Yes");
else puts("No");
return 0;
}

浙公网安备 33010602011771号