El número de posibles resultados numéricos de paréntesis de 2 ^ 2 ^… ^ 2

19

Considere una expresión 2^2^...^2con noperadores ^. Operador ^significa exponenciación ("al poder de"). Suponga que no tiene una asiatividad predeterminada, por lo que la expresión debe estar completamente entre paréntesis para dejar de ser ambigua. La cantidad de formas de paréntesis de la expresión están dadas por números catalanes C_n=(2n)!/(n+1)!/n! .

A veces, diferentes paréntesis dan el mismo resultado numérico, por ejemplo (2^2)^(2^2)=((2^2)^2)^2, por lo que el número de diferentes resultados numéricos posibles para un determinado nes menor que C_npara todos n>1. La secuencia comienza 1, 1, 2, 4, 8, ...en oposición a los números catalanes.1, 2, 5, 14, 42, ...

El problema es escribir el programa (o función) más rápido que acepta ncomo entrada y devuelve el número de diferentes resultados numéricos posibles de la expresión 2^2^...^2con noperadores ^. El rendimiento no debería deteriorarse significativamente a medida que ncrece, por lo que el cálculo directo de torres de alta potencia es probablemente una mala idea.

Vladimir Reshetnikov
fuente
Solo estoy compartiendo una idea aquí, pero parece que debería ser posible usar exclusivamente la suma y la multiplicación, ya que la respuesta siempre será de la forma 2^n, y por lo tanto sería innecesario hacer un seguimiento de cualquier cosa, excepto n. Es decir, solo usar las reglas de exponenciación parece sabio. Sin embargo, seguramente hay una forma más inteligente y completamente algebraica de hacer esto.
Fors
@ For, supongo nque todavía es demasiado grande para calcular. Aún así, bien notado. Tal vez una representación recursiva en la forma "1 o 2 ^ (...) o (...) + (...)"; pero aún tiene el problema de cómo normalizar dicha representación de un número (o comparar dos representaciones para la igualdad de valores).
John Dvorak
44
@ JanDvorak, A002845 (sin formulario cerrado)
Peter Taylor
3
Related oeis.org/A003018/a003018.pdf
Dr. belisarius
1
@Vladimir Reshetnikov: Creo que hay un error off-by-one en su fórmula. Cuando tiene ndos y C_n=(2n)!/(n+1)!/n!debe ser el número de paréntesis, entonces para n = 3 debería ser 5, ¿correcto? Ya veo (2^2)^2y 2^(2^2), pero ¿cuáles son las otras tres combinaciones? Creo que C_n te da el número de paréntesis para n + 1 dos.
Martin Thoma

Respuestas:

9

Python 2.7

Este enfoque aprovecha las siguientes consideraciones:

Cualquier número entero puede representarse como una suma de potencias de dos. Los exponentes en las potencias de dos también se pueden representar como potencias de dos. Por ejemplo:

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

Estas expresiones con las que terminamos se pueden representar como conjuntos de conjuntos (en Python, utilicé el incorporado frozenset):

  • 0se convierte en el conjunto vacío {}.
  • 2^ase convierte en el conjunto que contiene el conjunto que representa a. Por ejemplo: 1 = 2^0 -> {{}}y 2 = 2^(2^0) -> {{{}}}.
  • a+bse convierte en la concatenación de los conjuntos que representan ay b. P.ej,3 = 2^(2^0) + 2^0 -> {{{}},{}}

Resulta que las expresiones de la forma 2^2^...^2pueden transformarse fácilmente en su representación de conjunto única, incluso cuando el valor numérico es demasiado grande para ser almacenado como un entero.


Para n=20, esto se ejecuta en 8.7s en CPython 2.7.5 en mi máquina (un poco más lento en Python 3 y mucho más lento en 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()

(El concepto del decorador de memorias se copia de http://code.activestate.com/recipes/578231-probably-the-fastest-memoization-decorator-in-the-/ ).

Salida:

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

Tiempos para diferentes n:

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

Cualquiera de los nanteriores 21 produce un error de memoria en mi máquina.

Me interesaría si alguien puede hacer esto más rápido traduciéndolo a un idioma diferente.

Editar: Optimizado la get_resultsfunción. Además, usar Python 2.7.5 en lugar de 2.7.2 lo hizo correr un poco más rápido.

flornquake
fuente
Hice una traducción de C # pero utilicé matrices ordenadas y realicé la suma en orden en lugar de por conjuntos contiene comprobaciones. Es mucho más lento, y aún no he perfilado para ver si eso se debe a no memorizar la función sucesora o al costo de las comparaciones.
Peter Taylor
1
No he perfilado el código (brillante) de @ flornquake, pero supongo que gran parte del tiempo de la CPU se dedica a realizar pruebas de membresía y operaciones de manipulación de conjuntos, que están bastante bien optimizadas en Python, utilizando su tabla hash ubicua y su clave hash rutinas La memorización es ciertamente una gran cosa, con un algoritmo exponencial como este. Si lo omitió, puede esperar un rendimiento exponencialmente más lento.
Tobia
@Tobia, en realidad descubrí que en C # memorizar la función sucesora lo hacía más lento. También descubrí que una traducción más literal (usando operaciones de conjunto) era significativamente más lenta que mi adición de nivel inferior. La única mejora real que encontré sobre mi código original fue tener en cuenta (a^b)^c = (a^c)^b, y aún es mucho más lenta que esta implementación de Python.
Peter Taylor
@PeterTaylor: Editar: Hasta donde puedo ver, el algoritmo de flornquake se basa en la construcción de conjuntos de árboles, donde un árbol es un conjunto de árboles, y así sucesivamente. Se memorizan todas las piezas de estos árboles, desde el conjunto vacío más pequeño hasta el conjunto más grande. Esto significa que todos estos árboles contienen "estructura repetida" que solo se calcula una vez (por la CPU) y se almacena una vez (en la RAM). ¿Está seguro de que su algoritmo de "suma en orden" identifica toda esta estructura repetida y la calcula una vez? (lo que llamé complejidad exponencial arriba) Ver también en.wikipedia.org/wiki/Dynamic_programming
Tobia
@ Tobia, nos superpusimos. He publicado el código.
Peter Taylor
5

C#

Esta es una traducción del código Python de flornquake a C # usando una rutina de adición de nivel inferior que proporciona una aceleración moderada sobre una traducción directa. No es la versión más optimizada que tengo, pero es bastante más larga porque tiene que almacenar la estructura de árbol y los valores.

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;
        }
    }
}
Peter Taylor
fuente