括号中可能的数值结果数量2 ^ 2 ^…^ 2


19

考虑2^2^...^2带有n运算符的表达式^。运算符^表示幂运算(“幂”)。假定它没有默认的关联性,因此需要对表达式进行完全括号括起来才能明确。用加泰罗尼亚语数字 给出括号内表达式的方式C_n=(2n)!/(n+1)!/n!

例如(2^2)^(2^2)=((2^2)^2)^2,有时不同的括号给出相同的数字结果,因此给定的不同可能数字结果的数量n少于C_n所有数字n>11, 1, 2, 4, 8, ...与加泰罗尼亚语数字相反,该序列开始1, 2, 5, 14, 42, ...

问题是编写最快的程序(或函数),将其接受n为输入并2^2^...^2使用n运算符返回表达式的不同可能数字结果的数量^。性能不会随着n增长而显着降低,因此直接计算高功率塔可能不是一个好主意。


我只是在这里分享一个想法,但是似乎应该可以专门使用加法和乘法,因为答案始终是形式2^n,因此没有必要跟踪除之外的任何内容n。即,仅使用求幂规则似乎是明智的。但是,肯定有一种更聪明,完全代数的方法可以做到这一点。
Fors,

@Fors我猜n仍然太大了来计算。仍然,众所周知。可能是以“ 1或2 ^(...)或(...)+(...)”的形式进行递归表示;但是您仍然有一个问题,如何对这样的数字表示形式进行规范化(或比较两个表示形式的值相等性)。
约翰·德沃夏克

4
@ JanDvorak,A002845(未提供封闭表格)
Peter Taylor


1
@Vladimir Reshetnikov:我认为您的公式中存在一个错误的错误。当您有n2并且C_n=(2n)!/(n+1)!/n!应该是括号的数目时,那么对于n = 3,它应该是5,对吗?我看到了(2^2)^22^(2^2),但是其他三个组合是什么?我认为C_n会为您提供n + 1的括号数量。
马丁·托玛

Answers:


9

Python 2.7

这种方法利用了以下注意事项:

任何整数都可以表示为2的幂。2的幂的指数也可以表示为2的幂。例如:

8 = 2^3 = 2^(2^1 + 2^0) = 2^(2^(2^0) + 2^0)

我们最终得到的这些表达式可以表示为集合集(在Python中,我使用内置的frozenset):

  • 0成为空集{}
  • 2^a成为包含代表的集合的集合a。例如:1 = 2^0 -> {{}}2 = 2^(2^0) -> {{{}}}
  • a+b成为表示a和的集合的串联b。例如,3 = 2^(2^0) + 2^0 -> {{{}},{}}

事实证明2^2^...^2,即使数值太大而无法存储为整数,形式的表达式也可以轻松地转换为其唯一的集合表示形式。


对于n=20,这在我的机器上的CPython 2.7.5上以8.7s运行(在Python 3中慢一点,在PyPy中慢得多):

"""Analyze the expressions given by parenthesizations of 2^2^...^2.

Set representation:  s is a set of sets which represents an integer n.  n is
  given by the sum of all 2^m for the numbers m represented by the sets
  contained in s.  The empty set stands for the value 0.  Each number has
  exactly one set representation.

  In Python, frozensets are used for set representation.

  Definition in Python code:
      def numeric_value(s):
          n = sum(2**numeric_value(t) for t in s)
          return n"""

import itertools


def single_arg_memoize(func):
    """Fast memoization decorator for a function taking a single argument.

    The metadata of <func> is *not* preserved."""

    class Cache(dict):
        def __missing__(self, key):
            self[key] = result = func(key)
            return result
    return Cache().__getitem__


def count_results(num_exponentiations):
    """Return the number of results given by parenthesizations of 2^2^...^2."""
    return len(get_results(num_exponentiations))

@single_arg_memoize
def get_results(num_exponentiations):
    """Return a set of all results given by parenthesizations of 2^2^...^2.

    <num_exponentiations> is the number of exponentiation operators in the
    parenthesized expressions.

    The result of each parenthesized expression is given as a set.  The
    expression evaluates to 2^(2^n), where n is the number represented by the
    given set in set representation."""

    # The result of the expression "2" (0 exponentiations) is represented by
    # the empty set, since 2 = 2^(2^0).
    if num_exponentiations == 0:
        return {frozenset()}

    # Split the expression 2^2^...^2 at each of the first half of
    # exponentiation operators and parenthesize each side of the expession.
    split_points = xrange(num_exponentiations)
    splits = itertools.izip(split_points, reversed(split_points))
    splits_half = ((left_part, right_part) for left_part, right_part in splits
                                           if left_part <= right_part)

    results = set()
    results_add = results.add
    for left_part, right_part in splits_half:
        for left in get_results(left_part):
            for right in get_results(right_part):
                results_add(exponentiate(left, right))
                results_add(exponentiate(right, left))
    return results


