生成独特有序的勾股三元组

31

这是我编写的计算毕达哥拉斯三元组的程序。运行程序时,由于if语句的存在,每组三元组都会被打印两次。有没有办法让程序只打印一组新的三元组呢?谢谢。

import math

def main():
    for x in range (1, 1000):
        for y in range (1, 1000):
            for z in range(1, 1000):
                if x*x == y*y + z*z:
                    print y, z, x
                    print '-'*50

if __name__ == '__main__':
    main()  

15
那是针对欧拉计划第9题的吗? - starblue
21个回答

83

勾股数三元组是一个很好的例子,证明了“for循环有害论”的正确性,因为for循环会引导我们去考虑计数,而这通常是任务中最不相关的部分。

(为了避免编程语言偏见,并保持伪代码的简洁性,我将不优化例如x * xy * y的多次计算。)

版本1:

for x in 1..N {
    for y in 1..N {
        for z in 1..N {
            if x * x + y * y == z * z then {
                // use x, y, z
            }
        }
    }
}

这是最糟糕的解决方案。它会产生重复,并遍历不必要的空间部分(例如每当z < y时)。它的时间复杂度在N上为立方级别。

Version 2,第一个改进,要求x < y < z成立,如下所示:

for x in 1..N {
    for y in x+1..N {
        for z in y+1..N {
            if x * x + y * y == z * z then {
                // use x, y, z
            }
        }
    }
}

该算法减少了运行时间并消除了重复的解决方案。但是,对于N仍然是三次的;改进只是减少N的立方系数。

z * z < x * x + y * y不再成立后,继续检查增加的z值是没有意义的。这个事实促使版本3从暴力迭代z步骤中退出:

for x in 1..N {
    for y in x+1..N {
        z = y + 1
        while z * z < x * x + y * y {
            z = z + 1
        }
        if z * z == x * x + y * y and z <= N then {
            // use x, y, z
        }
    }
}

对于1000个数据,版本4比版本2快了约5倍,但它仍然是关于 N 的立方阶。

下一个见解是 xy 是唯一的自变量;z 取决于它们的值,上一个考虑过的 y 值的最后一个 z 值是下一个 y 值的良好起始搜索值。这导致了版本4:

for x in 1..N {
    y = x+1
    z = y+1
    while z <= N {
        while z * z < x * x + y * y {
            z = z + 1
        }
        if z * z == x * x + y * y and z <= N then {
            // use x, y, z
        }
        y = y + 1
    }
}

这种改进允许yz只扫描x以上的值一次。当N为1000时,它不仅比"计数循环"快100倍以上,而且在N上是二次的,因此随着N增加,速度提升更大。

我遇到了这种改进的情况足够多,以至于对于任何非微不足道的用途(例如遍历数组),我都会对"计数循环"持怀疑态度。

更新:显然,我应该指出V4中一些容易忽略的事情。

  1. 两个while循环都由z的值控制(一个直接,另一个通过z的平方间接控制)。内部的while实际上加速了外部的while,而不是与其正交。重要的是要看看循环在做什么,而不仅仅是计算有多少个循环。

  2. V4中的所有计算都是严格的整数算术。与之相比,转换为/从浮点数,以及浮点数计算是很昂贵的。

  3. V4在常量内存中运行,仅需要三个整数变量。没有需要分配和初始化的数组或哈希表(潜在地会导致内存不足错误)。

  4. 最初的问题允许所有xyz在相同的范围内变化。V1..V4遵循这种模式。

下面是一组不太科学的计时结果(在我的旧笔记本电脑上使用Eclipse的Java和其他东西运行...),其中"使用x,y,z"是通过实例化一个具有三个值的Triple对象并将其放入ArrayList中实现的。(对于这些运行,N被设置为10000,在每种情况下产生了12471个三元组。)

Version 4:           46 sec.
using square root:  134 sec.
array and map:      400 sec.

"数组和映射"算法基本上是:

