// 数论初试:2022/1/19
1.欧几里得算法(Euclid algorithm)
求a,b的最大公约数gcd(a,b)
int gcd(int a,int b){
return b==0?a:gcd(b,a%b);
}
求a,b的最小公倍数lcm(a,b)
int lcm(int a,int b){
return a/gcd(a,b)*b;
}
//原理是 gcd(a,b)*lcm(a,b)=a*b
值得注意的是,因为如果直接移项即,可能会因为a*b超过int范围,即使最终答案在int范围之内,也有可能导致中间过程越界。
2.Eratosthenes筛法(埃氏筛法)
“只对一个整数操作,O(N),已经足够了,如果对许多整数进行素性检测,还有更高效的算法,比如埃氏筛法。
问题:枚举n以内所有素数
操作:先把所有整数列出来,然后把2的倍数全部剔除,然后是三的,以此类推,遍历所有素数,把倍数全部划去。
对于每个数字i,如果没被划去,他一定是素数,因为他不是任何2到i-1数字的倍数。然后就开始划它的倍数就好。”
int a[maxx];
int b[maxx+1];
int gg(int n)
{
int p=0;//记录素数个数
for(int i=0;i<n+1;i++)b[i]=1;
b[0]=0;
b[1]=0;
//准备完毕
for(int i=2;i<=n;i++){
if(b[i]){
a[p++]=i;//记录素数和个数
for(int j=2*i;j<=n;j+=i)b[j]=0;//剔除倍数
}
}
return p;//返回素数个数
}
lrj紫书代码
memset(vis,0,sizeof(vis));
for(int i=2;i<=n;i++){
for(int j=2*i;j<=n;j+=i) vis[j]=1;
}
改进代码
int m=sqrt(n+0.5);
memset(vis,0,sizeof(vis));
for(int i=2;i<=m;i++){
if(!vis[i]){
for(int j=i*i;j<=n;j+=i) vis[j]=1;
}
}
素数定理:
其中,表示不超过x的素数的个数。上述定理的直观含义是:它和比较接近。
3.扩展欧几里得算法
int exgcd(int a,int b,int &x,int &y)//扩展欧几里得算法
{
if(b==0)
{
x=1;y=0;
return a; //到达递归边界开始向上一层返回
}
int r=exgcd(b,a%b,x,y);
int temp=y; //把x y变成上一层的
y=x-(a/b)*y;
x=temp;
return r; //得到a b的最大公因数
}
lrj紫书
void gcd(int a,int b,int& d,int& x,int& y){
if(!b){ d=a; x=1; y=0;}
else{ gcd(b,a%b,d,y,x); y-=x*(a/b);}
}
巫白书
int extgcd(int a, int b, int& x, int& y){
int d=a;
if(b!=0){
d=extgcd(b, a % b, y, x);
y-=(a/b) * x;
}else{
x=1; y=0;
}
return d;
}
4. 同余与模算术
模运算公式
注意在减法中,由于a mod n 可能小于b mod n,需要在结果加上n,而在乘法中,需要注意
a mod n和b mod n相乘是否会溢出。例如,当时,ab mod n一定在int范围内,但是
a mod n 和b mod n的乘积可能会超过int。需要用long long保存中间结果,例如:
int mul_mod(int a, int b, int n){
a %= n; b %= n;
return (int)((long long)a * b % n);
}
大整数取模
输入正整数n和m,输出n mod m的值。
分析:首先把大整数写成“自左向右”的形式:,然后用前面的公式,每步取模,例如:
scanf("%s%d",n,&m);
int len=strlen(n);
int ans=0;
for(int i=0;i<len;i++)
ans=(int)(((long long)ans*10+n[i]-'0')%m);
printf("%d\n",ans);
幂取模
输入正整数a、n和m,输出 的值。
时间复杂度O(n)代码
int pow_mod(int a, int n, int m){
int ans=1;
for(int i=0;i<n;i++) ans=(int)((long long)ans * n % m);
}
时间复杂度O(logn)代码(分治法)
int pow_mod(int a, int n, int m){
if(n==0) return 1;
int x=pow_mod(a,n/2,m);
long long ans=(long long)x*x%m;
if(n%2==1) ans=ans*a%m;
return (int)ans;
}
模线性方程组
输入正整数a,b,n,解方程
分析:本题中发现了一个新记号:同余。的含义是“a和b关于模n同余”,即
a mod n=b mod n. 不难得到,的充要条件是:a-b是n的整数倍。
提示:的含义是“a和b除以n的余数相同”,其充要条件是“a-b是n的整数倍”.
这样原来的方程就可以理解成:ax-b是n的正整数值。设这个“倍数”为y,则,
移项得,这恰好就是紫书10.1.3节介绍的不定方程(a,n,b是已知量,x和y是未知数)!
唯一需要说明的是,如果x是方程的解,满足的其他整数y也是方程的解。因此,当谈到同余方程的一个解时,其实指的是一个同余等价类。
特别情况:方程的解称为a关于模n的逆。当gcd(a,n)=1时,该方程有唯一解;否则,该方程无解。
5. 应用举例
1 UVA11582 巨大的斐波那契数! Colossal Fibonacci Numbers!
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
插播快速幂 : P1226 【模板】快速幂||取余运算
快速幂板子
typedef long long ll;
ll mod_pow(ll x, ll n, ll mod){
if(n==0) return 1;
ll res = mod_pow(x*x%mod, n/2, mod);
if(n&1) res=res*x%mod;
return res;
}
P1226 Code:
#include<bits/stdc++.h>
#define endl "\n"
using namespace std;
typedef long long ll;
ll mod_pow(ll x,ll n,ll mod){
if(n==0) return 1;
ll res=mod_pow(x*x%mod,n/2,mod);
if(n&1) res=res*x%mod;
return res;
}
int main(){
ios::sync_with_stdio(false);
int a,b,p;
cin>>a>>b>>p;
cout<<a<<"^"<<b<<" mod "<<p<<"="<<mod_pow(a,b,p)<<endl;
return 0;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
solution:
所有计算都是对n取模的,不妨设。不难发现,当二元组(F(i),F(i+1))出现重复时,整个序列就开始重复。例如,n=3,序列F(i)的前10项为1,1,2,0,2,2,1,0,1,1,第9、10项和前两项完全一样。根据递推公式,第11项会等于第3项,第12项等于第4项.......
不难发现,因为余数最多n种,所以最多项就会出现重复。则只需要计算出F(0)~F(),然后算出等于其中的哪一项即可。
code:
//UVA11582
#include<bits/stdc++.h>
#define endl "\n"
using namespace std;
typedef unsigned long long ll;
ll a,b;
int n,mod;
int f[1000010]={0,1,1};
//快速幂模板
int quick_pow(ll a, ll b, int mod){
a%=mod;
ll ans=1,base=a;
while(b>0){
if(b&1){
ans*=base;
ans%=mod;
}
base*=base;
base%=mod;
b>>=1;
}
return ans%mod;
}
void solve(){
cin>>a>>b>>n;
if(n==1||a==0){
cout<<"0"<<endl;
return;
}
for(int i=3;i<=n*n+4;i++){
f[i]=(f[i-1]+f[i-2])%n;
if(f[i]==1&&f[i-1]==1){
mod=i-2; // 回溯,mod可以理解为由n确定的一个周期
break;
}
}
cout<<f[quick_pow(a,b,mod)]<<endl;
}
int main(){
ios::sync_with_stdio(false);
int T;
cin>>T;
while(T--){
solve();
}
return 0;
}
学习总结:
看题解的时候发现了 快读的输入、输出版本
inline int read()//输入
{
int x=0,y=1;char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') y=-y;
for(;c>='0'&&c<='9';c=getchar()) x=(x<<3)+(x<<1)+(c^'0');
return x*y;
}
inline void write(int x)//输出
{
if(x<0) x=-x,putchar('-');
if(x>9) write(x/10);
putchar(x%10+'0');
}
2 UVA12169 不爽的裁判 Disgruntled Judge
一个显然的事实是,那么可以通过扩展欧几里得算法(extgcd)根据x1,x3算出a和b来,每次看看X(2i-1)是不是给定的即可。算(a+1)*b的时候需要用逆元,直接预处理处理,注意要开long long
紫书:“如果知道了a,就可以计算出x2,进而根据算出b。有了x1、a和b,就可以在O(T)时间内计算出整个序列了。如果在计算过程中发现和输入矛盾,则这个a是非法的。由于a是0~10000的整数(因为递推公式对10001取模),即使枚举所有的a,时间效率也足够高。”
//UVA12169
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int inf=1<<30;
const int maxn=4e4+5,mod=10001;
int inv[maxn],in[maxn];
int read(){
int x=0,y=1;char c=getchar();
for(;c<'0'||c>'9';c=getchar()) if(c=='-') y=-y;
for(;c>='0'&&c<='9';c=getchar()) x=(x<<3)+(x<<1)+(c^'0');
return x*y;
}
void write(int x){
if(x<0) x=-x,putchar('-');
if(x>9) write(x/10);
putchar(x%10+'0');
}
int extgcd(int a,int b,int& x,int& y){
if(!b){
x=1;y=0;
return a;
}
int res=extgcd(b,a%b,x,y);
int tmp=x;
x=y;
y=tmp-a/b*y;
return res;
}
int getinv(int a){ // ax=1(mod 10001)
int x,y;
int tmp=extgcd(a,mod,x,y);
x/=tmp;
x=(x%mod+mod)%mod;
return x;
}
int main(){
for(int i=0;i<=10000;i++) inv[i]=getinv(i);
int n; n=read();
for(int i=1;i<=n;i++) in[2*i-1]=read();
int a,b;
for(a=0;a<=10000;a++){
LL in3=in[3]-1ll*a*a*in[1]; in3=(in3%mod+mod)%mod;
in3=1ll*in3*inv[a+1]%mod;
int fg=1;
b=(int)in3;
for(int i=2;i<=n*2;i++){
if(i&1){
int xi=(1ll*in[i-1]*a+b)%mod;
if(xi!=in[i]){
fg=0;
break;
}
}else{
in[i]=(1ll*in[i-1]*a+b)%mod;
}
}
if(fg==1) break;
}
for(int i=2;i<=n*2;i+=2){
write(in[i]);
printf("\n");
}
return 0;
}
3 UVA10375 选择与除法 Choose and divide
法一:紫书 分析:本题正是唯一分解定理的用武之地。首先,求出10000以内的所有素数primes,然后用数组e表示当前结果的唯一分解式中各个素数的指数。例如,e={1,0,2,0,0,0,...}表示.
//UVA10375 紫书
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4+10;
int e[maxn],vis[maxn];
vector<int> primes;
//乘以或除以n,其中d=0表示乘,d=-1表示除
void add_integer(int n,int d){
for(int i=0;i<primes.size();i++){
while(n%primes[i]==0){
n/=primes[i];
e[i]+=d;
}
if(n==1) break; // 提前终止循环,节约时间
}
}
//add_factorial(n,d)表示把结果乘以(n!)^d,它的实现如下
void add_factorial(int n,int d){
for(int i=1;i<=n;i++){
add_integer(i,d);
}
}
void Is_Prime(){
for(int i=2;i<=100;i++){
if(!vis[i]){
for(int j=i*i;j<=10000;j+=i){
vis[j]=1;
}
}
}
for(int i=2;i<=10000;i++){
if(!vis[i]) primes.push_back(i);
}
}
int main(){
Is_Prime();
int p,q,r,s;
while(~scanf("%d%d%d%d",&p,&q,&r,&s)){
memset(e,0,sizeof(e));
add_factorial(p,1);
add_factorial(q,-1);
add_factorial(p-q,-1);
add_factorial(r,-1);
add_factorial(s,1);
add_factorial(r-s,1);
double ans=1;
for(int i=0;i<primes.size();i++){
ans*=pow(primes[i],e[i]);
}
printf("%.5lf\n",ans);
}
return 0;
}
法二:
处理多项式的时候我们有 ln+exp
的思路,也可以用来处理大数。
(注意没有ln函数,只有log,就是自然对数)
精度容易丢失,要用 long double
#include<bits/stdc++.h>
using namespace std;
long double res=0.0,a,b,c,d;
void add(long double x){while(x>0)res+=log(x--);}
void cut(long double x){while(x>0)res-=log(x--);}
int main(){
while(scanf("%Lf %Lf %Lf %Lf",&a,&b,&c,&d)==4){
res=0.0;
add(a);cut(a-b);cut(b);
cut(c);add(c-d);add(d);
printf("%0.5Lf\n",exp(res));
}
}
法三:组合数可以边乘边除
对于,分母有mm项,显然分子有m-n+nm−n+n项,即上下项数相等,当两个组合数相除时,这个性质也满足,所以可以边乘边除。
#include<bits/stdc++.h>
inline int max(int a,int b){return a > b ? a : b;}
int main(){
int p,q,r,s;
while(scanf("%d %d %d %d",&p,&q,&r,&s) != EOF){
double ans = 1.00000;
int max1 = max(p - q,q);
int max2 = max(r - s,s);
int max3 = max(max1,max2);
for(int i = 1;i <= max3;i++){
if(i <= max1) ans = ans / i * (p - max1 + i);
if(i <= max2) ans = ans / (r - max2 + i) * i;
}
printf("%.5lf\n",ans);
}
return 0;
}
4 UVA10791 最小公倍数的最小和 Minimum Sum LCM
紫书分析:本题再次用到了唯一分解定理。设唯一分解式,不难发现每个作为一个单独的整数时最优。
如果就这样匆匆编写程序,可能会掉入陷阱。本题有好几个特殊情况要处理:n=1时答案为1+1=2;n只有一种因子时需要加个1,还要注意时不要溢出。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
// 算出n里面最大能除掉多少d,返回d的k次方
int DivedeAll(int &n, int d){
int x = 1;
while (n % d == 0){
n /= d;
x *= d;
}
return x;
}
ll Solve(int n){
if (n == 1){ // 注意特判:1 == 1 * 1
return 2;
}
ll Ans = 0; // 最后的答案
int pf = 0; // 质因子的个数
int m = (int)sqrt(n); // 不要放在循环里,因为要修改n的值
for (int i = 2; i <= m; ++i){
if (n % i == 0){ // i是一个新的质因子
++pf;
Ans += DivedeAll(n, i);
}
}
if (n > 1){ // 如果除到最后还有剩余的,那就把它也加上去
++pf;
Ans += n;
}
if (pf <= 1){
++Ans; // 如果n这个数是个质数,那还要加上1(n == 1 * n)
}
return Ans;
}
int main(){
int n, NO = 0;
while (cin >> n && n){
cout << "Case " << ++NO << ": " << Solve(n) << endl;
}
return 0;
}
题解二:
#include <cstdio>
typedef long long LL;
int nn;
inline void sol(LL n)
{
int f=0;
LL ans=0;
if(n==1)//对1的特判
{
printf("Case %d: 2\n",++nn);
return;
}
LL ttt,tn=n;
for(LL i=2; i*i<=n; ++i)//计算标准分解式,枚举到sqrt即可
{
ttt=1;
if(!(n%i) && (n!=1))
{
do
{
ttt*=i;
n/=i;
}
while(!(n%i) && (n!=1));
f++,ans+=ttt;
}
if(n==1) break;
}
if(tn==n || f==1) ans++;
//tn==n:n是素数,f==1:n不是素数但除1与n外的因子只有一个
if(n!=1) ans+=n;//在sqrt(n)以上除n外还有一个n的因子
printf("Case %d: %lld\n", ++nn, ans);
//最后一行的换行让我很惊讶(UVa什么时候对输出这么随意了?)
return;
}
int main()
{
LL n;
while(scanf("%lld", &n) && n) sol(n);
return 0;
}
题解三:
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<iostream>
using namespace std;
long long n,ans,cnt,q;
int num;
void solve()
{
int tmp=n;
while(n%q==0) n/=q;
if(tmp/n>1) ans+=(tmp/n),num++;
return;
}//加上px^ax
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
while(cin>>n&&n!=0)
{
cnt++;
ans=0;
for(q=2;q<=sqrt(n);q++) solve();
if(n>1) ans+=n,num++;
if(num==0) ans+=2;
if(num==1) ans+=1;//特判
printf("Case %lld: %lld\n",cnt,ans);
num=0;//清零
}
return 0;
}
先介绍紫书的解法:
解法一:时间复杂度
本题看上去很难找到简洁的数学公式,因为gcd和xor看上去似乎毫无相干。不过xor的好处是:,则,所以可以枚举a和c,然后算出,最后验证一下是否有。时间复杂度如何呢?因为c是a的约数,所以和素数筛法类似,时间复杂度为。再加上gcd的时间复杂度为O(logn),所以总的时间复杂度为。
解法二:时间复杂度
我们还可以做得更好。上述程序写出来之后,可以打印一些满足的三元组(a,b,c),然后很容易发现一个现象:。
证明如下:不难发现,且。假设存在c使得,则,与矛盾。
有了这个结论,还是沿用上述算法,枚举a和c,计算b=a-c,则,因此只需验证是否有,时间复杂度降为了。
题解分析:
#include<bits/stdc++.h>
#define maxn 30000001//事先预处理,不然每次处理依旧会T飞
using namespace std;
int ans[maxn];//ans记录答案
void solve(int n){
int k=(n>>1);
for(int b=1;b<=k;b++)//枚举b
for(int a=b+b;a<=n;a+=b)//枚举a
{
if((a xor b)== a-b )//这里注意位运算的优先级,一定要注意打括号(用^也是同样的问题,卡了好久)
ans[a]++;//记录
}
for(int i=2;i<=n;i++)
ans[i]+=ans[i-1];//前缀和
}
int main(){
solve(maxn);
int t,x;
scanf("%d",&t);
for(int i=1;i<=t;i++){
scanf("%d",&x);
printf("Case %d: %d\n",i,ans[x]);//按要求输出
}
return 0;//AC
}
//二 计数与概率基础 2022/01/21
容斥定理:
有重复元素的全排列
问题描述:有k个元素,其中第i个元素有个,求全排列个数。
分析:令所有之和为n,再设答案为x。首先做全排列,然后把所有元素编号,其中第s种元素编号为(例如,有3个a,2个b,先排列成aabba,然后可以编号为)。这样做以后,由于编号后所有元素均不相同,方案总数为n的全排列数n!。根据乘法原理,得到了一个方程:,移项即得:
可重复选择的组合
问题描述:有n个不同元素,每个元素可以选多次,一共选k个元素,有多少种方法?例如,n=3,k=2时有6种:
分析:设第i个元素选个,问题转化为求方程的非负整数解的个数。令,则答案为的正整数解的个数。想象有k+n个数字“1”排成一排,则问题等价于:把这些“1”分成n个部分,有多少种方法?这相当于在k+n-1个“候选分割线”中选n-1个,即。
1 杨辉三角与二项式定理
组合数在组合数学中占有重要地位。与组合数相关的最重要的两个内容是杨辉三角与二项式定理。二项式系数正好和杨辉三角一致。
//时间复杂度为O(n*n)
memset(C,0,sizeof(C));
for(int i=0;i<=n;i++){
C[i][0]=1;
for(int j=1;j<=i;j++){
C[i][j]=C[i-1][j-1]+C[i-1][j];
}
}
//时间复杂度为O(n)
C[0]=1;
for(int i=1;i<=n;i++){
C[i]=C[i-1]*(n-i+1)/i;
}
注意,应该先乘后除,因为C[i-1]/i可能不是整数。但这样一来增加了溢出的可能性——即使最后结果在int或long long范围之内,乘法也可能溢出。如果担心这样的情况出现,可以先约分,不过一般来说是不必要的。
// 2022/1/22更新
1 UVA1635 无关的元素 Irrelevant Elements
只需要依次计算m的唯一分解式中各个素因子在中的指数即可完成判断。这些指数仍然可以用 递推,并且不会涉及高精度。(如果直接递推每个系数除以m的余数,但遗憾的是,递推式中有除法,而模m意义下的逆并不一定存在。)
//待看 待思考
#include<bits/stdc++.h>
#define MAXN 100010
using namespace std;
// 存素数
int prime[MAXN];
// 存指数
int e[MAXN];
int flag[MAXN];
int counter, n, m;
// 把 m 分解成素数和指数的形式
void calFactors(int x) {
memset(e, 0, sizeof(e));
counter = 0;
int _ = (int)(sqrt(x) + 0.5);
for (int i = 2; i <= _; i++) {
if (x % i == 0) {
prime[counter] = i;
while (x % i == 0) {
e[counter]++;
x /= i;
}
counter++;
}
if (x == 1) break;
}
if (x != 1) {
prime[counter] = x;
e[counter++] = 1;
}
}
vector<int> solve() {
memset(flag, 0, sizeof(flag));
n--;
vector<int> v;
for (int i = 0; i < counter; i++) {
int cure = 0;
// 因为系数是对称的所以这里其实只用算一半就行了
// 但是自己比较懒所以就直接从 1 算到 n 了
for (int j = 1; j < n; j++) {
int curu = n - j + 1, curd = j;
while (curu % prime[i] == 0) {
curu /= prime[i];
cure++;
}
while (curd % prime[i] == 0) {
curd /= prime[i];
cure--;
}
if (cure < e[i]) {
flag[j] = 1;
}
}
}
for (int i = 1; i < n; i++) {
if (!flag[i]) v.push_back(i + 1);
}
return v;
}
int main() {
while (~scanf("%d%d", &n, &m)) {
calFactors(m);
vector<int> ans = solve();
printf("%d\n", (int)ans.size());
for (int i = 0; i < ans.size(); i++) {
printf("%d%c", ans[i], i == ans.size() - 1 ? '\n' : ' ');
}
if (ans.size() == 0) printf("\n"); // 如果答案为 0 的话需要输出一行空行
}
return 0;
}