def exponentiate(base, exponent):
    """Return the result of the exponentiation of <operands>.

    <operands> is a tuple of <base> and <exponent>.  The operators are each
    given as the set representation of n, where 2^(2^n) is the value the
    operator stands for.

    The return value is the set representation of r, where 2^(2^r) is the
    result of the exponentiation."""

    # Where b is the number represented by <base>, e is the number represented
    # by <exponent> and r is the number represented by the return value:
    #   2^(2^r) = (2^(2^b)) ^ (2^(2^e))
    #   2^(2^r) = 2^(2^b * 2^(2^e))
    #   2^(2^r) = 2^(2^(b + 2^e))
    #   r = b + 2^e

    # If <exponent> is not in <base>, insert it to arrive at the set with the
    # value: b + 2^e.  If <exponent> is already in <base>, take it out,
    # increment e by 1 and repeat from the start to eventually arrive at:
    #   b - 2^e + 2^(e+1) =
    #   b + 2^e
    while exponent in base:
        base -= {exponent}
        exponent = successor(exponent)
    return base | {exponent}

@single_arg_memoize
def successor(value):
    """Return the successor of <value> in set representation."""
    # Call exponentiate() with <value> as base and the empty set as exponent to
    # get the set representing (n being the number represented by <value>):
    #   n + 2^0
    #   n + 1
    return exponentiate(value, frozenset())


def main():
    import timeit
    print timeit.timeit(lambda: count_results(20), number=1)
    for i in xrange(21):
        print '{:.<2}..{:.>9}'.format(i, count_results(i))

if __name__ == '__main__':
    main()

