目录
对于数学问题,需要特别严谨,不能随便更改数据,认为正确。
一、基础方面
一、stl:sort()
在定义sort()的比较函数时,比如:
bool cmp(int a,int b){ }
cmp返回true第一个在前面,cmp返回false第二个在前面。sort()使用的是快速排序,不能保证原顺序保持不变,如果需要原顺序保持不变,那就需要一个下标比较。
bool cmp(int & a,int& b){ return a.pos<b.pos;//a.pos小的时候返回true,a放在前面 }
二、stl:数学函数
需要头文件#include<cmath>或#include<math.h>(万能头搞定)
sqrt():求平方根。浮点型
int num; num=sqrt(num);//赋值给整型,向下取整
三、数据类型
①在有些题中,要求结果模去1e9+7,你可能会认为此时不需要开long long因为结果一定在1e9+7内,但是如果用两个1e9的int做乘法,其中间结果会爆int类型,导致答案错误。所以模1e9的问题,最好用long long 一定不会超。
②float t=sqrt(n), t*t==n是成立的,但是int(t)*t==n也是可能成立的,因为n的取值可能刚好使得t无限趋近于int(t),所以我们要做的时候int(t)*int(t)==n才是最严谨的。
③取模运算:
a%b,如果a是负数,那么结果会是负数余数。需要余正数必须这样写(a+b)%b
如果a是正数,那么结果是正数余数。
④类型转换:
long * int ,int会自动转换成long,然后与long运算,结果为long。
(1)long(num)*long(result[0]*result[1]);
(2)long(num)*long(result[0])*long(result[1]);
(3)num*result[0]*long(result[1]);
(4)num*long(result[0])*result[1];
上面的(1)问题在于,result[0]和result[1]两个都是int类型,结果会先爆掉int,然后再转换成long
(2)对的
(3)乘法按顺序进行,num和result[0]是两个int类型,会爆掉,然后再转成long与之后的运算
(4)乘法按顺序进行,result[0]是long类型,num转换成long,运算结果还是long,接着result[1]转换成long,与(2)的结果是一样的。
⑤小心int,用long long!:
#include<bits/stdc++.h> using namespace std; #define INF 998244353 int main(void){ ios_base::sync_with_stdio(0);cin.tie(0); int n; cin>>n; long long ans=0; for(int i=0;i<n;++i){ int x,y; cin>>x>>y; x=y-x; int c=1; long long temp=0; while(x!=1){ temp=INF/x; x=(temp+1)*x%INF; c=(c*(temp+1))%INF; } c=c*y%INF;/****************注意两个int相乘*****************/ ans=((ans+1)*c)%INF; } cout<<ans; return 0; }
x和y<=1e9,你自认为c和ans都不可能爆int,但实际上确实不会,但问题出现在,运算的中间过程爆int。比如c=c*y%INF,c和y都是int型,y=1e9,c=10就会爆掉int,这样写才对:
c=long(c)*y%INF,中间过程计算时其中一个数转成long long!
别给自己添麻烦,别给自己增加错误的可能,定义long long y,long long c!!因为有些竞赛看不到对错,你交了一个int 你自己还不知道中间爆了,多可惜!
二、约数
对于任何一个数N,若其有约数d,则必然有N/d也是N的约数。
当d=N/d时(或者满足d<N/d的最大值),则d=,即当d<=是N的约数时,必然有N/d>=是N的约数。
则约数总是成对出现的,仅当sqrt(N)*sqrt(N)=N时,此时仅算一个
一、求一个数约数的集合—试除法
时间复杂度:O()
只需要判断1~是不是N的约数即可。对于我们特殊处理。
原因在于:除了根号N这个约数之外,其他约数都是成对存在的(而这个成对必然是在根号N的左右),只需要判断到根号N就行。
#include<bits/stdc++.h> using namespace std; int main(void){ int num; cin>>num; vector<int> factor; float j=sqrt(num); //求根号返回值是float,因此我们需要用浮点型存储,不然丧失精度,循环用小于号会出现问题 //根号N的取地板,(根号N-1)*(根号N-1)=N-2*根号N+1 一定不是N的约数。 for(int i=1;i<j;++i){ if(num%i==0){//我们限制i在根号N的取地板之下,必然保证了成对出现 factor.push_back(i); factor.push_back(num/i); } } if(int(j)*int(j)==num) factor.push_back(j); //对于根号N 特殊处理,浮点数要转换成整型,不然任何数还是等于! for(int i=0;i<factor.size();++i){ cout<<factor[i]<<' '; } return 0; }
二、求1~N每个数的正约数集合—倍数法
需要求解1~N中所有数的正约数集合,我们可以采用倍数法,如果我们对每个数采用试除法,时间复杂度为O(N),但是如果我们采用倍数法,则只需要O(NlogN)。
对于每一个数d<=N,它能作为谁的约数?d,2d,3d,4d,地板(N/d) * d,(一共N/d次保存)
从1开始遍历到N,则时间复杂度:
N/1+N/2+N/3+N/4·······+N/N=O(NlogN).
vector<int> factor(500010); for(int d=1;d<=N;++d) for(int i=1;i<=N/d;++i){ factor[d*i].push_back(d); }
三、高精度求最大公因数
考虑最大公因数可以,用质数表示法来考虑。
①当a==b时,gcd(a,b)=a;
②当a和b均为偶数时,gcd(a,b)=2*gcd(a/2,b/2)
③当a是偶数,b是奇数时,根据质数表示法可以发现,此时b一定没有因数2,a和b的最大公因数在a的质数表示去掉2的部分里,因此gcd(a,b)=gcd(a/2,b)
④当a和b均为奇数时,将a和b的质数表示法的最大公因数部分拿出来,则
a-b的因数包含a和b的最大公因数,如果a-b与b还有其他公因数,则b含有该公因数,a-b含有该公因数,则a也含有该公因数,与该公因数不在最大公因数里矛盾,因此a-b与b的最大公因数就是a和b的最大公因数,gcd(a,b)=gcd(a-b,b) //a>b
那么我们在求高精度最大公因数时,可以采用上述方法,逐渐减小两数,直到两数相等。其中我们需要记录的是第②步的2的个数,这样在求出来最后的公因数时,还应当乘以若干个2才是答案。(此时当a==b时,退出递归,然后计算若干个2和a的乘积,得到原始a和b的最大公因数)
三、质数
一、判断一个正整数是否为质数
对于任何一个数n只能有以下六种情况:
n=6m
n=6m+1
n=6m+2
n=6m+3
n=6m+4
n=6m+5
我们可以对一个数模6,除了2和3之外,当其余数为0,2,3,4时,它必然不是一个质数。当它余数为1或5的时候它可能是一个质数。这样我们先筛选出了一部分可能。
bool Is_prime(int num){ if(num==1) return false; if(num==2||num==3) return true; if(num%6!=1&&num%6!=5) return false; if(num%2==0||num%3==0) return false; int j=sqrt(num); for(int i=5;i<=j;i+=6){ if(num%i==0||num%(i+2)==0){ return false; } } return true; } //试除法 //这里判断一个数是否为质数,只需要看看有没有质因子,因此只需要除以可能的质数看看是否余数为0, //并且由于因数成对出现,则只需要判断到 i*i<=num即可 //在2~根号num中没有因数则必然根号num~num-1也没有。
二、对一个数进行质因数分解
一个数N可以写成其质因数分解的形式,我们从小到大遍历质数,遇到是N的因数的质数,则将其记录,并用N除尽该质数,记录次数,然后除尽后的数继续去找它的质因数。由于质因数也是因数,则也只需要遍历到需要找质因数的数的平方根即可。
我们从2开始遍历,一旦N%i==0,则必有i是质数,因为,我们将N进行质因数分解,我们可以发现能整除N的最小值(不考虑1)是N的最小质因数,当我们将这个最小质因数从N中除尽之后,能整除新的N的也是其最小质因数!所以当N%i==0时,必然有i是质数。
当然,我们会发现,当你除到最后一个质因数的时候,并且最后一个质因数的次数为1,一定不会再有N%i==0,因为i等于N的时候才能被除尽,而N*N<=N不成立,因此会退出循环,所以N>1是必要的。
vector<int> ans; vector<int> nums; int N; for(int i=2;i*i<=N;++i){//N不断减小,i不断增大 if(N%i==0){//≤根号N 的一个质因子i ans.push_back(i); nums.push_back(0); while(N%i==0) {nums[nums.size()-1]++;N/=i;} //把该质因数除尽,然后求Ni的质因数,它的最小质因数一定比当前i大~ 所以仍然遍历即可。 } } if(N>1) { //14=2*7 //如果N在除尽了所有≤根号N的质因子之后不等于1,则剩下的这个N一定是一个质数,因为小于等于根号N的质因子都被筛选完了,如果这个数不是质数,则一定存在小于等于根号N的质因子,不然这个数只有两个>根号N的因数,这两个因数的乘积必然大于N,矛盾,因此这个数是质数,所以这一定是大于根号N的一个质数 //大于根号N的质数只能有一个,并且用之前的循环可能筛选不掉,所以需要最后的一次判断。 ans.push_back(N); nums.push_back(1); }
三、欧拉筛
在埃氏筛法:从2开始遍历,将2的所有倍数标记,
接下来用下一个遍历,如果一个数没有被标记,那么这个数一定是质数,原因在于这个数的最小质因数一定比它小,而比它小的质因数已经标记了它的所有倍数,而我没被标记说明,没有任何一个质数是我的因数,我也是质数。
欧拉筛(线性筛):时间复杂度O(n)
在埃氏筛法的基础上,我们只用一个数的最小质因数来标记该数。
算法思路如下:
(1)从2开始到n遍历数i,
如果数i没有被标记没有被标记这个数一定是质数,并且质数也一定不会被标记,我们把这个没有被标记的数i放入到质数集的末尾中。
(2)从最小质数开始遍历当前质数集,记当前质数为p:
标记所有i*p,一旦遇到i%p==0,则这个数不是质数且将其标记,并退出该循环。用最小质数标记了数字i。或者当i*p>N时退出循环,因为这个时候超出了标记范围,之后都会超出。
为什么一定会被最小质数标记?
对于任意一个数i=p*j而言,p是其一个质因子,由于质因子一定会被放入质数集中,且p小于i,因此在循环(1)数字为i时,(2)中一定会遍历到p,由于j比i小,因此之前p*j这个数已经被标记。不会出现没有被标记的情况吗? 不会,因为p是i的最小质因数,则j>p,p已经被放入质数集中,且j中不可能含有比p小的质因数。因此在(1)遍历数字为j时,质数集遍历到p时,循环(2)都不会退出,因为j%p==0是不一定不成立的(如果j%p==0成立,则也会先标记j*p再退出),j%p0==0(p0<p)是不成立的(因为p是i的最小质因数如果成立,则存在i的更小质因数p0)。因此任何遍历到的数字 i 一定会被标记,而且会被其最小质数标记。当出现i%p==0成立时,退出循环即可,不会出现问题,比如即使是这样退出循环,i也会被标记。
由于每个数只标记一次,则时间复杂度是O(n)的。
代码:
vector<int> prime; vector<int> notp(N+1); notp[1]=1;//初始化为0,因此默认一个数是质数,不是质数用1标记 for(int i=2;i<N;++i){ if(!notp[i]) prime.push_back(i); //如果一个数没被标记,其一定是质数,不是质数的一定被其最小质因数标记了。 for(int j=0;j<prime.size();++j){ if(i * prime[j]>N) break;//范围超限,之后都超限,直接不管了 notp[i*prime[j]]=1;//这个数一定不是质数 if(i % prime[j]==0) break;//prime[j]是i的最小质因数! 退出咯 } }
四、数论分块
在求这个问题时,会存在很多相同的商,我们可以对相同的商只求一次,将整数1~n 通过不同的商划分为不同的区间,而同样的结果划分在同一个区间,这就是一个典型的数论分块问题。
我们来思考一下,如果我们不向下取整,那么我们求出来的n/m,这样的值就是带小数点的,在同样一样整数部分,他们在向下取整时被分为一类记为区间x,将该整数部分记为商q。
我们考虑n/q向下取整得到的值什么?一定是在该整数部分,且靠近小数部分是0的那个数,因为,在该区间x内,n除以这里的数,得到的商越小,说明这个数越大,当刚好整除时,这个数达到该区间x内最大的极限,所以在一个连续的i区间内,该用n除以该区间内的商,可以得到一个数,这数向下取整,一定是该i连续区间的商为该数的最大值。因此可以通过左区间数得到区间最右端的数,同时可以得到比该区间小的区间最右端的数。(不能得到比该区间大的,因为比该区间大商并不一定只大1,可能大很多,也就是说,在达到右边区间的商时,永远无法跨出本区间的右端点)
不过 可以用商找比自己小的区间,用除数直接找比自己大的区间。
实际上,由于无论如何i是连续的,可以用本区间的任何一个值,找到同一个商的最大值i,i+1就是下一个区间的第一个值了。
用这个商-1,可以去求比这个区间商小的,区间中最大的数。
五、欧拉函数
欧拉函数的性质:
①对正整数n,欧拉函数是少于或等于n的数中与n互质的数的数目。
因此
②欧拉函数是积性函数,如果gcd(a,b)=1,则。
③当n=,
原因是在1,2,···p,p+1,····2p,2p+1·····3p·····p^k,中,与n不互质的只有p的倍数,一共有p^{k-1}个。
④由以上可得欧拉函数公式。
在计算机中,1/pi,等于0,因此需要先将答案初始化为n
其中n一定可以整除pi。
#include<bits/stdc++.h> using namespace std; int main( ) { int n; cin>>n; int ans=n;//欧拉函数一定比ans小 for(int i=2;i*i<=n;++i){ if(n%i==0){//n的一个≤根号n的质因子i ans=ans/i*(i-1); while(n%i==0){ n/=i; } } } if(n>1) ans=ans/n*(n-1); //n>1,则n是一个大于根号n的质因子,比如n=2,质因子2大于根号2 cout<<ans; return 0; }
六、费马小定理—乘法逆元
这是一个典型的费马小定理的题目,p是一个质数,求n的乘法逆元。不仅是乘法逆元,还能求xa≡b(mod p),只需要先求出逆元,然后再乘以b即可。
一种不用费马小定理解决的方法:
a和p互质,p是质数
xa≡1(mod p)
在求xa≡1(mod p)的时候,我在程序中用到了m=p/a向下取整,那么a*(m+1)%p能够得到一个新数,并且这个数一定小于a,因为p/a一定是不整除的,则a*m<p,但a*(m+1)>p,并且是p<a*(m+1)<p+a的,只有当p/a整除时,a*(m+1)=p+a。因此得到的数a*(m+1)是一个小于a,大于等于1的数,把它当做新的a。并且我们记录(m+1)作为x的一个因子。循环一直到a=1即可。
gpt如是说:
用此方法依然可以解决乘法逆元问题(时间复杂度无法证明),下面方法的基本思路是:
尝试让n乘以数字i(i从2到x),直到这个数模p为1,当然要使得越靠近1,那么就得让n乘以这个数超过p,这样可以模掉变小,所以求出temp来乘。
#include<bits/stdc++.h> using namespace std; int main( ) { long long n; cin>>n; long long x=1; long long p=1e9+7; while(n!=1){ long long temp=p/n; n=(n*(temp+1))%p; x=((temp+1)*x)%p; } cout<<x; return 0; }
费马小定理:
如果p是一个质数,且a不是p的倍数,即gcd(a,p)=1,则满足:
证明:
①若gcd(m,c)=1,且ac≡bc(mod m),则a≡b(mod m)
因为ac-bc≡0(mod m),(a-b)c≡0(mod m),因为gcd(m,c)=1,则a-b是m的倍数
所以,(a-b)≡0(mod m) 即 a≡b(mod m)
②
所以本题只需要求即可
快速幂 & 费马小定理:
#include<bits/stdc++.h> using namespace std; int main( ) { long long n; cin>>n; long long x=1; long long p=1e9+7; long long b=p-2; while(b){ if(b&1) x=(x*n)%p; n=(n*n)%p; b>>=1; } cout<<x; //n^(p-2)%p return 0; }
七、第二类斯特林数
组合数:很好理解,n个里面拿出m个,可以将n分为a1~an,从a2~an里面就拿出m个或者从a2~an里面拿出m-1个,另外再直接拿剩下的这个a1。
S(n,m)=S(n-1,m-1)+mS(n-1,m)
将球标号为a1~an,我们可以考虑a1存放的情况。
第一种情况:a1单独放在一个盒子里,这样只有一种情况,即S(n-1,m-1)
第二种情况:a1与其他球放在同意盒子里,我们这样考虑:
S(n-1,m),假设这个结果是n-1放入m种不同盒子的组合,设为x,每一种组合x[i]。对于剩下的一个球m而言,存在m*x[i]种放法。由于不存在x[j]和x[i]是完全相同的(j≠i),则因此m*x[i]和m*x[j]的情况也是不相同的,因为x[i]是一种组合情况,即使m*x[i]和m*x[j]个别盒子的球一样,他们也不属于同一种组合。
则对于a1而言有mS(n-1,m)种放法。
八、卡特兰数
推导情景:
小明从学校回家,学校在m×n矩阵方格的左下角顶点处,家在矩阵的右上角顶点处,每一次他只能沿着矩阵边沿往上或者往左走。问一共有多少种走法。
我们可以知道,要从学校走到家里,无论何种走法,必须需要往右走n步,往上走m步,一共需要走m+n步,在这m+n步的序列中,拿出m步往上走或拿出n步往左走的组合方式即是答案。
卡特兰数:从原点(0,0)出发,每次向x轴或者y轴正方向移动1个单位,直到到达(n,n)点,且在移动过程中不越过第一象限平分线的移动方案总数。卡特兰数(Catalan)-CSDN博客
在具体求解时,由于数特别大,因此我们可以用分解质因数的方式求解。
九、莫比乌斯反演
例题1:
考虑到要对2~N的所有数求其质因数分解,我们可以用O(N)的时间复杂度去求2~N的所有质数(欧拉筛),然后再去求质因数分解时,只考虑质数即可。
如果不用欧拉筛,可能是可行的,但是速度会慢一些,因为每个数i,都要遍历2~sqrt(i) 去找质数然后除尽。
#include<bits/stdc++.h> using namespace std; int main( ) { ios_base::sync_with_stdio(0); cin.tie(0); int N; cin>>N; int flag[1000001]={};//两重用,第一重欧拉筛,第二重记录质数个数 vector<int> prime; /*----------------欧拉筛--------------------*/ for(int i=2;i<=N;++i){ if(flag[i]==0){ prime.push_back(i); //没被标记i是质数 } for(int j=0;j<prime.size();++j){ if(prime[j]*i>N) break; flag[prime[j]*i]=-1; if(i%prime[j]==0) break; } } /*-----------对所有数进行质因数分解------------*/ for(int i=2;i<=N;++i){ int n=i; for(int j=0;j<prime.size();++j){ while(n%prime[j]==0){ flag[prime[j]]++; n/=prime[j]; } if(prime[j]>sqrt(n)) break; } if(n>1) flag[n]++; } for(int i=2;i<=N;++i){ if(flag[i]>0){ cout<<i<<' '<<flag[i]<<'\n'; } } return 0; }
X、数学思考
一、二分查找
找到target的位置,或者target应该被插入的位置:
int searchInsert(vector<int>& nums, int target) {
int left=0,right=nums.size()-1;
while(left<=right){
int mid=(left+right)>>1;
if(nums[mid]==target) return mid;
if(nums[mid]>target) right=mid-1;
else left=mid+1;
}
return left;
}
(1)实际上 int mid=(left-left + right-left)>>1+left=(right-left)>>1+left;
即我这里想说明的问题是:任何两端区间[left,right],你都能在[0,right-left]中类比查看,它们两个区间只是一个偏移量的问题。
因此我们接下来只需要查看[0,bound]的情况
(2)bound可能是奇数,也可能是偶数;
当bound是奇数时,一共有偶数个数,如0 1 2 3 / 0 1 2 3 4 5,因此mid不是最中间。
当bound是偶数时,一共有奇数个数,mid一定在最中间。
(3)不论哪种情况,bound总在奇 偶 之间变化,最终随着区间不断缩小会变成[0,0] 或者[0,1]这两种情况的一种。不过
(4)left和right的变化实际上就是要找到一个区间[left,right]使之包含target,当区间交错之后,你会发现target的大小,还是在right和left之间的那个位置。因为要交错,必然是其中某一个选择要移动,要移动的目的就是保证target在范围内。