代码改变世界

Mathematics for TopCoders[翻译]

2007-03-14 20:48  老博客哈  阅读(1297)  评论(5编辑  收藏  举报

                                                          Mathematics for TopCoders
                【原文见: http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=math_for_topcoders
                                                                                              作者:  By dimkadimon
                                                                                                            Topcoder Member
                                                                                               翻译:农夫三拳@seu(drizzlecrj@gmail.com)

Introduction
    我看到很多参赛者抱怨他们受到不公平的对待因为TopCoder中很多问题太数学化了。就个人而言,我喜欢数学并且因此我对这个看法有偏见。虽然如此,我坚信问题中应该至少包含一些数学,因为数学和计算机科学通常是在一起的。很难想象在这个世界上两个领域可以在没有任何相互交互的情况下存在。这些年,计算机上使用了许多应用数学来解决大量的方程式和一些没有闭合公式的不等式的近似解。计算机科学中广泛的使用了数学,尤其在图算法和计算机视觉方面。

    这篇文章讨论一些很常见的数学构造的理论及实际应用。论题包括:素数,最大公约数,初等几何,基数,分数和复数。

Primes
    一个数如果仅能被1和自身整除就是素数。例如 2,3,5,79,311和1931都是素数,而21不是素数,因为它能够被3和7整除。判断一个数是不是素数我们可以简单的检查它是否能够整除所有小于它的数。我们可以使用取模运算(%)来检查可除性:

for (int i=2; i<n; i++)
   
if (n%i==0return false;

return true;

    这个代码可以运行的更快如果我们注意了仅需要检查那些小于等于n的平方根(把它称为m)的值i的可除性。如果n能够整除一个大于m的数字,那么结果将得到一个小于m的数,因此n同样也可以整除一个小于等于m的数。另外一个优化是不存在大于2的偶素数。一旦我们检查到n不是偶数,我们可以安全的将i的值增加2.我们现在写出最终检查一个数是否为素数的方法:

public boolean isPrime (int n)
{
   
if (n<=1return false;
   
if (n==2return true;
   
if (n%2==0return false;
   
int m=Math.sqrt(n);

   
for (int i=3; i<=m; i+=2)
      
if (n%i==0)
         
return false;

   
return true;
}


现在假定我们想要找到从1到

100000之间所有的素数,那么我们需要调用上面的方法100000次。这是非常低效的,因为我们将同样的计算重复了很多遍。在这种情况下,最好的方法是使用Eratosthenes筛子。Eratosthenes筛子将产生所有从2到给定数字n之间所有的素数。它刚开始假定所有的数字都是素数。然后取出第一个素数,并且除去所有它的倍数。然后将此方法应用在下一个素数上。这样持续下去直到所有的数字都被处理过。例如,考虑找到2到20之间的素数。我们开始写下面的数字:
【译者注:此处删除线显示不出,可参见原文】
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
2是第一个偶数。我们现在划去所有它的倍数,也就是每隔一个的数字:
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
下面一个没有划去的数字是3,因此它是第二个素数。我们现在划去3的所有倍数,也就是从3开始每隔两个数字
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
所有剩下的数字都是素数,因此我们可以安全的结束这个算法。下面是筛法的代码:

public boolean[] sieve(int n)
{
   boolean[] prime
=new boolean[n+1];
   Arrays.fill(prime,
true);
   prime[
0]=false;
   prime[
1]=false;
   
int m=Math.sqrt(n);

   
for (int i=2; i<=m; i++)
      
if (prime[i])
         
for (int k=i*i; k<=n; k+=i)
            prime[k]
=false;

   
return prime;
}
 

在上面的代码中,我们创建了一个素数boolean数组,保存每一个小于等于n的数的素数性。如果prime[i]为true,那么数字i是素数。外层循环找出下一个素数而内层循环移除当前素数的倍数。

GCD
两个数字的最大公约数(GCD)是指能够被a和b同时整除的最大数。比较幼稚的想法,我们可以从两个数中最小的方法开始然后一路往下算直到我们找到一个能够被两者整除的数:

for (int i=Math.min(a,b); i>=1; i--)
   
if (a%i==0 && b%i==0)
      
return i;

尽管这个方法对于大多数应用足够快了,但是有一个更快的算法,欧几里德算法。欧几里德算法迭代两个数直到余数为0。例如,假定我们要找出2336和1314的最大公约数。我们可以用较小数(1314)和余数表示较大的数:

2336 = 1314 x 1 + 1022

我们现在用1314和1022做同样的事情:

1314 = 1022 x 1 + 292

我们重复这个过程直到我们得到余数为0:

1022 = 292 x 3 + 146
292 = 146 x 2 + 0

最后一个非0的余数就是最大公约数。因此2336和1314的最大公约数是146.这个算法可以简单的写成一个递归函数:

//assume that a and b cannot both be 0
public int GCD(int a, int b)
{
   
if (b==0return a;
   
return GCD(b,a%b);
}

用用这个算法我们可以找到两个数字的最小公倍数(LCM)。例如6和9的最小公倍数是18因为18是最小的能够同时整除6和9的数字。下面是用于求LCM的代码:

public int LCM(int a, int b)
{
   
return b*a/GCD(a,b);
}

最后有个注意点,欧几里德算法可以用来解决线性Diophantine方程。这些等式有整数系数,并且形式如下:
ax+by = c
【译者注:可以使用扩展Gcd算法】

Geometry
    有时候有的问题需要我们找出矩形的交集部分。有很多方法来表示一个矩形。对于标准的笛卡尔平面,一个常用的方法是存储左下方和右上方的坐标。

    假设我们有两个矩形R1和R2。(x1,y1)是R1的左下角的坐标,(x2, y2)是右上角的坐标。类似的,(x3,y3)和(x4,y4)分别是R2的边角坐标。R1和R2的交集将是矩形R3,它的左下角的坐标是(max(x1,x3), max(y1,y3)),并且右上角的坐标是(min(x2,x4),min(y2,y4))。如果max(x1,x3)>min(x2,x4)或者max(y1,y3)>min(y2,y4),那么R3就不存在了。也就是说R1和R2不相交。这个方法可以扩展到2维之外,见CuboidJoin(SRM191, Div2 Hard)。

    我们经常需要处理一些顶点具有整数坐标的多边形。这些多边形称为格点多边形。lbackstrom在他的关于几何概念的教程中展示一个简洁的方法来查找一个给定顶点的多边形的面积。现在,假设我们不需要知道这些顶点的精确坐标,取而代之的我们有两个值:

= 多边形边界上的格点数目
= 多边形内部的各点数目

令人惊讶的是,这个多边形的面积是:

Area = B / 2 + I - 1

上面的公式称为皮克定理(Georg Alexander Pick(1859-1943))。为了显示皮克定理对所有的格点多边形适用,我们需要在4个分离的部分上进行证明。第一个部分我们证明它可以适用所有的格点矩形(边平行与轴)。由于一个直角三角形可以被看成矩形的一半,因此证明这个定理同样适用于任何直角三角形(边平行于轴)不是难事。下一步考虑一个普通的三角形,这个可以被看成一个被切除了一些直角三角形的矩形。最后,我们可以证明如果定理对任意两个拥有公共边的格点三角形成立的话,它同样适用于移除公共边的格点多边形。将上面的结果进行整合并且基于任何多边形都可以由三角形组成的事实,我们得到了最终版本的皮克定理。皮克定理在我们需要找到一个大的多边形内部的格数时非常有用。

    另外一个值得记住的是多面体的欧拉公式。一个多面体是一个被划分成多个小的多边形的多边形。小的多边形被称为面,面的一侧被称为边并且面的顶点通常称作顶点。欧拉公式表述为:

- E + F = 2,这里
V
=顶点数目
E
=边的数目
F
=面的数目

例如,考虑一个除去对角线的三角形。我们有V=5,E=8和F=5(正方形的外面也是一个面),有V-E+F = 2.
我们可以说明欧拉公式是如何工作的。我们首先使用V=2,因为每个顶点至少在一个边上。如果V=2,那么只有一种可能类型的多面体。它由E条边连接起来的两个顶点。我们可以假定V-E+F=2对于2<=V<=n是正确的。让V=n + 1,任意选择一个顶点w。现在假设w由多面体中剩下的G条边连接。如果移除w和所有的这些边,我们有了一个n个顶点,E-G条边和F-G+1个面。根据我们的假设,我们有:

(n) - (E - G) + (F - G + 1= 2
因此 (n
+1- E + F = 2

因为V=n+1,我们有V-E+F=2.因此通过数学原理的说明,我们已经证明了欧拉公式。

Bases
    TopCoder参赛者经常遇到的一个问题是十进制和二进制之间的来回转换(在众多转换之中)。
    那么这些数字的基数意味着什么呢?我们将从标准(十进制)基数上谈起。考虑十进制数4325。4325可以表示成5+2*10+3*10*10+4*10*10*10。注意在我们从右向左的扫描中,每一个后续的数字将增加10的倍数。二进制数也是类似这样的,当我们从右向左扫描时每个单独的0和1值会乘以一个增加2倍的因子。例如,1011在2进制中在十进制中表示成1+1*2+0*2*2+1*2*2*2=1+2+8=11。我们仅仅是将一个二进制的数转换为了十进制。对于其他进制也是一样。下面的代码是将b(2<=b<=10)进制的数n为一个十进制数:

public int toDecimal(int n, int b)
{
   
int result=0;
   
int multiplier=1;
      
   
while(n>0)
   
{
      result
+=n%10*multiplier;
      multiplier
*=b;
      n
/=10;
   }

      
   
return result;
}

使用Java的人直到上面的过程可以写成如下会很高兴:

return Integer.parseInt(""+n,b);

    将一个十进制转换为一个二进制也相当的简单。假定我们想要将10进制数43转换为2进制。在每一步方法中我们将43处理2并且记录下余数。最后余数的列表就是二进制的表示形式:

43/2 = 21 + remainder 1
21/2 = 10 + remainder 1
10/2 = 5  + remainder 0
5/2  = 2  + remainder 1
2/2  = 1  + remainder 0
1/2  = 0  + remainder 1

    因此43的二进制形式为101011.通过将前面方法中所有10的出现位置替换成b,我们创建了一个能够将十进制数n变为b进制的数(2<=b<=10):

public int fromDecimal(int n, int b)
{
   
int result=0;
   
int multiplier=1;
      
   
while(n>0)
   
{
      result
+=n%b*multiplier;
      multiplier
*=10;
      n
/=b;
   }

      
   
return result;
}

   如果上面的基数b超过10,我们就必须使用非数值字符来表示大于或者等于10的数字了。我们可以用'A'代表10,'B'代表11,一次类推。下面的代码能够将一个10进制数转换为任何进制(到20进制):

public String fromDecimal2(int n, int b)
{
   String chars
="0123456789ABCDEFGHIJ";
   String result
="";
      
   
while(n>0)
   
{
      result
=chars.charAt(n%b) + result;
      n
/=b;
   }

      
   
return result;
}

    在Java当中,当把10进制的数转换位其他一些常见的表示实,例如二进制(基数为2),八进制(基数为8)和十六进制(基数为16):

Integer.toBinaryString(n);
Integer.toOctalString(n);
Integer.toHexString(n);

Fractions and Complex Numbers
   分数出现在许多问题中。也许最难处理分数的方面就是找出恰当的方法来表示它们。尽管创建一个包含必要的属性和方法的类是有必要的,但是大多数情况下用一个包含2个元素的数组(对)来表示就足够了。思路是我们在第一个元素中存储分子,在第二个元素中存储分母。我们就拿两个分数a和b的乘法开始:

public int[] multiplyFractions(int[] a, int[] b)
{
   
int[] c={a[0]*b[0], a[1]*b[1]};
   
return c;
}

   分数的加法稍微有些复杂,因为只有具有相同分母的分数才可以在一起相加。首先我们必须找出两个分数的公共分母,然后进行乘法来将分数进行变换,使得它们将公共分母作为分母。公共的分母就是能够整除两者分母的数,就是两个分母的LCM(前面定义过了)。例如让我们用4/9加上1/6。9和6的最小公倍数是18,因此转换第一个分数我们需要乘以2/2,而第二个需要乘以3/3:

4/9 + 1/6 = (4*2)/(9*2+ (1*3)/(6*3= 8/18 + 3/18

一旦两个分数具有了相同的分母,我们可以将两者的分子相加得到最终的答案11/18。加法和这个类似,除了最后一步是减法:

4/9 - 1/6 =  8/18 -  3/18 =  5/18

下面是两个分数相加的代码:

public int[] addFractions(int[] a, int[] b)
{
   
int denom=LCM(a[1],b[1]);
   
int[] c={denom/a[1]*a[0+ denom/b[1]*b[0], denom};
   
return c;
}

最后需要直到如何将分数化为最简形式。一个分数的最简形式是指分子和分母的最大公约数为1。我们可以像这样做:

public void reduceFraction(int[] a)
{
   
int b=GCD(a[0],a[1]);
   a[
0]/=b;
   a[
1]/=b;
}

 

使用一个类似的方法我们可以表示其他特殊的数字,例如复数。一般来说,复数是a+ib形式的数字,这里a和b都是实数并且i是-1的平方根。例如,将两个复数m=a+ib和n=c+id相加,我们可以简单的将两者组合起来:

+ n
= (a + ib) + (c + id)
= (a + c) + i(b + d)

两个复数相乘和两个实数相乘也是一样的,除了我们必须使用i^2=-1:

* n
= (a + ib) * (c + id)
= ac + iad + ibc + (i^2)bd
= (ac - bd) + i(ad + bc)

    通过在数组的第一个元素中存储实部,在第二个元素中存储虚部,我们可以进行上述乘法:

public int[] multiplyComplex(int[] m, int[] n)
{
   
int[] prod = {m[0]*n[0- m[1]*n[1], m[0]*n[1+ m[1]*n[0]};
   
return prod;
}

Conclusion
结尾我想补充一点,任何一个不了解本篇文章中数学构造和算法大纲的人不可能排名在TopCoder的前面。也许数学问题中最常见的论题就是素数问题了。接下来的就是关于基数的论题了,也许是因为计算机都是用二进制运算的因此我们需要了解如何将二进制转换为十进制。GCD和LCM的概念在纯粹的数学和几何问题中都很常见。最后,我总结了一个在TopCoder比赛中不是很有用的但是其他方面很有用的论题,它提供了一个处理特定数字的方法。