2018-11-12 20:11:11
数学,特别是数论和计算机科学有着密切的联系,所以也常被选做题材。虽然数学问题大多需要使用特定方法求解,但其中有几个基础算法扮演着重要的角色。
一、辗转相除法
1、求最大公约数
让我们来看一下如下的问题。
问题描述:
给定平面上的两个格点P1(x1, y1)和P2(x2,y2),线段P1P2上,除P1和P2以外一共有几个格点?
限制条件:
-10 ^ 9 <= x1, x2,y1, y2 <= 10 ^ 9
问题求解:
其实本题是两个距离的最大公约数 - 1,要注意特判距离为0时的答案是0。
gcd(a, b) = gcd(b, a % b),直到第二项为0,直接输出第一项。
算法时间复杂度为O(log max(a, b))以内。
int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
2、扩展欧几里得算法
扩展欧几里得定理:对于与不完全为0的非负整数 a, b, gcd(a, b)表示a, b的最大公约数。那么存在整数x,y使得 gcd(a, b)=ax + by。
那么如何高效的求解这个公式呢?
我们不妨设 a>b。
1,显然当 b=0,gcd(a,b)=a。此时 x=1,y=0;
2,ab != 0 时
设 ax 1 +by 1 =gcd(a,b);
bx 2 +(a%b) y 2 =gcd(b,a%b);
根据朴素欧几里德原理有 gcd(a,b)=gcd(b,a%b);
则:ax 1 +by 1 =bx 2 +(a%b)y 2 ;
即:ax 1 +by 1 =bx 2 +(a-(a/b)*b)y 2 =ay 2 +bx 2 -(a/b)*by 2 ;
根据恒等定理得:x 1 =y 2 ; y 1 =x 2 -(a/b)*y 2 ;
这样我们就得到了求解 x1,y1 的方法:x1 ,y1 的值基于 x2 ,y2.
通过上述的变换,就可以将原本求x,y的问题,转化成求x2,y2的问题,并且两个系数在衰减,直到b = 0,答案是x = 1, y = 0。
public class Extgcd {
public int extgcd(int a, int b, int[] x, int[] y) {
if (b == 0) {
int res = a;
x[0] = 1;
y[0] = 0;
return res;
}
else {
int res = extgcd(b, a % b, x, y);
int tmp = x[0];
x[0] = y[0];
y[0] = tmp - a / b * y[0];
return res;
}
}
public static void main(String[] args) {
Extgcd e = new Extgcd();
int[] x = new int[1];
int[] y = new int[1];
System.out.println(e.extgcd(4, 11, x, y));
System.out.println(x[0]);
System.out.println(y[0]);
}
}
二、有关素数的基础算法
素数广泛应用于密码学中,因而也有很多相关算法。不过程序设计竞赛涉及的主要是埃式筛法、简单的素性测试和整数分解这类算法。
1、素性测试
问题描述:
给定正整数n,请判断n是不是正素数。
限制条件:
1 <= n <= 10 ^ 9
问题求解:
所谓素数,就是指恰好有2个约数的整数。因为n的约数都不超过n,因此只需要判断2 - n - 1即可。另外,如果d是n的约数,那么n / d自然也是其约数,所以只需要检测2 - sqrt(n)。
public boolean isPrime(int num) {
for (int i = 2; i * i <= num; i++) {
if (num % i == 0) return false;
}
return num != 1;
}
2、埃式筛法
如果只是对一个数进行素性检测,通常O(sqrt(n))的算法已经够用了。但是如果需要对许多整数进行素性检测,则有更为高效的算法。
问题描述:
给定整数n,请问n以内有多少个素数。
限制条件:
n <= 10 ^ 6
问题求解:
public int sieve(int n) {
int res = 0;
boolean[] isPrime = new boolean[n + 1];
Arrays.fill(isPrime, true);
isPrime[0] = isPrime[1] = false;
for (int i = 2; i <= n; i++) {
if (isPrime[i]) {
res++;
for (int j = 2 * i; j <= n; j += i) {
isPrime[j] = false;
}
}
}
return res;
}
3、区间筛法
问题描述:
给定整数a 和 b,请问区间[a, b)内有多少素数?
限制条件:
a < b <= 10 ^ 12
b - a <= 10 ^ 6
问题求解:
b以内的合数的最小质因数一定是不超过sqrt(b)的,因此如果有了sqrt(b)的素数表,就可以将[a, b)中的素数完全筛选出来。
以下使用POJ #2689测试程序。
需要特别注意一下边界点,如果a = 1,则直接让其自增即可,另外对于长度为1的单个数据点,直接输出即可。
import java.util.Arrays;
import java.util.Scanner;
public class SegmentSieve {
public String segmentSieve(long a, long b) {
if (a + 1 == b) return "There are no adjacent primes.";
boolean[] small = new boolean[(int)Math.sqrt(b)];
boolean[] isPrime = new boolean[(int)(b - a)];
Arrays.fill(small, true);
Arrays.fill(isPrime, true);
for (int i = 2; i < small.length; i++) {
if (small[i]) {
for (int j = i * 2; j < small.length; j += i) small[j] = false;
for (long j = Math.max((long)2, (a % i == 0 ? a / i : (a / i) + 1)) * i; j < b; j += i) isPrime[(int)(j - a)] = false;
}
}
int minLen = isPrime.length;
int maxLen = 0;
int a1 = -1;
int b1 = -1;
int a2 = -1;
int b2 = -1;
int prev = -1;
for (int i = 0; i < isPrime.length; i++) {
if (isPrime[i]) {
if (prev == -1) prev = i;
else {
int len = i - prev;
if (len < minLen) {
minLen = len;
a1 = prev;
b1 = i;
}
if (len > maxLen) {
maxLen = len;
a2 = prev;
b2 = i;
}
}
prev = i;
}
}
if (minLen != isPrime.length) {
return String.format("%d,%d are closest, %d,%d are most distant.", (long)(a1 + a), (long)(b1 + a), (long)(a2 + a), (long)(b2 + a));
}
else return "There are no adjacent primes.";
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
long a = sc.nextLong();
long b = sc.nextLong() + 1;
if ((int) a == 1) a++;
SegmentSieve ss = new SegmentSieve();
System.out.println(ss.segmentSieve(a, b));
}
}
}
三、快速幂运算
除了数学问题之外,也有很多地方用到了幂运算。可以使用反复平方法来进行快速幂运算,时间复杂度为O(logn)。
问题描述:
问题求解:
主要的问题就是在n = Integer.MIN_VALUE的时候,-n会溢出,所以最好把n转成long。
public double myPow(double x, int n) {
return pow(x, (long)n);
}
private double pow(double x, long n) {
if (n == 0) return 1;
if (n < 0) {
n = -n;
x = 1 / x;
}
return (n % 2 == 0) ? pow(x * x, n / 2) : x * pow(x * x, n / 2);
}