O número de possíveis resultados numéricos de parênteses de 2 ^ 2 ^… ^ 2


19

Considere uma expressão 2^2^...^2com noperadores ^. Operador ^significa exponenciação ("ao poder de"). Suponha que ela não tenha associação padrão, portanto a expressão precisa ser totalmente entre parênteses para se tornar inequívoca. O número de maneiras entre parênteses da expressão é dado por números catalães C_n=(2n)!/(n+1)!/n! .

Às vezes, parênteses diferentes dão o mesmo resultado numérico, por exemplo (2^2)^(2^2)=((2^2)^2)^2, portanto, o número de diferentes resultados numéricos possíveis para um dado né menor do que C_npara todos n>1. A sequência começa 1, 1, 2, 4, 8, ...em oposição aos números catalães1, 2, 5, 14, 42, ...

O problema é escrever o programa (ou função) mais rápido que aceita ncomo entrada e retorna o número de diferentes resultados numéricos possíveis da expressão 2^2^...^2com os noperadores ^. O desempenho não deve se deteriorar significativamente à medida que ncresce, portanto, o cálculo direto de torres de alta potência é provavelmente uma má ideia.


Estou apenas compartilhando uma ideia aqui, mas parece que deve ser possível usar exclusivamente adição e multiplicação, pois a resposta sempre será da forma 2^ne, portanto, seria desnecessário acompanhar qualquer coisa, exceto n. Ou seja, apenas usar as regras da exponenciação parece sábio. No entanto, certamente existe uma maneira mais inteligente e completamente algébrica de fazer isso.
Fors

@Fors Acho que aindan é muito grande para calcular. Ainda assim, bem observado. Talvez uma representação recursiva na forma "1 ou 2 ^ (...) ou (...) + (...)"; mas você ainda tem o problema de como normalizar essa representação de um número (ou comparar duas representações para igualdade de valor).
John Dvorak

4
@JanDvorak, A002845 (nenhum dado de forma fechada)
Peter Taylor


11
@ Vladimir Reshetnikov: Acho que há um erro de um por um na sua fórmula. Quando você tem ndois e C_n=(2n)!/(n+1)!/n!deve ser o número de parênteses, então para n = 3 deve ser 5, correto? Eu vejo (2^2)^2e 2^(2^2), mas quais são as outras três combinações? Eu acho que C_n fornece o número de parênteses para n + 1 dois.
Martin Thoma 15/05

Respostas:


9

Python 2.7

Essa abordagem aproveita as seguintes considerações:

Qualquer número inteiro pode ser representado como uma soma de potências de dois. Os expoentes nas potências de dois também podem ser representados como potências de dois. Por exemplo:

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

Essas expressões com as quais terminamos podem ser representadas como conjuntos de conjuntos (em Python, usei o built-in frozenset):

  • 0torna-se o conjunto vazio {}.
  • 2^atorna-se o conjunto que contém o conjunto que representa a. Por exemplo: 1 = 2^0 -> {{}}e 2 = 2^(2^0) -> {{{}}}.
  • a+btorna-se a concatenação dos conjuntos que representam ae b. Por exemplo,3 = 2^(2^0) + 2^0 -> {{{}},{}}

Acontece que expressões do formulário 2^2^...^2podem ser facilmente transformadas em sua representação de conjunto exclusiva, mesmo quando o valor numérico é muito grande para ser armazenado como um número inteiro.


Pois n=20, isso é executado em 8.7s no CPython 2.7.5 na minha máquina (um pouco mais lento no Python 3 e muito mais lento no 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()

(O conceito do decorador de memoização é copiado de http://code.activestate.com/recipes/578231-probably-the-fastest-memoization-decorator-in-the-/ .)

Resultado:

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

Horários para diferentes n:

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

Qualquer um nacima de 21 resulta em um erro de memória na minha máquina.

Eu estaria interessado se alguém puder tornar isso mais rápido traduzindo-o para um idioma diferente.

Editar: otimizou a get_resultsfunção. Além disso, o uso do Python 2.7.5 em vez do 2.7.2 fez com que fosse executado um pouco mais rápido.


Fiz uma tradução em C #, mas usando matrizes classificadas e fazendo a adição em ordem, em vez de por conjunto, contém verificações. É muito mais lento, e ainda não fiz um perfil para verificar se isso ocorre por não memorizar a função sucessora ou pelo custo das comparações.
Peter Taylor

11
Eu não criei o perfil do código (brilhante) do @ flornquake, mas presumo que grande parte do tempo da CPU é gasto realizando testes de associação de conjunto e operações de manipulação de conjunto, que são bastante otimizadas em Python, usando sua onipresente tabela de hash e chave de hash rotinas. Memoização é certamente uma grande coisa, com um algoritmo exponencial como esse. Se você deixar isso de fora, poderá esperar um desempenho exponencialmente mais lento.
27413 Tobia

@ Tobia, na verdade, descobri que em C # memoising a função sucessora a tornava mais lenta. Também descobri que uma tradução mais literal (usando operações definidas) era significativamente mais lenta que minha adição de nível inferior. A única melhoria real que encontrei em relação ao meu código original foi levar em consideração (a^b)^c = (a^c)^b, e ainda é muito mais lenta que esta implementação em Python.
22413 Peter Peter

@ PeterTaylor: Edit: Até onde eu sei, o algoritmo do flornquake depende da construção de conjuntos de árvores, onde uma árvore é um conjunto de árvores, e assim por diante. Todas as partes dessas árvores, do menor conjunto vazio ao maior conjunto de conjuntos, são memorizadas. Isso significa que todas essas árvores contêm "estrutura repetida" que é computada apenas uma vez (pela CPU) e armazenada uma vez (na RAM). Você tem certeza de que seu algoritmo de "adição em ordem" está identificando toda essa estrutura repetida e computando-a uma vez? (o que chamei de complexidade exponencial acima) Veja também en.wikipedia.org/wiki/Dynamic_programming
Tobia

@Tobia, nós nos sobrepomos. Eu publiquei o código.
27413 Peter Peter

5

C #

Esta é uma tradução do código Python do flornquake para C # usando uma rotina de adição de nível inferior que fornece uma aceleração moderada em relação à tradução direta. Não é a versão mais otimizada que eu tenho, mas é um pouco mais longa porque precisa armazenar a estrutura da árvore e os 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;
        }
    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.