squares = array of i*i for i in 1 .. N
roots = map of i*i -> i for i in 1 .. N
for x in 1 .. N
    for y in x+1 .. N
        z = roots[squares[x] + squares[y]]
        if z exists use x, y, z

“使用平方根”算法本质上是:

for x in 1 .. N
    for y in x+1 .. N
        z = (int) sqrt(x * x + y * y)
        if z * z == x * x + y * y then use x, y, z

V4的实际代码如下:

public Collection<Triple> byBetterWhileLoop() {
    Collection<Triple> result = new ArrayList<Triple>(limit);
    for (int x = 1; x < limit; ++x) {
        int xx = x * x;
        int y = x + 1;
        int z = y + 1;
        while (z <= limit) {
            int zz = xx + y * y;
            while (z * z < zz) {++z;}
            if (z * z == zz && z <= limit) {
                result.add(new Triple(x, y, z));
            }
            ++y;
        }
    }
    return result;
}

注意,x * x是在外部循环中计算的(尽管我没有打算缓存z * z);其他变体也进行了类似的优化。

如果需要,我很乐意提供其他我测试过的变体的Java源代码,以防我实现有误。


在你的第四个版本中你仍然有3个嵌套循环,因此计算前10,000个勾股三元组将需要数天时间。你可能想要尝试一下ΤΖΩΤΖΙΟΥ或者我的算法来实现一个只有两层循环的解决方案。 - MiniQuark
2
你不理解V4与V1-V3的区别。在我的笔记本电脑上,计算前12471个三元组花费了49秒。两个“while”循环都在测试z的值。 - joel.neely
@joel:我必须承认,我很难理解你的算法,但我刚刚运行了它,并且计算pythagore_triplets_v4(10000)花费了61秒,所以我相信你:它看起来是二次的。但是你的算法仍然比我的“数组和映射”算法慢50%。 - MiniQuark
@joel:有趣的是Python和Java之间的性能差异显然很大:根据您上面的结果,不需要进一步讨论,您的v4算法是完美的。但是在Python中,v4比我的“数组和映射”算法慢50%。总的来说,我建议使用v4。 - MiniQuark
@joel: 我终于明白了这个算法。花了我一些时间。;-) 做得好。 - MiniQuark
你想在Java中比较我的V.2(或可能是V.3)算法和你的V4吗?将你的V4转换为Python后,似乎我的V.2比你的快约1.7倍,所以我想知道在Java方面会发生什么。提前感谢。 - tzot

51

比目前任何解决方案都要快得多。通过三叉树找到三元组。

Wolfram表示:

Hall (1970) and Roberts (1977) prove that (a, b, c) is a primitive Pythagorean triple if and only if

(a,b,c)=(3,4,5)M

where M is a finite product of the matrices U, A, D.

这里有一个生成每个原始三元组的公式。

在上述公式中,斜边不断增长,所以很容易检查最大长度。

在Python中:

import numpy as np

def gen_prim_pyth_trips(limit=None):
    u = np.mat(' 1  2  2; -2 -1 -2; 2 2 3')
    a = np.mat(' 1  2  2;  2  1  2; 2 2 3')
    d = np.mat('-1 -2 -2;  2  1  2; 2 2 3')
    uad = np.array([u, a, d])
    m = np.array([3, 4, 5])
    while m.size:
        m = m.reshape(-1, 3)
        if limit:
            m = m[m[:, 2] <= limit]
        yield from m
        m = np.dot(m, uad)

