Il numero di possibili esiti numerici delle parentesi di 2 ^ 2 ^… ^ 2


19

Considera un'espressione 2^2^...^2con gli noperatori ^. Operatore ^significa esponenziazione ("alla potenza di"). Supponiamo che non abbia assosiatività predefinita, quindi l'espressione deve essere tra parentesi completa per diventare inequivocabile. Il numero di modi per parentesi nell'espressione è dato dai numeri catalani C_n=(2n)!/(n+1)!/n! .

A volte diverse parentesi danno lo stesso risultato numerico, ad esempio (2^2)^(2^2)=((2^2)^2)^2, quindi il numero di diversi possibili risultati numerici per un dato nè inferiore a C_ntutti n>1. La sequenza inizia 1, 1, 2, 4, 8, ...rispetto ai numeri catalani1, 2, 5, 14, 42, ...

Il problema è scrivere il programma (o funzione) più veloce che accetta ncome input e restituisce il numero di diversi possibili risultati numerici dell'espressione 2^2^...^2con gli noperatori ^. Le prestazioni non dovrebbero peggiorare in modo significativo man mano che ncresce, quindi il calcolo diretto delle torri ad alta potenza è probabilmente una cattiva idea.


Sto solo condividendo un'idea qui, ma sembra che dovrebbe essere possibile utilizzare esclusivamente l'addizione e la moltiplicazione, poiché la risposta sarà sempre del modulo 2^ne quindi non sarebbe necessario tenere traccia di qualsiasi cosa tranne n. Cioè, usare solo le regole dell'espiazione sembra saggio. Tuttavia, c'è sicuramente un modo più intelligente e completamente algebrico per farlo.
Fors

@Fors immagino che nsia ancora troppo grande per essere calcolato. Comunque, ben notato. Forse una rappresentazione ricorsiva nella forma "1 o 2 ^ (...) o (...) + (...)"; ma hai ancora il problema di come normalizzare tale rappresentazione di un numero (o confrontare due rappresentazioni per l'uguaglianza di valore).
John Dvorak,

4
@JanDvorak, A002845 (nessuna forma chiusa data)
Peter Taylor


1
@Vladimir Reshetnikov: penso che ci sia un errore off-by-one nella tua formula. Quando hai ndue e C_n=(2n)!/(n+1)!/n!dovresti avere il numero di parentesi, allora per n = 3 dovrebbe essere 5, giusto? Vedo (2^2)^2e 2^(2^2), ma quali sono le altre tre combinazioni? Penso che C_n ti dia il numero di parentesi per n + 1 due.
Martin Thoma,

Risposte:


9

Python 2.7

Questo approccio sfrutta le seguenti considerazioni:

Qualsiasi numero intero può essere rappresentato come una somma di potenze di due. Gli esponenti nei poteri di due possono anche essere rappresentati come poteri di due. Per esempio:

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

Queste espressioni con cui finiamo possono essere rappresentate come set di set (in Python, ho usato il built-in frozenset):

  • 0diventa l'insieme vuoto {}.
  • 2^adiventa il set contenente il set che rappresenta a. Ad esempio: 1 = 2^0 -> {{}}e 2 = 2^(2^0) -> {{{}}}.
  • a+bdiventa la concatenazione degli insiemi che rappresentano ae b. Per esempio,3 = 2^(2^0) + 2^0 -> {{{}},{}}

Si scopre che le espressioni del modulo 2^2^...^2possono essere facilmente trasformate nella loro rappresentazione di insieme univoca, anche quando il valore numerico è troppo grande per essere memorizzato come un numero intero.


Per n=20, questo funziona in 8.7s su CPython 2.7.5 sulla mia macchina (un po 'più lento in Python 3 e molto più lento in 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()

(Il concetto del decoratore di memoization è copiato da http://code.activestate.com/recipes/578231-probably-the-fastest-memoization-decorator-in-the-/ .)

Produzione:

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

Tempi per diversi n:

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

Qualsiasi nsopra 21 provoca un errore di memoria sulla mia macchina.

Sarei interessato se qualcuno potesse renderlo più veloce traducendolo in un'altra lingua.

Modifica: ottimizzata la get_resultsfunzione. Inoltre, l'utilizzo di Python 2.7.5 anziché 2.7.2 lo ha reso un po 'più veloce.


Ho fatto una traduzione C # ma usando array ordinati e facendo l'aggiunta in ordine piuttosto che per set contiene controlli. È molto più lento e non ho ancora creato un profilo per vedere se ciò è dovuto al fatto di non memorizzare la funzione successore o al costo dei confronti.
Peter Taylor,

1
Non ho profilato il codice (geniale) di @ flornquake, ma suppongo che gran parte del tempo della CPU viene impiegato per eseguire test di appartenenza e operazioni di manipolazione, che sono entrambi abbastanza ben ottimizzati in Python, usando la sua onnipresente tabella hash e chiave hash routine. La memorizzazione è certamente una cosa importante, con un algoritmo esponenziale come questo. Se lo hai lasciato fuori, puoi aspettarti prestazioni esponenzialmente più lente.
Tobia,

@Tobia, in realtà ho scoperto che in C # il memoising la funzione successiva ha rallentato. Ho anche scoperto che una traduzione più letterale (usando le operazioni set) era significativamente più lenta della mia aggiunta di livello inferiore. L'unico vero miglioramento che ho riscontrato rispetto al mio codice originale è stato quello di tener conto (a^b)^c = (a^c)^b, ed è ancora molto più lento di questa implementazione di Python.
Peter Taylor,

@PeterTaylor: Modifica: per quanto posso vedere, l'algoritmo di flornquake si basa sulla costruzione di gruppi di alberi, in cui un albero è un insieme di alberi stessi e così via. Tutti i pezzi di questi alberi, dal più piccolo set vuoto al più grande set di set, vengono memorizzati. Ciò significa che tutti questi alberi contengono "struttura ripetuta" che viene calcolata una sola volta (dalla CPU) e memorizzata una volta (nella RAM). Sei sicuro che il tuo algoritmo di "addizione in ordine" identifichi tutta questa struttura ripetuta e la computi una volta? (quella che ho chiamato sopra complessità complessità esponenziale) Vedi anche en.wikipedia.org/wiki/Dynamic_programming
Tobia

@Tobia, ci siamo sovrapposti. Ho pubblicato il codice.
Peter Taylor,

5

C #

Questa è una traduzione del codice Python di flornquake in C # usando una routine di aggiunta di livello inferiore che fornisce una velocità moderata rispetto a una traduzione diretta. Non è la versione più ottimizzata che ho, ma è un po 'più lunga perché deve memorizzare la struttura ad albero e i valori.

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;
        }
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.