(备注装饰器的概念是从http://code.activestate.com/recipes/578231-probably-the-fastest-memoization-decorator-in-the-/复制的。)

输出:

8.667753234
0...........1
1...........1
2...........1
3...........2
4...........4
5...........8
6..........17
[...]
19.....688366
20....1619087

不同的时间n

 n    time
16    0.240
17    0.592
18    1.426
19    3.559
20    8.668
21   21.402

任何n高于21的值都会在我的计算机上导致内存错误。

我很想知道是否有人可以通过将其翻译成另一种语言来使其更快。

编辑:优化get_results功能。另外,使用Python 2.7.5而不是2.7.2使其运行更快。


我进行了C#转换,但使用排序数组并按顺序而不是按set进行加法包含检查。它的速度要慢得多,而且我还没有进行分析,以了解这是由于没有记下后继功能还是由于比较成本。
彼得·泰勒

1
我没有分析@flornquake的(出色的)代码,但是我假设很多CPU时间都花在了设置成员资格测试和集合操作上,这在Python中使用了无处不在的哈希表和哈希键都得到了很好的优化。例行程序。使用这样的指数算法,记忆化当然是一件大事。如果您忽略了这一点,则可以预期性能会成倍下降。
Tobia

@Tobia,实际上我发现在C#中记忆后继功能会使它变慢。我还发现,更多的文字翻译(使用设置操作)比我的低级添加要慢得多。我在原始代码上发现的唯一真正的改进就是考虑了(a^b)^c = (a^c)^b,它仍然比此Python实现慢得多。
彼得·泰勒

@PeterTaylor:编辑:据我所知,flornquake的算法依赖于构建树集,其中树本身就是树集,依此类推。这些树的所有块,从最小的空集到最大的集,都被记忆。这意味着所有这些树都包含“重复结构”,该“重复结构”仅被计算一次(由CPU)并被存储一次(在RAM中)。您确定您的“按顺序添加”算法可以识别所有重复的结构并对其进行一次计算吗?(我在上面将其称为指数复杂性)另请参见en.wikipedia.org/wiki/Dynamic_programming
Tobia,

@Tobia,我们重叠了。我已经发布了代码。
彼得·泰勒

5

C#

这是使用较低级别的添加例程将flornquake的Python代码转换为C#的程序,该例程比直接转换提供适度的加速。它不是我拥有的最优化的版本,但是要花很多时间,因为它必须存储树结构以及值。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Sandbox {
    class PowerTowers {
        public static void Main() {
            DateTime start = DateTime.UtcNow;
            for (int i = 0; i < 17; i++)
                Console.WriteLine("{2}: {0} (in {1})", Results(i).Count, DateTime.UtcNow - start, i);
        }

        private static IList<HashSet<Number>> _MemoisedResults;

        static HashSet<Number> Results(int numExponentations) {
            if (_MemoisedResults == null) {
                _MemoisedResults = new List<HashSet<Number>>();
                _MemoisedResults.Add(new HashSet<Number>(new Number[] { Number.Zero }));
            }

            if (numExponentations < _MemoisedResults.Count) return _MemoisedResults[numExponentations];

            HashSet<Number> rv = new HashSet<Number>();
            for (int i = 0; i < numExponentations; i++) {
                IEnumerable<Number> rhs = Results(numExponentations - 1 - i);
                foreach (var b in Results(i))
                    foreach (var e in rhs) {
                        if (!e.Equals(Number.One)) rv.Add(b.Add(e.Exp2()));
                    }
            }
            _MemoisedResults.Add(rv);
            return rv;
        }
    }

    // Immutable
    struct Number : IComparable<Number> {
        public static Number Zero = new Number(new Number[0]);
        public static Number One = new Number(Zero);

        // Ascending order
        private readonly Number[] _Children;
        private readonly int _Depth;
        private readonly int _HashCode;

        private Number(params Number[] children) {
            _Children = children;
            _Depth = children.Length == 0 ? 0 : 1 + children[children.Length - 1]._Depth;

            int hashCode = 0;
            foreach (var n in _Children) hashCode = hashCode * 37 + n.GetHashCode() + 1;
            _HashCode = hashCode;
        }

        public Number Add(Number n) {
            // "Standard" bitwise adder built from full adder.
            // Work forwards because children are in ascending order.
            int off1 = 0, off2 = 0;
            IList<Number> result = new List<Number>();
            Number? carry = default(Number?);

            while (true) {
                if (!carry.HasValue) {
                    // Simple case
                    if (off1 < _Children.Length) {
                        if (off2 < n._Children.Length) {
                            int cmp = _Children[off1].CompareTo(n._Children[off2]);
                            if (cmp < 0) result.Add(_Children[off1++]);
                            else if (cmp == 0) {
                                carry = _Children[off1++].Add(One);
                                off2++;
                            }
                            else result.Add(n._Children[off2++]);
                        }
                        else result.Add(_Children[off1++]);
                    }
                    else if (off2 < n._Children.Length) result.Add(n._Children[off2++]);
                    else return new Number(result.ToArray()); // nothing left to add
                }
                else {
                    // carry is the (possibly joint) smallest value
                    int matches = 0;
                    if (off1 < _Children.Length && carry.Value.Equals(_Children[off1])) {
                        matches++;
                        off1++;
                    }
                    if (off2 < n._Children.Length && carry.Value.Equals(n._Children[off2])) {
                        matches++;
                        off2++;
                    }

                    if ((matches & 1) == 0) result.Add(carry.Value);
                    carry = matches == 0 ? default(Number?) : carry.Value.Add(One);
                }
            }
        }

        public Number Exp2() {
            return new Number(this);
        }

        public int CompareTo(Number other) {
            if (_Depth != other._Depth) return _Depth.CompareTo(other._Depth);

            // Work backwards because children are in ascending order
            int off1 = _Children.Length - 1, off2 = other._Children.Length - 1;
            while (off1 >= 0 && off2 >= 0) {
                int cmp = _Children[off1--].CompareTo(other._Children[off2--]);
                if (cmp != 0) return cmp;
            }

            return off1.CompareTo(off2);
        }

        public override bool Equals(object obj) {
            if (!(obj is Number)) return false;

            Number n = (Number)obj;
            if (n._HashCode != _HashCode || n._Depth != _Depth || n._Children.Length != _Children.Length) return false;
            for (int i = 0; i < _Children.Length; i++) {
                if (!_Children[i].Equals(n._Children[i])) return false;
            }

            return true;
        }

        public override int GetHashCode() {
            return _HashCode;
        }
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.