如果您想要所有三元组而不仅仅是基本元素:
def gen_all_pyth_trips(limit):
    for prim in gen_prim_pyth_trips(limit):
        i = prim
        for _ in range(limit//prim[2]):
            yield i
            i = i + prim

list(gen_prim_pyth_trips(10**4))花费2.81毫秒返回了1593个元素,而list(gen_all_pyth_trips(10**4))花费19.8毫秒返回了12471个元素。

参考资料,Python中的 被接受的答案对于12471个元素需要38秒。

只是为了好玩,将上限设置为一百万,list(gen_all_pyth_trips(10**6))在2.66秒内返回1980642个元素(在3秒内几乎有两百万个三元组)。list(gen_all_pyth_trips(10**7))会使我的计算机崩溃,因为列表变得如此之大,它消耗了所有的RAM。做类似sum(1 for _ in gen_all_pyth_trips(10**7))的操作可以避免这种限制,并在30秒内返回23471475个元素。

有关使用的算法的更多信息,请查看WolframWikipedia上的文章。


2
到目前为止最有效的代码。确切地说,它是Python 3代码。 - sono

12

你应该定义 x < y < z。

for x in range (1, 1000):
    for y in range (x + 1, 1000):
            for z in range(y + 1, 1000):

另一个好的优化方法是仅使用x和y,并计算zsqr = x * x + y * y。如果zsqr是一个完全平方数(或者z = sqrt(zsqr)是一个整数),那么它就是三元组,否则不是。这样,您只需要两个循环而不是三个循环(对于您的示例,快约1000倍)。


这一切都很有道理,但是请检查你的变量名!你把x、y和z搞混了。 :-) - Pitarou
已经修复了,谢谢。现在zsqr = x * x + y * y就像应该的那样。 - schnaader
问题实际上定义了 x+y+z=1000,因此您可以使用简单的 z = 1000-x-y 完全消除第三个循环。 - annakata
你不需要三个嵌套循环:可以看看ΤΖΩΤΖΙΟΥ的算法或我的算法。如果你想找到前一万个勾股三元组,我们的算法会节省你数天的计算时间。 - MiniQuark
1
我的回答是关于楼主的原始帖子,而不是欧拉计划第9题(他一开始没有回答是否与此问题有关)。在这种情况下,您还可以使用2个循环,并在x+y+z=1000时简单退出。任何进一步的优化都没有意义,因为运行时间少于1秒。 - schnaader

12
之前列出的生成勾股数的算法都是基于基本关系a^2 + b^2 = c^2(其中(a, b, c)是三个正整数的三元组)的朴素方法的修改。事实证明,勾股数满足一些相当显著的关系,可以用来生成所有的勾股数。 欧几里得发现了第一个这样的关系。他确定了每个勾股数(a, b, c),在可能重新排序ab之后,都有相对质的正整数mn,其中至少一个是偶数,以及一个正整数k,使得:
a = k (2mn)
b = k (m^2 - n^2)
c = k (m^2 + n^2)

要生成勾股数三元组,需要生成两个不同奇偶性的互质正整数 mn,以及一个正整数 k 并应用上述公式。

struct PythagoreanTriple {
    public int a { get; private set; }
    public int b { get; private set; }
    public int c { get; private set; }

    public PythagoreanTriple(int a, int b, int c) : this() {
        this.a = a < b ? a : b;
        this.b = b < a ? a : b;
        this.c = c;
    }

    public override string ToString() {
        return String.Format("a = {0}, b = {1}, c = {2}", a, b, c);
    }

    public static IEnumerable<PythagoreanTriple> GenerateTriples(int max) {
        var triples = new List<PythagoreanTriple>();
        for (int m = 1; m <= max / 2; m++) {
            for (int n = 1 + (m % 2); n < m; n += 2) {
                if (m.IsRelativelyPrimeTo(n)) {
                    for (int k = 1; k <= max / (m * m + n * n); k++) {
                        triples.Add(EuclidTriple(m, n, k));
                    }
                }
            }
        }

        return triples;
    }

    private static PythagoreanTriple EuclidTriple(int m, int n, int k) {
        int msquared = m * m;
        int nsquared = n * n;
        return new PythagoreanTriple(k * 2 * m * n, k * (msquared - nsquared), k * (msquared + nsquared));
    }
}

public static class IntegerExtensions {
    private static int GreatestCommonDivisor(int m, int n) {
        return (n == 0 ? m : GreatestCommonDivisor(n, m % n));
    }

    public static bool IsRelativelyPrimeTo(this int m, int n) {
        return GreatestCommonDivisor(m, n) == 1;
    }
}

