初等数论及其应用——唯一分解定理
写在前面:开这个专栏之前其实是很纠结的,为了博客专栏的分类纠结了一会。这个专栏叫做“初等数论及其应用”,没有按照以前的习惯,用哪本教材命名,实际上我原本也是想结合华章译丛的《初等数论及其应用》,但是手头其实很多资料能说到数论(《训练指南》、《具体数学》等等),因此这里不用哪本书名命名了,而以这个分支的名字命名,专栏下的博客也以数论下的专题或者知识点为标题。这个专栏的作用,主要体现在对数论这个大专题的总结反思与复习,在整理以前略显杂乱的博客以外,顺便做一些提升性的练习(因为笔者发现搞过理论性的东西后,应用的能力真的有待提高……)。因此在博客内容上,然后以应用思维主要侧重点,理论部分主要呈现应用中较为重要的部分。
唯一分解定理一个最重要的应用,就是在于计算非常大的数据时(例如组合数、大的阶乘数),便可以将运算过程放在素数表上,将乘除法变成素数指数的加减法,这样就会避免计算的结果其实很小但是中间过程就爆掉数据类型的情况。
应用1:大型组合数的求解(uva 10375).
已知C(m,n) = m!/(n!(m-n)!),现在给出p、q、r、s(p≥q,r≥s,p、q、r、s≤10000),计算C(p,q)/C(r,s)。输出结果不超过100000000,保留五位小数。
分析:计算组合数时间上没有太大问题,最终的输出结果也不算太大,但是这里如果分别求分母和分子的组合数,考察p、q、r、s的范围,计算10000!是会爆掉c++语言中任何一个数据类型的,因此这里考虑利用分解定理,将待求计算式分解到2~10000的素数表上。
参考代码如下:
#include<stdio.h> #include<iostream> #include<cmath> #include<cstring> const int N = 10001; using namespace std; bool isprime[N]; int prime[N],nprime; int e[N]; void doprime() //埃拉托色尼筛选法 prime[1]~prime[nprime]记录[1,N]区间的所有素数 { long long i , j; nprime = 0; memset(isprime,true,sizeof(isprime)); isprime[1] = 0; for(i = 2;i < N;i++) { if(isprime[i]) { prime[++nprime] = i; for(j = i+i;j < N ;j += i) { isprime[j] = false; } } } } void add_integer(int n , int d) //对整数n进行素数分解,指数存在e[]中,参数d取得正负1用来表示分子和分母的阶乘数。 { for(int i = 1;i <= nprime;i++) { while(n % prime[i] == 0) { n /= prime[i]; e[i] += d; } if(n == 1) break; } } void add_factorial(int n , int d) //将n的阶乘素数分解结果存储在e[]当中,参数d区别该数在分数线上的位置. { for(int i = 1;i <= n;i++) add_integer(i , d); } int main() { doprime(); //for(int i = 1;i <= nprime ;i++) //printf("%d ",prime[i]); int p , q , r , s; while(scanf("%d%d%d%d",&p , &q , &r , &s) != EOF) { memset(e , 0 , sizeof(e)); add_factorial(p , 1); add_factorial(s , 1); add_factorial(r , -1); add_factorial(q , -1); add_factorial(p-q , -1); add_factorial(r-s , 1); double ans = 1.0; for(int i = 1;i <= nprime;i++) ans *= pow(prime[i] , e[i]); printf("%.5lf\n",ans); } }
应用2:一个基于素数分解的最优化问题(uva 10791)
给出一个整数n,n最大可取2^31-1.求得一个长度至少为2的序列,使得这个序列的最小公倍数是n,且在所有满足这个条件的序列中,该序列的元素之和是最小的。求出这个最小元素之和。
分析:这里要讨论一个序列的最小公倍数,凭空构造非常麻烦,因此这里联想到唯一分解定理,我们基于整数n在素数表上的分解,构造一个满足“序列上各元素的最小公倍数是n”这个条件,然后再去考虑如何满足“序列元素之和最小”这个条件。
但是这个问题分析到这里还是不够,因为对于n=1,n只有一个因子的时候,为了满足题设“序列的长度不得小于2”的限制条件,需要进行做出特殊处理。
基于这些分析,便可以进行编码实现了。
简单的参考代码如下:
#include<stdio.h> #include<iostream> #include<cmath> #include<cstring> using namespace std; long long prime_resolve(long long n) { long long ans = 0; for(int i = 2;i <= sqrt(n)+1;i++) { long long temp = 1; while(n % i == 0) { n /= i; temp *= i; } if(temp != 1) ans += temp; if(n == 1) break; } if(n != 1) return ans+n; else return ans; } int main() { long long n; int tt = 1; while(scanf("%lld",&n) && n) { printf("Case %d: ",tt++); long long num = prime_resolve(n); if(n == 1) printf("2\n"); else if(n == num) printf("%lld\n",num+1); else printf("%lld\n",num); } }