Кількість можливих числових результатів круглих дужок 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>1. Послідовність починається як1, 1, 2, 4, 8, ... на відміну від каталонських чисел1, 2, 5, 14, 42, ...

Проблема полягає в тому, щоб записати найшвидшу програму (або функцію), яка приймає nяк вхід і повертає кількість операторів 2^2^...^2з можливих числових результатів виразу . У міру зростання продуктивність не повинна значно погіршуватися , тому прямий розрахунок башти з високою потужністю, мабуть, погана ідея.n^n


Я просто ділюсь ідеєю тут, але, здається, слід використовувати виключно додавання та множення, оскільки відповідь завжди буде такою формою 2^n, і тому було б зайвим слідкувати за будь-яким, крім n. Тобто просто використання правил експоненції здається мудрим. Однак, безумовно, є розумніший і цілком алгебраїчний спосіб зробити це.
Форс

@Fors Я думаю n, що все ще занадто великий для обчислення. Все-таки добре помічено. Може бути рекурсивне подання у формі "1 або 2 ^ (...) або (...) + (...)"; але у вас все ще виникає проблема, як нормалізувати таке подання числа (або порівняти два подання для рівності величин).
Джон Дворак

4
@JanDvorak, A002845 (закрита форма не надана)
Пітер Тейлор


1
@ Володимир Решетніков: Я думаю, що у вашій формулі є помилка по одному. Коли у вас є nдвійники і C_n=(2n)!/(n+1)!/n!має бути кількість круглих дужок, то для n = 3 вона повинна бути 5, правильно? Я бачу (2^2)^2і 2^(2^2), але які три інші комбінації? Я думаю, що C_n дає вам кількість круглих дужок для n + 1 двійки.
Мартін Тома

Відповіді:


9

Python 2.7

Цей підхід використовує наступні міркування:

Будь-яке ціле число може бути представлене як сума повноважень двох. Показники у владі двох можуть також бути представлені як сили двох. Наприклад:

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це працює в 8,7s на CPython 2.7.5 на моїй машині (трохи повільніше в 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 #, але використовував відсортовані масиви і робив додавання в порядку, а не набір містить чеки. Це набагато повільніше, і я ще не профлюював, щоб зрозуміти, чи це через непогашення функції спадкоємця або через вартість порівнянь.
Пітер Тейлор

1
Я не профілював (геніальний) код @ flornquake, але припускаю, що більша частина процесорного часу витрачається на тестування встановлених членств та встановлення маніпуляційних операцій, які в Python досить добре оптимізовані, використовуючи всюдисущу хеш-таблицю та хеш-ключ підпрограми. Пам'ять - це, звичайно, велика річ, з таким експоненціальним алгоритмом, як цей. Якщо ви цього не зробили, ви можете сподіватися, що експоненціально повільніше буде працювати.
Тобія

@Tobia, насправді я виявив, що в C # запам'ятовування функції спадкоємця робило її повільніше. Я також виявив, що більш буквальний переклад (з використанням заданих операцій) був значно повільнішим, ніж мій нижчий рівень додавання. Єдине справжнє вдосконалення, яке я знайшов у порівнянні зі своїм оригінальним кодом, було врахувати (a^b)^c = (a^c)^b, і це все набагато повільніше, ніж ця реалізація Python.
Пітер Тейлор

@PeterTaylor: Редагувати: Наскільки я бачу, алгоритм flornquake спирається на побудову наборів дерев, де дерево - це набір самих дерев тощо. Усі шматки цих дерев, від найменшого порожнього набору до найбільшого набору, запам'ятовуються. Це означає, що всі ці дерева містять "повторну структуру", яка обчислюється лише один раз (процесором) і зберігається один раз (в оперативній пам'яті). Ви впевнені, що ваш алгоритм "додавання в порядку" ідентифікує всю цю повторну структуру та обчислює її один раз? (що я назвав експоненціальною складністю вище) Дивіться також en.wikipedia.org/wiki/Dynamic_programming
Tobia

@Tobia, ми перекрилися. Я опублікував код.
Пітер Тейлор

5

C #

Це переклад Python-коду flornquake на 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;
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.