class Program {
    static void Main(string[] args) {
        PythagoreanTriple.GenerateTriples(1000).ToList().ForEach(t => Console.WriteLine(t));            
    }
}

生成勾股三元组的公式的维基百科文章中包含其他类似的公式。


三叉树方法非常好(请参阅维基百科文章)。 - starblue
1
m <= max / 2 应该被优化为 m <= (int) Math.sqrt(max/2)。否则,此算法不会比上述算法更快。 - Cheng

8

算法可以针对速度、内存使用、简单性和其他方面进行调整。

这里有一个针对速度进行调整的pythagore_triplets算法,但代价是内存使用和简单性。如果你只关心速度,这可能是一种选择。

在我的电脑上,计算list(pythagore_triplets(10000))需要40秒,而ΤΖΩΤΖΙΟΥ的算法需要63秒,而Tafkas的算法(以及所有使用3个嵌套循环而不仅仅是2个循环的其他算法)可能需要数天的计算时间。

def pythagore_triplets(n=1000):
   maxn=int(n*(2**0.5))+1 # max int whose square may be the sum of two squares
   squares=[x*x for x in xrange(maxn+1)] # calculate all the squares once
   reverse_squares=dict([(squares[i],i) for i in xrange(maxn+1)]) # x*x=>x
   for x in xrange(1,n):
     x2 = squares[x]
     for y in xrange(x,n+1):
       y2 = squares[y]
       z = reverse_squares.get(x2+y2)
       if z != None:
         yield x,y,z

>>> print list(pythagore_triplets(20))
[(3, 4, 5), (5, 12, 13), (6, 8, 10), (8, 15, 17), (9, 12, 15), (12, 16, 20)]

请注意,如果您要计算前十亿个三元组,则此算法在开始之前就会崩溃,因为由于内存错误。因此,在处理高值n时,ΤΖΩΤΖΙΟΥ的算法可能更安全。
顺便说一下,这是Tafkas的算法,为了进行性能测试而翻译成Python。它的缺陷是需要3个循环,而不是2个。
def gcd(a, b):
  while b != 0:
    t = b
    b = a%b
    a = t
  return a

def find_triple(upper_boundary=1000):
  for c in xrange(5,upper_boundary+1):
    for b in xrange(4,c):
      for a in xrange(3,b):
        if (a*a + b*b == c*c and gcd(a,b) == 1):
          yield a,b,c

Python虚拟机中的平方根成本被高估了,但我喜欢你的答案。 - tzot
顺便说一句,我在算法的第一个版本中发现了一个错误:我需要反转平方数直到n*2+(n-1)2,但我只有反转平方数到n2。所以我将其四舍五入到2*(n*2):错误已经修复。 - MiniQuark
请在 http://en.wikipedia.org/wiki/Binary_GCD_algorithm 中查看最快的算法的GCD。 - lakshmanaraj
请仔细重新阅读V4。外部while测试z,内部while也在递增z。因此,内部while加速了外部while。这就是为什么它是二次的,而不是你认为的三次的原因。如果字典变大,V4不会耗尽内存。 - joel.neely
我同意你的观点,认为Python和Java之间的性能差异很有趣。我怀疑Java处理“平方根哈希映射”的装箱/拆箱方式可能是一个因素。 - joel.neely
我想出了一个算法(请参见我的答案中的V.2),它仍然具有最小的内存需求,比我之前的建议快了近4倍,并且似乎比你这里的算法快1.64倍;你可能需要在你的电脑上验证一下。 - tzot

6
def pyth_triplets(n=1000):
    "Version 1"
    for x in xrange(1, n):
        x2= x*x # time saver
        for y in xrange(x+1, n): # y > x
            z2= x2 + y*y
            zs= int(z2**.5)
            if zs*zs == z2:
                yield x, y, zs

>>> print list(pyth_triplets(20))
[(3, 4, 5), (5, 12, 13), (6, 8, 10), (8, 15, 17), (9, 12, 15), (12, 16, 20)]

