Мне дана формула f (n), где f (n) определяется для всех неотрицательных целых чисел как:

f(0) = 1

f(1) = 1

f(2) = 2

f(2n) = f(n) + f(n + 1) + n (for n > 1)

f(2n + 1) = f(n - 1) + f(n) + 1 (for n >= 1)

Моя цель - найти для любого заданного числа s наибольшее n, где f (n) = s. Если такого n нет, верните None. s может быть до 10 ^ 25.

У меня есть решение методом грубой силы, использующее как рекурсию, так и динамическое программирование, но ни одно из них не является достаточно эффективным. Какие концепции могут помочь мне найти эффективное решение этой проблемы?

2
HawkStorm 30 Ноя 2014 в 10:21

4 ответа

Лучший ответ

Я хочу добавить небольшой анализ сложности и оценить размер f (n).

Если вы посмотрите на один рекурсивный вызов f (n), вы заметите, что вход n в основном делится на 2 перед вызовом f (n) еще два раза, где всегда один вызов имеет четное значение, а другой - нечетный ввод.
Таким образом, дерево вызовов - это в основном двоичное дерево, где всегда половина узлов на определенной глубине k предоставляет слагаемое приблизительно n / 2 k + 1 . Глубина дерева равна log₂ (n).

Таким образом, значение f (n) в целом составляет около Θ (n / 2 ⋅ log₂ (n)).

Обратите внимание: это справедливо для четных и нечетных входов, но для четных входов значение примерно на дополнительное слагаемое на n / 2 больше. (Я использую-нотацию, чтобы не задумываться о некоторых константах).

Теперь о сложности:

Наивная грубая сила

Чтобы вычислить f (n), вы должны вызвать f (n) Θ (2 log₂ (n) ) = Θ (n) раз.
Поэтому, если вы хотите вычислить значения f (n) до тех пор, пока не достигнете s (или заметите, что n нет с f (n) = s), вам нужно вычислить f (n) s⋅log₂ (s) раз, что всего Θ (s²⋅log (s)).

Динамическое программирование

Если вы сохраняете каждый результат f (n), время вычисления f (n) сокращается до Θ (1) (но для этого требуется гораздо больше памяти). Таким образом, общая временная сложность уменьшится до (s⋅log (s)).

Примечание: поскольку мы знаем, что f (n) ≤ f (n + 2) для всех n, вам не нужно сортировать значения f (n) и выполнять двоичный поиск.

Использование двоичного поиска

Алгоритм (ввод s):

  1. Установите l = 1 и r = s
  2. Установите n = (l + r) / 2 и округлите до следующего четного числа.
  3. вычислить val = f (n).
  4. если val == s, то верните n.
  5. если val иначе установите r = n.
  6. перейти к 2

Если вы нашли решение, хорошо. Если нет: попробуйте еще раз, но округлите на шаге 2 до нечетных чисел. Если и это не возвращает решения, значит, решения вообще не существует.

Это займет у вас Θ (log (s)) для двоичного поиска и Θ (s) для вычисления f (n) каждый раз, так что в сумме вы получите Θ (s⋅log (s)).

Как видите, оно имеет ту же сложность, что и решение для динамического программирования, но вам не нужно ничего сохранять.

Примечание: r = s не выполняется для всех s в качестве начального верхнего предела. Однако, если s достаточно велико, оно выполняется. Для экономии можно изменить алгоритм:
сначала проверьте, если f (s)

1
Community 20 Июн 2020 в 09:12
  1. Можете ли вы вычислить значение f (x), которое равно x от 0 до MAX_SIZE, только один раз?

    Я имею в виду: вычислить значение по DP.

    f (0) = 1
    f (1) = 1
    f (2) = 2
    f (3) = 3
    f (4) = 7
    f (5) = 4
    ... ...
    f (MAX_SIZE) = ???

  2. Если 1-й шаг недопустим, выйдите. В противном случае отсортируйте значение от маленького к большему.
    Например, 1,1,2,3,4,7, ...
    Теперь вы можете узнать, существует ли n, удовлетворяющее f (n) = s за время O (log (MAX_SIZE)).

0
loverszhaokai 30 Ноя 2014 в 08:04

К сожалению, вы не упоминаете, насколько быстрым должен быть ваш алгоритм. Возможно, вам нужно найти действительно умную переписать формулу, чтобы сделать ее достаточно быстрой, в этом случае вы можете разместить этот вопрос на математическом форуме.

Время выполнения вашей формулы составляет O (n) для f (2n + 1) и O (n log n) для f (2n), согласно Главной теореме, поскольку:

T_even (n) = 2 * T (n / 2) + n / 2

T_odd (n) = 2 * T (n / 2) + 1

Таким образом, время выполнения общей формулы составляет O (n log n).

Таким образом, если n является ответом на проблему, этот алгоритм будет работать прибл. O (n ^ 2 log n), потому что вам нужно выполнить формулу примерно n раз.

Вы можете сделать это немного быстрее, сохранив предыдущие результаты, но, конечно, это компромисс с памятью.

Ниже представлено такое решение на Python.

D = {}

def f(n):
    if n in D:
        return D[n]
    if n == 0 or n == 1:
        return 1
    if n == 2:
        return 2
    m = n // 2
    if n % 2 == 0:
        # f(2n) = f(n) + f(n + 1) + n (for n > 1)
        y = f(m) + f(m + 1) + m
    else:
        # f(2n + 1) = f(n - 1) + f(n) + 1 (for n >= 1)
        y = f(m - 1) + f(m) + 1
    D[n] = y
    return y

def find(s):
    n = 0
    y = 0
    even_sol = None
    while y < s:
        y = f(n)
        if y == s:
            even_sol = n
            break
        n += 2
    n = 1
    y = 0
    odd_sol = None
    while y < s:
        y = f(n)
        if y == s:
            odd_sol = n
            break
        n += 2
    print(s,even_sol,odd_sol)

find(9992)
0
wvdz 30 Ноя 2014 в 09:39

Эта рекурсия на каждой итерации для 2n и 2n+1 увеличивает значения, поэтому, если в любой момент вы получите значение больше, чем s, вы можете остановить свой алгоритм.

Чтобы сделать эффективный алгоритм, вам нужно найти или красивую формулу, которая будет вычислять значение, или сделать это в небольшом цикле, который будет намного, намного, намного более эффективным, чем ваша рекурсия. Ваша рекурсия обычно O (2 ^ n), где цикл - O (n). Так может выглядеть петля:

int[] values = new int[1000];
values[0] = 1;
values[1] = 1;
values[2] = 2;
for (int i = 3; i < values.length /2 - 1; i++) {
    values[2 * i] = values[i] + values[i + 1] + i;
    values[2 * i + 1] = values[i - 1] + values[i] + 1;
}

И внутри этого цикла добавить условие возможного его прерывания с успехом неудачи.

0
AbcAeffchen 30 Ноя 2014 в 23:44