数论及其应用——积性函数问题
在学习快速幂的过程中,我们曾遇到过因子和函数σ(n),曾提及该函数是积性函数,不过当时并没有给出证明。在这篇文章中,我们将针对数论中的积性函数问题,讨论更多的模型。包括欧拉函数、和我们曾讨论过的gcd函数。
首先我们给出一些定义
定义1:定义在所有正整数上的函数成为算数函数。
定义2:算术函数f如果满足对于任意两个互素的正整数m、n,均由f(mn) = f(m)f(n),就称其为积性函数。如果对于任意的m、n满足上述性质,则称其为完全积性函数。
因子和函数σ(n):
定义:对于整数n所有因子的和,我们记做σ(n)。
下面我们来考虑这样一个问题,我们如何利用编程快速计算σ(n)呢?
法一:首先我们最容易想到的就是暴力法,枚举n的每一个因子嘛,然后求和。
法二:既然涉及到因子,我们很容易想到与整数的拆分密切相关的素数基本定理,n = ∏(p[i]^ei),我们可以构造一个母函数 f = ∏∑pi^j (j∈[0,ei]),便可不重不漏的构造出整数n的所有因子,然后求和即可。
法三:我们还是从暴力枚举的角度来思考,如何更加优化简洁的完成这个枚举过程。其实非常类似桶排序和埃及筛法的思想,我们用一维数组记录a[n]来记录σ(n),我们设置参量i,然后设置j来遍历i的整数倍,表示i是j的一个因子,然后i这个元素"扔到"a[j]这个桶里面,即a[j] += i。
这种自然描述是为了更好解释算法的逻辑细节,虽然显得有些繁琐,其伪码过程如下。
for i
for j 2i to ki
a[j] += i
我们通过一个简单的题目来完成代码实现。(Problem source : hdu 1215)
我们这里利用法三,将数据的最大值考虑进去,参考代码如下。
#include<cstdio> using namespace std; const int maxn = 500002; int a[maxn] = {0 , 0}; void fun() { for(int i = 2;i < maxn;++i) a[i] = 1; for(int i = 2;i < maxn/2;++i) for(int j = i + i;j < maxn;j += i) a[j] += i; } int main() { fun(); int ncase; scanf("%d", &ncase); while(ncase--) { int n; scanf("%d",&n); printf("%d\n",a[n]); } return 0; }
欧拉函数φ(n):
首先,该函数的含义表示不超过n且与n互素的正整数的个数。基于对该函数的含义,我们会得出如下的定理。
定理1:如果p是素数,则φ(p) = p - 1。
定理2:如果p是素数,则φ(p^a) = p^a - p^(a-1)。
证明:不超过p^a的数有p^a 个,这其中我们找到与p^a是非互素关系的数——p、2p、3p、4p……p^(a-1)p,有p^(a-1)个,由此得证。
定理3:设n =∏p^ai是正整数n的素数幂分解,那么有φ(n) = n ∏(1 - 1/pi)。
证明:首先基于算数基本定理,对于任意大于1的正整数我们都可以写成素数幂分解的形式,然后再基于定理2,通过化简整理即可得到欧拉函数的通式。
基于我们给出的这三条定理,我们通过一个题目来具体实现欧拉函数值的求解。(Problem source : pku 2407)
Description
Input
Output
题目大意:典型的计算欧拉函数值的问题。
编程实现:知晓了的计算通式,我们只需要通过简单的编程技巧来实现即可。
实现计算欧拉函数值的方法有很多,这里我们给出直接实现的方法。即遍历出n所有的素因子然后套用公式,优化技巧很类似与我们在《数论及其应用——素数问题》中探讨过的,找素因子只需穷举<=sqrt(n),即可。
参考代码如下。
#include<cstdlib> #include<iostream> using namespace std; int phi(int n) { int rea = n; for(int i = 2;i*i <=n;i++) if(n%i == 0) { rea = rea - rea/i; do n /= i; while(n%i == 0); } if(n > 1) rea = rea - rea/n; return rea; } int main() { int n; while(cin >> n && n) { cout << phi(n) << endl; } return 0; }
我们来看一道应用到欧拉函数的题目。(Problem soure : pku 1284)
Description
Input
Output
题目大意:给出了原根的概念,然后给出一个奇素数p,然你求解奇素数p有多少个原根。
数理分析:这里我们引用数论中的一条现成结论——p是素数,则p有φ(p-1)个原根,基于这条结论,我们就能很好的解决这个问题了。由于时间原因,这里依然暂且折叠这条结论的证明过程,笔者会在后续的深入学习中补充。
参考代码如下。
#include<cstdlib> #include<iostream> using namespace std; int main() { int p; while(cin >> p) { p--; int re = p; for(int i = 2;i*i <= p;i++) if(p%i == 0) { re = re - re/i; do p /= i; while(p%i == 0); } if(p > 1) re = re - re/p; cout << re << endl; } return 0; }
让我们来看一道有关欧拉函数φ(n)的应用。(Problem source : hdu 2588)
题目大意:给定n,m,求解满足x<=n并且gcd(x,n)>=m的x的数目。
数理分析:我们从gcd(x,n) >= m这个条件入手。设gcd(x,n) = d,则有x = p*d , n = q*d,其中gcd(p,q) = 1。因此当符合要求gcd(x,n)一旦确定,我们通过寻找不大于q且与q互素的个数来确定x的个数,而这刚好是欧拉函数φ(q)能够做的事情。
下面我们需要做的便是找到所有满足要求的gcd(x,n),这很简单,通过试除法遍历n所有的因子,然后判断其是否满足大于等于m即可。
分析到这里,你是否有疑惑,对于gcd(x1,n) = d1 , gcd(x2,n) = d2,在两种情况中我们是否会构造出相同的x?即是否需要筛除重复出现的x?
看来我们需要对我们算法的正确性做一个有效的证明。
证明:假设x存在重复的情况
因此有x = p1*d1 = p2*d2.
而记n = q1*d1 = q2*d2.
p1/p2 = q1/q2
这与gcd(p1,q1) = gcd(p2,q2) = 1矛盾,因此假设不成立。
简单的参考代码如下。
#include<cstdlib> #include<cstdio> #include<iostream> using namespace std; int phi(int n) { int rea = n; for(int i = 2;i*i <=n;i++) if(n%i == 0) { rea = rea - rea/i; do n /= i; while(n%i == 0); } if(n > 1) rea = rea - rea/n; return rea; } int main() { int t; while(scanf("%d",&t)!=EOF) { while(t--) { int sum = 0; int n , m; scanf("%d%d",&n,&m); for(int i = 1;i*i <= n;i++) { if(n%i == 0) { if(i >=m) sum += phi(n/i); if(n/i != i && (n/i)>= m) sum += phi(i); } } printf("%d\n",sum); } } return 0; }
基于我们之前对欧拉函数的初涉,这里我们再给出它与另一个积性函数——gcd有关的定理。
定理4:对于定值n,如果d是n的一个约数,则符合gcd(i,n) = d 的i的个数有phi(n/d)个,这里的phi即是欧拉函数。
同样由于时间原因,笔者会在以后的拓展中给出该定理的证明,这里暂时不给出证明。
基于这个定理,我们来通过一个问题具体的应用它。(Problem source : pku 2480)
Description
"Oh, I know, I know!" Longge shouts! But do you know? Please solve it.
Input
Output
那么我们现在让p' = gcd(i,p),q'=gcd(i,q),gcd(p,q) = 1,显然此时的p'*q'是p*q所有的公因子中最大的,而且由于p、q互素,p'和q'也一定互素,因此保证了p'*q'是最大公因子,上述定理得证。
则记f(n) = ∑gcd(i, n),f(n)也是积性函数。
此时我们再基于算术基本定理,即可以把n表示成p1^a1 * p2 ^ a2 * p3 ^ a3 ……的形式,带入到f(n)当中去,即可用积性函数的性质将其展开。
f(n) = f(p1^a1) * f(p2 ^a2)*f(p3^a3) *…… ①
基于定理4,我们得到某个因子的个数,基于n = pi^ai这种形式,其因子我们是可遍历的(pi,pi^2,……pi^ai).
这样我们即可展开f(ai^pi) = f(pi^ai) = Φ(pi^ai)+pi*Φ(pi^(ai-1))+pi^2*Φ(pi^(ai-2))+...+pi^(ai-1)* Φ(pi)+ pi^ai *Φ(1)
带入欧拉函数φ化简后,我们得到f(pi^ai) = ai*(p^r - p^(r-1)) + p^r,再将此式子用到等式①当中,便可求解。
透彻理解了以上的数理分析,编程实现就是简单的模拟。
参考代码如下。
#include<cstdlib> #include<iostream> #include<stdio.h> using namespace std; int main() { long long s , n; while(scanf("%I64d",&n) != -1) { s = 1; long long x ,r; for(long long i = 2;i*i <= n;i++) if(n%i == 0) { x = 1;r = 0; do { n /= i; x *= i; r++; }while(n%i == 0); s *= (r+1)*x- r*x/i; } if(n > 1) s *= (2*n-1); printf("%I64d\n",s); } return 0; }
我们再来看一道简单的有关因子和函数的问题。(Problem source : hdu 1999)
题目表述很简单,其实也不涉及很巧妙的数论证明,类似基于一个映射关系,原象对应着象,这里的原像是m,像则是s(m),现在给出一个值n,是象集合当中的元素,问你整数n是否有对应的原像。
对于这个问题较为抽象的提炼,进行简单的模拟即可解决问题。
值得注意的是,模拟过程首先我们要基于原象和象的一一对应关系,观察到象的数据范围——[1,1000],原像的数据范围应该是多少呢?
简单分析一下可知,小于1000最大的素数是997,那么对于原象997 * 997 , 它对应的原象是997 + 1,这就已经很接近象的最大值了,由此我们不难看到,原象的范围在[1,1000000]是最严谨的。
基于以上分析,参考代码如下。
#include<cstdio> #include<cmath> #include<cstring> bool h[1001]; int main() { int i , j , n , s , o; memset(h , false , sizeof(h)); h[1] = true; for(n = 4;n < 1000000;n++) { s = 1; o = (int)sqrt(double(n)); for(i = 2;i <= o;i++) { if(n%i == 0) { s += i; if(n/i != i) s += n/i; } if(s > 1001) break; } if(s < 1001) h[s] = true; } int t; scanf("%d",&t); while(t--) { scanf("%d",&n); if(h[n]==true) printf("no\n"); else printf("yes\n"); } }
——未完