V.1算法具有单调递增的x值。

编辑

看来这个问题还没有解决 :)
自从我回来并重新审视了代码后,我尝试了第二种方法,它几乎比我的先前建议快4倍(对于N = 10000,大约占CPU时间的26%),因为它避免了许多不必要的计算:

def pyth_triplets(n=1000):
    "Version 2"
    for z in xrange(5, n+1):
        z2= z*z # time saver
        x= x2= 1
        y= z - 1; y2= y*y
        while x < y:
            x2_y2= x2 + y2
            if x2_y2 == z2:
                yield x, y, z
                x+= 1; x2= x*x
                y-= 1; y2= y*y
            elif x2_y2 < z2:
                x+= 1; x2= x*x
            else:
                y-= 1; y2= y*y

>>> print list(pyth_triplets(20))
[(3, 4, 5), (6, 8, 10), (5, 12, 13), (9, 12, 15), (8, 15, 17), (12, 16, 20)]

请注意,此算法具有递增的z值。
如果将该算法转换为C语言,由于乘法比加法需要更多时间,因此可以最小化必要的乘法,考虑到连续平方之间的步骤是:
(x+1)² - x² = (x+1)(x+1) - x² = x² + 2x + 1 - x² = 2x + 1
因此,所有内部的x2= x*x和y2= y*y都将被转换为像这样的加法和减法。
def pyth_triplets(n=1000):
    "Version 3"
    for z in xrange(5, n+1):
        z2= z*z # time saver
        x= x2= 1; xstep= 3
        y= z - 1; y2= y*y; ystep= 2*y - 1
        while x < y:
            x2_y2= x2 + y2
            if x2_y2 == z2:
                yield x, y, z
                x+= 1; x2+= xstep; xstep+= 2
                y-= 1; y2-= ystep; ystep-= 2
            elif x2_y2 < z2:
                x+= 1; x2+= xstep; xstep+= 2
            else:
                y-= 1; y2-= ystep; ystep-= 2

当然,在Python中,额外生成的字节码实际上会使算法比版本2慢,但我敢打赌(没有检查 :))V.3在C中更快。
大家加油 :)

4

我只是扩展了Kyle Gullion的答案,使三元组按照斜边和最长边的顺序排序。

它不使用numpy,但需要一个SortedCollection(或SortedList),例如this one

def primitive_triples():
""" generates primitive Pythagorean triplets x<y<z
sorted by hypotenuse z, then longest side y
through Berggren's matrices and breadth first traversal of ternary tree
:see: https://en.wikipedia.org/wiki/Tree_of_primitive_Pythagorean_triples
"""
key=lambda x:(x[2],x[1])
triples=SortedCollection(key=key)
triples.insert([3,4,5])
A = [[ 1,-2, 2], [ 2,-1, 2], [ 2,-2, 3]]
B = [[ 1, 2, 2], [ 2, 1, 2], [ 2, 2, 3]]
C = [[-1, 2, 2], [-2, 1, 2], [-2, 2, 3]]

while triples:
    (a,b,c) = triples.pop(0)
    yield (a,b,c)

    # expand this triple to 3 new triples using Berggren's matrices
    for X in [A,B,C]:
        triple=[sum(x*y for (x,y) in zip([a,b,c],X[i])) for i in range(3)]
        if triple[0]>triple[1]: # ensure x<y<z
            triple[0],triple[1]=triple[1],triple[0]
        triples.insert(triple)

