数论
质数
质数的判定——试除法
即最原始的朴素算法
.”d|n”代表的含义是d能整除n,(这里的”|”代表整除)…
一个合数的约数总是成对出现的,如果d|n,那么(n/d)|n,因此我们判断一个数是否为质数的时候,
只需要判断较小的那一个数能否整除n就行了,即只需枚举i<=(n/i),即i*i<=n,i<=sqrt(n)就行了.
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;
}
分解质因数
从2开始往大除,因此不会出现因数为合数
算数基本定理(唯一分解定理):任何一个大于1的自然数N,如果N不为质数,那么N可以唯一分解成有限个质数的乘积
N
=
P
1
a
1
P
2
a
2
P
3
a
3
.
.
.
.
.
.
P
n
a
n
N=P_1a_1P_2a_2P_3a_3......P_na_n
N=P1a1P2a2P3a3......Pnan
这里P1<P2<P3…<Pn均为质数,其中指数ai是正整数。
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;
}
质数筛
朴素筛
把2~(n-1)中的所有的数的倍数都标记上,最后没有被标记的数就是质数.
假定有一个数p未被2(p-1)中的数标记过,那么说明,不存在2(p-1)中的任何一个数的倍数是p,
时间复杂度约为O(nlogn)
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_primes(int n)
{
for (int i = 2; i < n; i++)//埃式筛(优化后接近o(n))
{
if (!st[i])
{
for (int j = i * i; j < n; j += i)st[j] = 1;
}
}
}
线性筛
这个很好用,后面会经常用
若n在10的6次方的话,线性筛和埃氏筛的时间效率差不多,若n在10的7次方的话,线性筛会比埃氏筛快了大概一倍.
然后我们会发现,当找到质数2是会删去6,找到3时也会删去6,6被重复提起了!
因此我们发现,每次只需删去最小质因子就可以,因此时线性的
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;//p[j]是i的最小质因数,跳过,避免后面重复赋值,优化后能到O(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;
}
求约数个数
(int范围内约数个数最多为一千五百个)
首先,我们要知道,一个数字k可以拆成
k
=
p
1
b
1
+
p
2
b
2
+
.
.
.
.
.
.
+
p
n
b
n
k=p_1^{b_1}+p_2^{b_2}+......+p_n^{b_n}
k=p1b1+p2b2+......+pnbn
而k的约数u,则可以拆成
u
=
p
1
a
1
+
p
2
a
2
+
.
.
.
.
.
.
+
p
n
a
n
u=p_1^{a_1}+p_2^{a_2}+......+p_n^{a_n}
u=p1a1+p2a2+......+pnan
其中
0
<
=
a
<
=
b
0<=a<=b
0<=a<=b
因此,k的约数个数就是a的组合数
int main()
{
int n;
cin >> n;
unordered_map<int, int> primes;
while (n -- )
{
int x;
cin >> x;
for (int i = 2; i <= x / i; i ++ )
while (x % i == 0)
{
x /= i;
primes[i] ++ ;
}
if (x > 1) primes[x] ++ ;
}
long long res = 1;
for (auto p : primes) res = res * (p.second + 1) % mod;
cout << res << endl;
return 0;
}
约数之和
对于质因数p1
∑
P
1
=
p
1
1
+
p
1
2
+
.
.
.
+
p
1
b
1
\sum{P1}=p_1^1+p_1^2+...+p_1^{b_1}
∑P1=p11+p12+...+p1b1
综合就是
∑
P
1
+
∑
P
2
+
.
.
.
+
∑
P
n
\sum{P_1}+\sum{P_2}+...+\sum{P_n}
∑P1+∑P2+...+∑Pn
int main()
{
int n;
cin >> n;
unordered_map<int, int> primes;
while (n -- )
{
int x;
cin >> x;
for (int i = 2; i <= x / i; i ++ )
while (x % i == 0)
{
x /= i;
primes[i] ++ ;
}
if (x > 1) primes[x] ++ ;
}
long long res = 1;
for (auto p : primes)
{
long long a = p.first, b = p.second;
long long t = 1;
while (b -- ) t = (t * a + 1) % mod;
res = res * t % mod;
}
cout << res << endl;
return 0;
}
求最大公约数和最小公倍数
采用欧几里得算法计算最大公约数,根据拉梅定理:用欧几里得算法计算两个正整数的最大公约数,需要的除法次数不会超过两个整数中较小的那个十进制数的位数的 5 倍。这个算法时间复杂度会变得很低,而且还短,好记忆,个人非常喜欢。
欧几里得算法的核心为
g
c
d
(
a
,
b
)
=
g
c
d
(
a
,
a
m
o
d
b
)
gcd(a,b)=gcd(a,a\mod{b})
gcd(a,b)=gcd(a,amodb)
验证很容易,跳过
由此可以得出求解最大公约数的代码gcd
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;//欧几里得算法,求最小公约数,即辗转相除法
}
对此,有定理a、b 两个数的最小公倍数(lcm)乘以它们的最大公约数(gcd)等于 a 和 b 本身的乘积。
即求最小公倍数有
l
c
m
(
a
,
b
)
=
g
c
d
(
a
,
b
)
∗
(
a
,
b
)
lcm(a,b)=gcd(a,b)*(a,b)
lcm(a,b)=gcd(a,b)∗(a,b)
int lcm(int a, int b)
{
return a / gcd(a,b) * b;//即ab积除以a,b的最小公约数,但是怕a*太大,所以先除后乘
}
欧拉函数
定义:
对于一个正整数n,n的欧拉函数ϕ ( n ),表示小于等于n与n互质的正整数的个数
性质(个人总结):
1.其与因数的次数无关,例如N=2100+31000,ϕ(N)=N*(1-1/2)(1-1/3)
公式:
ϕ ( N ) = N ( 1 − 1 p 1 ) ( 1 − 1 p 2 ) . . . ( 1 − 1 p n ) \phi(N)=N (1-\frac1{p_1})(1-\frac1{p_2})...(1-\frac1{p_n}) ϕ(N)=N(1−p11)(1−p21)...(1−pn1)
证明
N = p 1 b 1 + p 2 b 2 + . . . . . . + p n b n N=p_1^{b_1}+p_2^{b_2}+......+p_n^{b_n} N=p1b1+p2b2+......+pnbn
1.去掉这些质因素的倍数,因为这些数i和n有共同的因数pi,
2.加上pi *pj
的倍数,因为pi
与pj
都是n的质因数被减了两次
3.减去pi*pj *pk
,因为这些减了三次,又在第二步加了三次
4.以此类推,即容斥原理,最后得出公式
ϕ
(
N
)
=
N
p
1
+
.
.
.
N
p
n
−
N
p
1
p
2
−
.
.
.
−
N
p
n
−
1
p
n
+
.
.
.
±
N
p
1
p
2
.
.
.
p
n
\phi(N)=\frac{N}{p_1}+...\frac{N}{p_n}-\frac{N}{p_1p_2}-...-\frac{N}{p_{n-1}p_n}+...\pm\frac{N}{p_1p_2...p_n}
ϕ(N)=p1N+...pnN−p1p2N−...−pn−1pnN+...±p1p2...pnN
化简可得如上欧拉函数公式
代码实现
int main()
{
int t;
cin>>t;
while(t--)
{
int n;
cin >> n;
int res=n;
for(int i=2;i<= n/i;i++)
{
if(n%i==0)
{
res=res/i*(i-1);//1-1/i=(1-i)/i,由于从最小质因数开始,所以我if成功的i不会是合数,就实现了上面的公式
while(n%i==0) n/=i;//去掉这个质因数的所有倍数
}
}
if(n>1)res=res/n*(n-1);
cout<<res<<endl;
}
return 0;
}
筛法求欧拉函数
即求多个欧拉函数,时间复杂度O(N)
其与质因数的次数无关,可证
当i % pi == 0时
pi * i的质因数,pi是i的质因子,所以pi * i和i的质因数一样,所以
ϕ
(
p
i
∗
i
)
=
p
i
∗
i
∗
(
1
−
1
p
1
)
(
1
−
1
p
2
)
.
.
.
(
1
−
1
p
n
)
=
ϕ
(
i
)
∗
p
i
\phi({p_i*i})=p_i*i* (1-\frac1{p_1})(1-\frac1{p_2})...(1-\frac1{p_n})=\phi({i})*p_i
ϕ(pi∗i)=pi∗i∗(1−p11)(1−p21)...(1−pn1)=ϕ(i)∗pi
当i % pi ! = 0时
pi * i的质因数,除了i的质因数还多一个pi,所以
ϕ
(
p
i
∗
i
)
=
p
i
∗
i
∗
(
1
−
1
p
1
)
(
1
−
1
p
2
)
.
.
.
(
1
−
1
p
n
)
∗
(
1
−
1
p
i
)
=
ϕ
(
i
)
∗
p
i
∗
(
1
−
1
p
i
)
\phi({p_i*i})=p_i*i* (1-\frac1{p_1})(1-\frac1{p_2})...(1-\frac1{p_n})*(1-\frac1{p_i})=\phi({i})*p_i*(1-\frac1{p_i})
ϕ(pi∗i)=pi∗i∗(1−p11)(1−p21)...(1−pn1)∗(1−pi1)=ϕ(i)∗pi∗(1−pi1)
LL get_eulers (int n)
{
euler[1]=1;
for(int i=2;i<=n;i++)
{
if(!st[i])
{
prime[cnt++]=i;//质数
euler[i]=i-1;//质数的欧拉函数
}
for(int j=0;prime[j]<=n/i;j++)
{
st[prime[j]*i]=true;//标记质数的倍数
if(i%prime[j]==0)//质因数
{
euler[prime[j]*i]=euler[i]*prime[j];//ϕ(pi*i)
break;
}
euler[prime[j]*i]=euler[i]*(prime[j]-1);
}
}
LL sum=0;
for(int i=0;i<=n;i++)
{
sum+=euler[i];
}
return sum;
}
欧拉定理
‘≡’ 是同余符号,例如a≡d(mod c)意思是a和b除以c的余数相等
若a与n互质,即gcd(a,n)=1,则
公式
a ϕ ( n ) ≡ 1 ( m o d n ) a^{\phi_{(n)}} ≡ 1 (mod\;n) aϕ(n)≡1(modn)
意思就是 a 的 φ(n) 次幂除以 n 的余数和1除以n的余数相等
a
ϕ
(
n
)
m
o
d
n
=
1
a^{\phi_{(n)}}\mod{n}=1
aϕ(n)modn=1
证明:
∵ a 1 , a 2 . . . a ϕ ( n ) 与 n 互质, a 与 n 互质 ∴ a 1 ∗ a , a 2 ∗ a . . . a ϕ ( n ) ∗ a 也和 n 互质(可能相对位置发生变化,但是 m o d n 后值不变) ∴ a 1 ∗ a 2 ∗ . . . ∗ a ϕ ( n ) ≡ a ϕ ( n ) ∗ a 1 . . . ∗ a ϕ ( n ) ( m o d n ) ∴ a ϕ ( n ) ≡ 1 ( m o d n ) \because a_1,a_2...a_{\phi({n})}与n互质,a与n互质\\ \therefore a_1*a,a_2*a...a_{\phi({n})}*a也和n互质(可能相对位置发生变化,但是mod\;n后值不变)\\ \therefore a_1*a_2*...*a_{\phi({n})} \equiv a^{\phi({n})}*a_1...*a_{\phi({n})} (\mod n)\\ \therefore a^{\phi({n})}\equiv 1(\mod n) ∵a1,a2...aϕ(n)与n互质,a与n互质∴a1∗a,a2∗a...aϕ(n)∗a也和n互质(可能相对位置发生变化,但是modn后值不变)∴a1∗a2∗...∗aϕ(n)≡aϕ(n)∗a1...∗aϕ(n)(modn)∴aϕ(n)≡1(modn)
ok,现在也是证明完了,那么有什么用呢(我现在也还不知道)
欧拉定理有个推论叫做
费马定理
即当欧拉定理中的n为质数时,ϕ ( n )=n-1,即存在
a
n
−
1
≡
1
(
m
o
d
n
)
a^{n-1} ≡ 1 (mod\;n)
an−1≡1(modn)
快速幂
快速幂模板
公式原理为
x
n
=
(
x
2
)
n
2
x^n=(x^2)^{\frac{n}{2}}
xn=(x2)2n
如此即可节省接近一半的时间,一直递归下去则可使时间复杂度达到O(logn)
int ksm(int x, int y)//递归实现快速幂
{
if (y == 1)return x;
else if (y % 2 == 0)
{
int num = ksm(x, y / 2);
return num * num;
}
else if (y % 2 == 1)
{
int num = ksm(x, y / 2);
return x * num * num;
}
}
如下是非递归的的方式
int ksm(int a, int n) {
int ans = 1;
while (n) {
if (n & 1) //如果n的当前末位为1
ans *= a; //ans乘上当前的a
a *= a; //a自乘
n >>= 1; //n往右移一位
}
return ans;
}
快速幂求逆元
逆元的定义:
对于一个运算,如果存在元素 a
和 b
,使得 a
与 b
进行该运算的结果为单位元(identity element),那么就称 b
是 a
的逆元,a
也是 b
的逆元。其可以将除法运算转化成乘法运算。
于数论中,有判别式
a
∗
b
≡
1
(
m
o
d
n
)
a*b≡1(\mod {n})
a∗b≡1(modn)
即可说在mod n的情况下a与b互为逆元
ok,到此为止,对代码的编写都用不到,只是为了打基础
逆元存在的前提是a与n互质,n为质数,a ^ ( p - 1) ≡ 1 ( mod p) (费马定理)
所以a的逆元为a^(p-1)
#include<iostream>
using namespace std;
typedef long long LL;
LL qmi(int a,int k,int b)
{
LL res=1;
while(k)
{
if(k&1)res=(LL)res*a%b;
a=(LL)a*a%b;
k>>=1;
}
return res;
}
int main()
{
int t;
cin>>t;
while(t--)
{
int a,p;
scanf("%d %d",&a,&p);
LL ans=qmi(a,p-2,p);
if(a%p!=0)printf("%lld\n",ans);
else printf("impossible\n");
}
}
扩展欧几里得算法
裴蜀定理
对于任意正整数a,b,一定存在一对x,y,使得ax+by=gcd(a,b)
扩展欧几里算法目标就是求解给定 n 对正整数 ai,bi,
对于每对数,求出一组 xi,yi,
使其满足 ai × xi+bi × yi=gcd(ai,bi)
公式
a ∗ x + b ∗ y = g c d ( a , b ) = b ∗ y + ( a m o d b ) ∗ y a m o d b = a − ⌊ a / b ⌋ ∗ b a*x+b*y=gcd(a,b)=b*y+(a\;mod{\;b})*y\\ a\;mod{\;b}=a-\lfloor{a/b}\rfloor*b a∗x+b∗y=gcd(a,b)=b∗y+(amodb)∗yamodb=a−⌊a/b⌋∗b
代码实现
int gcd_plus(int a, int b,int &x,int &y)
{
if(b==0)
{
x=1,y=0;
return a;//这里y取0,a就是最大公约数
}
int d=gcd_plus(b,a%b,y,x);//d是最大公约数
y-=a/b*x;//按照公式更新y的值
return d;//返回最大公约数
}
线性同余方程
#include<iostream>
using namespace std;
typedef long long LL;
int gcd_plus(int a, int b,int &x,int &y)
{
if(b==0)
{
x=1,y=0;
return a;//这里y取0,a就是最大公约数
}
int d=gcd_plus(b,a%b,y,x);//d是最大公约数
y-=a/b*x;//按照公式更新y的值
return d;//返回最大公约数
}
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
{
int a,b,c;
cin>>a>>b>>c;
int x,y;
LL d=gcd_plus(a,c,x,y);
LL ans=b / d * x % c;
if(b%d)cout<<"impossible"<<endl;
else cout<<ans<<endl;
}
return 0;
}
中国剩余定理
有 n 个数字 m 1 , m 2 . . . m n , 他们两两互质,求解线性同余方程组 { x = a 1 m o d m 1 x = a 2 m o d m 2 . . . x = a n m o d m n 例如用白话描述就是,现在有一个数不知道是多少,只知道这个数除以 3 余 2 ,除以 5 余 3 ,除以 7 余 2 ,问这个数是多少?设 M = m 1 ∗ m 2 ∗ . . . ∗ m n M 1 = M m 1 , 即除了 m i 外所有 m 的乘积 ( 由于所有数都互质,所以 M i 与 m i 互质 ) 令 M − 1 为 M 的逆 ∴ x = a 1 ∗ M 1 ∗ M 1 − 1 + a 2 ∗ M 2 ∗ M 2 − 1 + . . . + a n ∗ M n ∗ M n − 1 有n个数字m_1,m_2...m_n,他们两两互质,求解线性同余方程组\begin{cases} x=a_1\mod\;m_1\\x=a_2\mod\;m_2\\...\\x=a_n\mod\;m_n \end{cases}\\ 例如用白话描述就是,现在有一个数不知道是多少,只知道这个数除以3余2,除以5余3,除以7余2, 问这个数是多少? 设M=m_1*m_2*...*m_n\\M_1=\frac{M}{m_1},即除了m_i外所有m的乘积\\(由于所有数都互质,所以M_i与m_i互质) \\令M^{-1}为M的逆\\ \therefore x=a_1*M_1*M_1^{-1}+a_2*M_2*M_2^{-1}+...+a_n*M_n*M_n^{-1} 有n个数字m1,m2...mn,他们两两互质,求解线性同余方程组⎩ ⎨ ⎧x=a1modm1x=a2modm2...x=anmodmn例如用白话描述就是,现在有一个数不知道是多少,只知道这个数除以3余2,除以5余3,除以7余2,问这个数是多少?设M=m1∗m2∗...∗mnM1=m1M,即除了mi外所有m的乘积(由于所有数都互质,所以Mi与mi互质)令M−1为M的逆∴x=a1∗M1∗M1−1+a2∗M2∗M2−1+...+an∗Mn∗Mn−1
cpp实现
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 10;
int n;
int A[N], B[N];
LL exgcd(LL a, LL b, LL &x, LL &y) // 扩展欧几里得算法, 求x, y,使得ax + by = gcd(a, b)
{
if (!b)
{
x = 1; y = 0;
return a;
}
LL d = exgcd(b, a % b, y, x);
y -= (a / b) * x;
return d;
}
int main()
{
scanf("%d", &n);
LL M = 1;
for (int i = 0; i < n; i ++ )
{
scanf("%d%d", &A[i], &B[i]);
M *= A[i];
}
LL res = 0;
for (int i = 0; i < n; i ++ )
{
LL Mi = M / A[i];
LL ti, x;
exgcd(Mi, A[i], ti, x);
res = (res + (__int128)B[i] * Mi * ti) % M;
// B[i] * Mi * ti可能会超出long long范围,所以需要转化成__int128
}
cout << (res % M + M) % M << endl;
return 0;
}
高斯消元
在O(N^3)时间复杂度解决n个多元线性方程组
{
b
1
=
a
11
∗
x
1
+
a
12
∗
x
2
+
.
.
.
+
a
1
n
∗
x
n
b
2
=
a
21
∗
x
1
+
a
22
∗
x
1
+
.
.
.
+
a
2
n
∗
x
n
.
.
.
b
n
=
a
n
1
∗
x
1
+
a
n
2
∗
x
2
+
.
.
.
+
a
n
n
∗
x
n
\begin{cases} b_1=a_{11}*x_1+a_{12}*x_2+...+a_{1n}*x_n\\b_2=a_{21}*x_1+a_{22}*x_1+...+a_{2n}*x_n\\... \\b_n=a_{n1}*x_1+a_{n2}*x_2+...+a_{nn}*x_n \end{cases}\\
⎩
⎨
⎧b1=a11∗x1+a12∗x2+...+a1n∗xnb2=a21∗x1+a22∗x1+...+a2n∗xn...bn=an1∗x1+an2∗x2+...+ann∗xn
操作
高斯消元需要进行的操作有三个
1.把某一行乘一个非零的数
2.交换两行
3.把某一行加到另一行上
结果
最终的结果也有三种情况
1.行列式最终为完美阶梯型——有唯一解
2.行列式出现0=0——无穷解
3.行列式出现0=非零——无解
步骤
1.遍历每一列,找到绝对值最大那一行
2.把这一行移到最上面
3.同除一个数使此行最前面的那个数字变成1
4.通过加减此行使此行下方的行的最前面这个数的系数变成0
实现
int gauss() // 高斯消元,答案存于a[i][n]中,0 <= i < n
{
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]; // 将当前行的首位变成1
for (int i = r + 1; i < n; i ++ ) // 用当前行将下面所有的列消成0
{
if (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.当a,b较小时可以预处理,将所有a,b所有情况的值都存下来,通过递推做
c ( a b ) m o d p = ( c ( a − 1 b − 1 ) + c ( a b − 1 ) ) m o d p c\binom{a}{b}\mod{p}=(c\binom{a-1}{b-1}+c\binom{a}{b-1})\mod{p} c(ba)modp=(c(b−1a−1)+c(b−1a))modp
#include<iostream>
using namespace std;
typedef long long LL;
const int mod=1e9+7;
const int N=2020;
int c[N][N];
int main()
{
int n;
cin>>n;
for(int i=0;i<N;i++)
{
for(int j=0;j<=i;j++)
{
if(j==0)c[i][j]=1;
else c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
}
}
for(int i=0;i<n;i++)
{
int a,b;
cin>>a>>b;
cout<<c[a][b]<<endl;
}
return 0;
}
2.当其较大时,就需要换一种方式
c ( a b ) ( m o d N ) = a ! ( b − a ) ! ( ! b ) ! ( m o d N ) = ( a ! m o d N ) ∗ ( ( ( b − a ) ! ) − m o d N ) ∗ ( ( b ! ) − m o d N ) c\binom{a}{b}(\mod N)=\frac{a!}{(b-a)!(!b)!}(\mod N)=(a!\mod N)*(((b-a)!)^-\mod N)*((b!)^-\mod N) c(ba)(modN)=(b−a)!(!b)!a!(modN)=(a!modN)∗(((b−a)!)−modN)∗((b!)−modN)
就是说对分数取模运算不能简单的上模除下模,要转换为逆元进行运算
逆元的计算,即a * b ≡ 1 ( mod N),可以通过快速幂计算逆元,即费马小定理
当然,此处要求N为质数
3.当a,b非常大时
lucas定理
c
(
a
b
)
≡
c
(
a
m
o
d
p
b
m
o
d
p
)
∗
c
(
a
/
p
b
/
p
)
m
o
d
p
c\binom{a}{b}≡c\binom{a\mod{p}}{b\mod{p}}*c\binom{a/p}{b/p}\mod{p}
c(ba)≡c(bmodpamodp)∗c(b/pa/p)modp
LL qmi(int a,int k,int p)//求逆元
{
LL res=1;
while(k)
{
if(k&1)res=(LL)res*a%p;
a=(LL)a*a%p;
k>>=1;
}
return res;
}
LL C(int a,int b,int p)
{
if(b>a)return 0;
int res=1;
for(int i=a,j=1;j<=b;i--,j++)
{
res=(LL)res*i%p;
res=(LL)res*qmi(j,p-2,p)%p;
}
return res;
}
LL lucas(LL a,LL b,int p)
{
if(a<p&&b<p)return C(a,b,p);
else return C(a%p,b%p,p)*lucas(a/p,b/p,p)%p;
}
4.a,b非常大,结合高精度的
这里是直接用的y总的
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 5010;
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)
{
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;
}
int main()
{
int a, b;
cin >> a >> b;
get_primes(a);
for (int i = 0; i < cnt; i ++ )
{
int p = primes[i];
sum[i] = get(a, p) - get(a - b, p) - get(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 -- ) printf("%d", res[i]);
puts("");
return 0;
}
Catalan数
卡特兰数,广泛运用于好多地方,例如合法路径,括号匹配等等
先来介绍题目的具体要求
给定 n 个 0 和 n 个 1,它们将按照某种顺序排成长度为 2n 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 00 的个数都不少于 1 的个数的序列有多少个。
这就是一道合法路径问题
将每一种组合抽象成一条路径,0是向右,1是向左,而题目的要求抽象后就是不接触红线
而每一条接触红线的路线在接触红线那个点开始作轴对称,能发现它一定经过点(n-1,n+1)
因此答案很轻易可以推导出为
C
a
r
a
l
a
n
=
C
(
n
2
n
)
−
C
(
n
−
1
2
n
)
Caralan=C\binom{n}{2n}-C\binom{n-1}{2n}
Caralan=C(2nn)−C(2nn−1)
代码如下
#include <iostream>
#include <algorithm>
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 a = n * 2, b = n;
int res = 1;
for (int i = a; i > a - b; i -- ) res = (LL)res * i % mod;
for (int i = 1; i <= b; i ++ ) res = (LL)res * qmi(i, mod - 2, mod) % mod;
res = (LL)res * qmi(n + 1, mod - 2, mod) % mod;
cout << res << endl;
return 0;
}
容斥原理
其理论非常简单,平时的数学学习中经常使用
代码实现
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 20;
int p[N];
int main()
{
int n,m;
cin>>n>>m;
int res=0;
for(int i=0;i<m;i++)cin>>p[i];
for(int i=1;i<1<<m;i++)//执行2^m次,即所有可能
{
int t=1,cnt=0;//记录和以及集合个数
for(int j=0;j<m;j++)
{
if(i>>j&1)//二的第几位了
{
if((LL)t*p[j]>n)
{
t=-1;
break;
}
t*=p[j];
cnt++;
}
}
if(t!=-1)
{
if(cnt%2)res+=n/t;//奇数加偶数减
else res-=n/t;
}
}
cout<<res;
}
博弈论
SG
函数
意义
这个函数的参数是游戏的状态,并且返回值是一个非负整数,当函数值为 0 时,先手必败,否则先手必胜。
mex
运算
这是一个施加于集合的运算,表示求出这个集合中最小的没有出现过的非负整数,比如 mex{0,2,3}=1,mex{1,2}=0,mex{}=0,mex{1,2,3}=0。
SG
函数的求法
设当前游戏的状态为 x ,后继状态(也就是操作一次后能得到的状态)有 a1,a2,...,ap,
那么SG[x]=mex{SG[a1],SG[a2],...,SG[ap]}。
我们将终点状态定为0,即当状态为0时获胜
因此当先手面对SG[a]=0时,先手无法到0,则此子游戏先手必败。
若是SG[a]!=0,则先手一定可以到0,则此子游戏先手必胜。
SG
定理
一个游戏的SG 函数值等于各个游戏的 SG 函数值的Nim 和(各个游戏即指子游戏,Nim和即异或和)
Nim
游戏
简单来说,将a1到an全部异或,若是0则先手必败,否则先手必胜,具体思路如下
首先,所有情况可以分成两种状态,即必胜状态和必败状态,经验证,改变某个x就可改变两个状态,验证如下
[!NOTE]
一个圆一个加是异或,代码中是^
a 1 ⨁ a 2 ⨁ a 3 ⨁ . . . ⨁ a n = x ∴ 设 x 的第一个 1 在第 k 位 ∴ 一定存在 a i 的第 k 位是 1 ∴ a i ⨁ x < a i ∴ 从 a i 中拿走 a i − a i ⨁ x 个,即可使 a 1 ⨁ a 2 ⨁ a 3 ⨁ . . . ⨁ a n = x = 0 a i = a i − ( a i − a i ⨁ x ) = a i ⨁ x ∵ a 1 ⨁ a 2 ⨁ a 3 ⨁ . . . ⨁ a n ⨁ x = 0 a_1\bigoplus{a_2}\bigoplus{a_3}\bigoplus...\bigoplus{a_n}=x\\ \therefore设x的第一个1在第k位\\ \therefore一定存在a_i的第k位是1\\ \therefore a_i\bigoplus{x}<a_i\\ \therefore 从a_i中拿走a_i-a_i\bigoplus{x}个,即可使a_1\bigoplus{a_2}\bigoplus{a_3}\bigoplus...\bigoplus{a_n}=x=0\\a_i=ai-(a_i-a_i\bigoplus{x})=a_i\bigoplus{x}\\ \because a_1\bigoplus{a_2}\bigoplus{a_3}\bigoplus...\bigoplus{a_n}\bigoplus{x}=0 a1⨁a2⨁a3⨁...⨁an=x∴设x的第一个1在第k位∴一定存在ai的第k位是1∴ai⨁x<ai∴从ai中拿走ai−ai⨁x个,即可使a1⨁a2⨁a3⨁...⨁an=x=0ai=ai−(ai−ai⨁x)=ai⨁x∵a1⨁a2⨁a3⨁...⨁an⨁x=0
代码实现
#include<iostream>
using namespace std;
int main()
{
int n;
cin>>n;
int res=0;
for(int i=0;i<n;i++)
{
int num;
cin>>num;
res=res^num;
}
if(res==0)cout<<"No"<<endl;
else cout<<"Yes"<<endl;
}
台阶-Nim游戏
与经典的Nim游戏不同的是,只能往下一节台阶放,而不是直接拿走,所以只需要考虑奇数阶,因为n阶要拿n次才能到0阶,所以可以看做自己和自己异或n次,所以不需要考虑偶数阶台阶,因为偶数阶台阶自身异或必为0
代码实现
#include <iostream>
#include <algorithm>
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;
}
集合-Nim游戏
与经典Nim游戏不同的是,不能拿随机个数的石子,只能拿给定数组的个数
因此用于计算sg函数的值不是简单的0和1,而是每堆石子的个数和集合给的数字,因此需要改一下sg函数即可
代码实现
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110, M = 10010;
int n, m;
int s[N], f[M];
int sg(int x)
{
if (f[x] != -1) return f[x];
unordered_set<int> S;
for (int i = 0; i < m; i ++ )
{
int sum = s[i];
if (x >= sum) S.insert(sg(x - sum));
}
for (int i = 0; ; i ++ )
if (!S.count(i))
return f[x] = i;
}
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游戏
由于最大值一直在减小,因此一定能结束
[!NOTE]
题目最狗的是用了“放入”两字,误导性很强,需要注意
而由SG定理可知,若x分为两堆,即x状态变成i和j状态,则SG[x]=SG[i]^SG[j]。进行记忆化搜索即可
代码实现
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110;
int n;
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 ++ )
S.insert(sg(i) ^ sg(j));
for (int i = 0;; i ++ )
if (!S.count(i))
return f[x] = i;
}
int main()
{
cin >> n;
memset(f, -1, sizeof f);
int res = 0;
while (n -- )
{
int x;
cin >> x;
res ^= sg(x);
}
if (res) puts("Yes");
else puts("No");
return 0;
}