def triples():
""" generates all Pythagorean triplets triplets x<y<z 
sorted by hypotenuse z, then longest side y
"""
prim=[] #list of primitive triples up to now
key=lambda x:(x[2],x[1])
samez=SortedCollection(key=key) # temp triplets with same z
buffer=SortedCollection(key=key) # temp for triplets with smaller z
for pt in primitive_triples():
    z=pt[2]
    if samez and z!=samez[0][2]: #flush samez
        while samez:
            yield samez.pop(0)
    samez.insert(pt)
    #build buffer of smaller multiples of the primitives already found
    for i,pm in enumerate(prim):
        p,m=pm[0:2]
        while True:
            mz=m*p[2]
            if mz < z:
                buffer.insert(tuple(m*x for x in p))
            elif mz == z: 
                # we need another buffer because next pt might have
                # the same z as the previous one, but a smaller y than
                # a multiple of a previous pt ...
                samez.insert(tuple(m*x for x in p))
            else:
                break
            m+=1
        prim[i][1]=m #update multiplier for next loops
    while buffer: #flush buffer
        yield buffer.pop(0)
    prim.append([pt,2]) #add primitive to the list

代码可在我的Python库math2模块中获得。它已经针对OEIS的一些系列进行了测试(底部的此处有代码),这让我能够找到A121727中的一个错误 :-)

2
我用Ruby编写了这个程序,它类似于Python的实现。重要的一行是:
if x*x == y*y + z*z && gcd(y,z) == 1:

接下来,您需要实现一个方法,返回两个给定数字的最大公约数(gcd)。再次以Ruby为例:

def gcd(a, b)
    while b != 0
      t = b
      b = a%b
      a = t
    end
    return a
end

完整的Ruby方法查找三元组如下所示:
def find_triple(upper_boundary)

  (5..upper_boundary).each {|c|
    (4..c-1).each {|b|
      (3..b-1).each {|a|
        if (a*a + b*b == c*c && gcd(a,b) == 1)
          puts "#{a} \t #{b} \t #{c}"
        end
      }
    }
  }
end

只是为了澄清一下,如果你想打印原始三元组,那么GCD步骤才是必需的。如果你想要任何三元组,那就不需要这一步骤。 - Himadri Choudhury
该算法使用了3个嵌套循环,尽管只需要2个(参见ΤΖΩΤΖΙΟΥ的算法或我的算法)。这意味着,如果您搜索前10,000个勾股三元组,您的算法将比“2个循环”的算法慢10,000倍。 - MiniQuark
小心这种推理。我上面所提供的算法采用三个循环结构,但比ΤΖΩΤΖΙΟΥ使用的两个循环结构快大约三倍。 - jason
@Jason:在比较时要小心,特别是当比较的是不同类型的事物。此外,MiniQuark的评论足够泛化和模糊,以至于无法反驳(虽然不一定适用),而你的评论缺乏数字和共同的运行环境。 - tzot
@ΤΖΩΤΖΙΟΥ:我将你的代码翻译成了C#并进行了一一对比测试。 - jason

2

虽然这是一个老问题,但我仍然想分享一下我的看法。有两种通用方法可以生成唯一的勾股数三元组。一种是通过缩放,另一种是使用这个古老的公式。

所谓缩放,就是取一个常数 n,然后将基本三元组(例如 3、4、5)分别乘以 n。以 n=2 为例,我们得到下一个三元组 6、8、10。

缩放

def pythagoreanScaled(n):
    triplelist = []
    for x in range(n):
        one = 3*x
        two = 4*x
        three = 5*x
        triple = (one,two,three)
        triplelist.append(triple)
return triplelist

公式方法利用了这样一个事实:如果我们取一个数x,计算2m、m^2+1和m^2-1,那么这三个数总是构成一个勾股三元组。

公式

def pythagoreantriple(n):
    triplelist = []
    for x in range(2,n):
        double = x*2
        minus = x**2-1
        plus = x**2+1
        triple = (double,minus,plus)
        triplelist.append(triple)
    return triplelist

1
from  math import sqrt
from itertools import combinations

#Pythagorean triplet - a^2 + b^2 = c^2 for (a,b) <= (1999,1999)
def gen_pyth(n):
if n >= 2000 :
  return
ELEM =   [  [ i,j,i*i + j*j ] for i , j in list(combinations(range(1, n +   1 ), 2)) if sqrt(i*i + j*j).is_integer() ]
print (*ELEM , sep = "\n")


gen_pyth(200)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接