Euler № 15 проекта

Вчера вечером я пытался решить проблему № 15 от Euler Проекта:

При запуске в верхнем левом углу 2×2 сетка, существует 6 маршрутов (не отслеживая в обратном порядке) к правому нижнему углу.

alt text
(источник: projecteuler.net)

Сколько маршрутов там через 20×20 сетка?

Я полагал, что это не должно быть настолько твердо, таким образом, я записал основную рекурсивную функцию:

const int gridSize = 20;

// call with progress(0, 0)
static int progress(int x, int y)
{
    int i = 0;

    if (x < gridSize)
        i += progress(x + 1, y);
    if (y < gridSize)
        i += progress(x, y + 1);

    if (x == gridSize && y == gridSize)
        return 1;

    return i;
}

Я проверил, что это работало на меньшие сетки такой как 2×2 или 3×3 и затем установило его для выполнения за 20×20 сетка. Вообразите мое удивление, когда 5 часов спустя программа все еще счастливо производила подсчеты, и сделанных только приблизительно 80% (на основе исследования ее текущей позиции / маршрут в сетке).

Очевидно я иду об этом неправильным путем. Как Вы решили бы эту проблему? Я думаю, что это должно быть решено с помощью уравнения, а не метода как мой, но это - к сожалению, не сильная моя сторона.

Обновление:

У меня теперь есть рабочая версия. В основном это кэширует результаты, полученные прежде, когда n×m блок все еще остается быть пересеченным. Вот код наряду с некоторыми комментариями:

// the size of our grid
static int gridSize = 20;

// the amount of paths available for a "NxM" block, e.g. "2x2" => 4
static Dictionary pathsByBlock = new Dictionary();

// calculate the surface of the block to the finish line
static long calcsurface(long x, long y)
{
    return (gridSize - x) * (gridSize - y);
}

// call using progress (0, 0)
static long progress(long x, long y)
{
    // first calculate the surface of the block remaining
    long surface = calcsurface(x, y);
    long i = 0;

    // zero surface means only 1 path remains
    // (we either go only right, or only down)
    if (surface == 0)
        return 1;

    // create a textual representation of the remaining
    // block, for use in the dictionary
    string block = (gridSize - x) + "x" + (gridSize - y);

    // if a same block has not been processed before
    if (!pathsByBlock.ContainsKey(block))
    {
        // calculate it in the right direction
        if (x < gridSize)
            i += progress(x + 1, y);
        // and in the down direction
        if (y < gridSize)
            i += progress(x, y + 1);

        // and cache the result!
        pathsByBlock[block] = i;
    }

    // self-explanatory :)
    return pathsByBlock[block];
}

Вызов его 20 раз, для сеток с размером 1×1 через 20×20 производит следующий вывод:

There are 2 paths in a 1 sized grid
0,0110006 seconds

There are 6 paths in a 2 sized grid
0,0030002 seconds

There are 20 paths in a 3 sized grid
0 seconds

There are 70 paths in a 4 sized grid
0 seconds

There are 252 paths in a 5 sized grid
0 seconds

There are 924 paths in a 6 sized grid
0 seconds

There are 3432 paths in a 7 sized grid
0 seconds

There are 12870 paths in a 8 sized grid
0,001 seconds

There are 48620 paths in a 9 sized grid
0,0010001 seconds

There are 184756 paths in a 10 sized grid
0,001 seconds

There are 705432 paths in a 11 sized grid
0 seconds

There are 2704156 paths in a 12 sized grid
0 seconds

There are 10400600 paths in a 13 sized grid
0,001 seconds

There are 40116600 paths in a 14 sized grid
0 seconds

There are 155117520 paths in a 15 sized grid
0 seconds

There are 601080390 paths in a 16 sized grid
0,0010001 seconds

There are 2333606220 paths in a 17 sized grid
0,001 seconds

There are 9075135300 paths in a 18 sized grid
0,001 seconds

There are 35345263800 paths in a 19 sized grid
0,001 seconds

There are 137846528820 paths in a 20 sized grid
0,0010001 seconds

0,0390022 seconds in total

Я принимаю ответ danben, потому что его помогшие меня находят это решение больше всего. Но upvotes также Tim Goodman и Agos :)

Бонусное обновление:

После чтения ответа Eric Lippert я бросил другой взгляд и переписал его несколько. Основная идея является все еще тем же, но кэширующаяся часть была вынута и вставила отдельную функцию, как в примере Eric. Результатом является некоторый намного более изящно выглядящий код.

// the size of our grid
const int gridSize = 20;

// magic.
static Func Memoize(this Func f)
{
    // Return a function which is f with caching.
    var dictionary = new Dictionary();
    return (A1 a1, A2 a2) =>
    {
        R r;
        string key = a1 + "x" + a2;
        if (!dictionary.TryGetValue(key, out r))
        {
            // not in cache yet
            r = f(a1, a2);
            dictionary.Add(key, r);
        }
        return r;
    };
}

// calculate the surface of the block to the finish line
static long calcsurface(long x, long y)
{
    return (gridSize - x) * (gridSize - y);
}

// call using progress (0, 0)
static Func progress = ((Func)((long x, long y) =>
{
    // first calculate the surface of the block remaining
    long surface = calcsurface(x, y);
    long i = 0;

    // zero surface means only 1 path remains
    // (we either go only right, or only down)
    if (surface == 0)
        return 1;

    // calculate it in the right direction
    if (x < gridSize)
        i += progress(x + 1, y);
    // and in the down direction
    if (y < gridSize)
        i += progress(x, y + 1);

    // self-explanatory :)
    return i;
})).Memoize();

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

45
задан Glorfindel 12 June 2019 в 19:08
поделиться

11 ответов

Это можно сделать намного быстрее, если вы используете динамическое программирование. (сохранение результатов подзадач, а не их пересчет). Динамическое программирование может применяться к задачам, которые демонстрируют оптимальную подструктуру - это означает, что оптимальное решение может быть построено из оптимальных решений подзадач (кредит Википедия ).

Я бы предпочел не выдавать ответ, но подумайте, как количество путей к правому нижнему углу может быть связано с количеством путей к соседним квадратам.

Также - если бы вы собирались решить эту проблему вручную, как бы вы это сделали?

40
ответ дан 26 November 2019 в 20:49
поделиться

Несмотря на то, что динамическое программирование выглядит привлекательным способом решения проблемы (и делает его интересной задачей кодирования), немного творческого мышления в отношении структур данных дает чтобы дать немедленный ответ.

[остальная часть этого, по сути, является объяснением того, почему ответ Тима Гудмана лучший, для некоторого значения "лучший"]
Если у нас есть сетка nXm, мы можем представить каждый допустимый маршрут из угла в угол в виде битовой строки n + m, используя либо 0, либо 1 для представления «вниз». Немного подумав, можно подумать, что точное количество маршрутов - это количество способов взять N элементов из N + M элементов, и это, для удобства, оказывается стандартным простым комбинаторным M над N.

Итак, для любого N + M прямоугольник, количество возможных маршрутов из верхнего левого угла в нижний правый угол равно (n + m) (n + m-1) ... (m + 1) / ( п * (п-1) * ... 1).

Самая быстрая программа - это та, которая не должна хранить очень много, много использовать для хранения и (в идеале) имеет ответ в закрытой форме.

4
ответ дан 26 November 2019 в 20:49
поделиться

Вы можете вдвое сократить время расчета, приняв во внимание, что, как только вы уменьшите его до квадрата, сетка станет симметричной. Таким образом, если у вас есть равное количество свободного места в направлении X и Y для перемещения, вы можете использовать тот же расчет для увеличенного хода по оси x и увеличения хода по оси y.

При этом я сделал это на python и много кэшировал результаты, чтобы избежать пересчета.

1
ответ дан 26 November 2019 в 20:49
поделиться

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

0
ответ дан 26 November 2019 в 20:49
поделиться

Быстрое решение без программирования (на основе комбинаторики)

Я так понимаю, что «без возврата» означает, что мы всегда либо увеличиваем x, либо увеличиваем y.

Если это так, то мы знаем, что всего у нас будет 40 шагов, чтобы достичь финиша - 20 увеличений по x, 20 увеличений по y.

Единственный вопрос в том, какие из 40 увеличивают x. Проблема сводится к следующему: сколькими различными способами вы можете выбрать 20 элементов из набора из 40 элементов. (Элементы: шаг 1, шаг 2 и т. Д., И мы выбираем, скажем, те, которые увеличиваются по x).

Для этого есть формула: это биномиальный коэффициент с 40 сверху и 20 снизу.Формула 40! / ((20!) (40-20)!) , другими словами 40! / (20!) ^ 2 . Вот ! представляет собой факториал. (например, 5! = 5 * 4 * 3 * 2 * 1 )

Отмена одного из 20! и часть 40 !, это становится: (40 * 39 * 38 * 37 * 36 * 35 * 34 * 33 * 32 * 31 * 30 * 29 * 28 * 27 * 26 * 25 * 24 * 23 * 22 * 21) / (20 * 19 * 18 * 17 * 16 * 15 * 14 * 13 * 12 * 11 * 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1) . Таким образом, задача сводится к простой арифметической. Ответ: 137 846 528 820 .

Для сравнения обратите внимание, что (4 * 3) / (2 * 1) дает ответ из их примера, 6 .

48
ответ дан 26 November 2019 в 20:49
поделиться

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

Вы можете увидеть проблему в расположении ряда «правильных» и «вниз», при этом опасаясь, что не следует пересчитывать несколько раз идентичные расположения.
Например, решения проблемы размера 2 (указанные на изображениях в вопросе) можно увидеть следующим образом:

→→↓↓  
→↓→↓
→↓↓→
↓→→↓
↓→↓→
↓↓→→

Итак, для любой сетки со стороной n вы можете найти решение с помощью комбинаторики :

from math import factorial
n = 20
print factorial(2*n)/(factorial(n)*factorial(n))

2n! это количество комбинаций 20 → + 20 ↓, а два n! учитывать идентичные способы расположения знаков → и ↓.

19
ответ дан 26 November 2019 в 20:49
поделиться

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

Позвольте мне показать вам небольшой трюк программирования более высокого порядка, который принесет большие дивиденды. Возьмем более простую рекурсивную задачу:

long Fib(n) 
{
    if (n < 2) return 1;
    return Fib(n-1) + Fib(n-2);
}

Вы просите это вычислить Fib (5). Это вычисляет Fib (4) и Fib (3). Вычисление Fib (4) вычисляет Fib (3) и Fib (2). Вычисление Fib (3) вычисляет Fib (2) и Fib (1). Вычисление Fib (2) вычисляет Fib (1) и Fib (0). Теперь вернемся и снова вычислим Fib (2) . Затем мы возвращаемся и снова вычисляем Fib (3) . Огромное количество пересчетов.

Предположим, мы кэшировали результаты вычислений. Затем при втором запросе вычисления мы просто возвращали кешированный результат. Теперь перейдем к трюку высшего порядка.Я хочу представить эту концепцию «кэширования результатов функции» как функцию, которая принимает функцию и возвращает мне функцию с этим прекрасным свойством. Я напишу его как метод расширения для функций:

static Func<A, R> Memoize(this Func<A, R> f)
{
    // Return a function which is f with caching.
    var dictionary = new Dictionary<A, R>();
    return (A a)=>
    {
        R r;
        if(!dictionary.TryGetValue(a, out r))
        { // cache miss
            r = f(a);
            dictionary.Add(a, r);
        }
        return r;
    };
}

Теперь мы немного переписываем Fib:

Func<long, long> Fib = null;
Fib = (long n) => 
{
    if (n < 2) return 1;
    return Fib(n-1) + Fib(n-2);
};

Хорошо, у нас есть немемоизованная функция. Теперь волшебство:

Fib = Fib.Memoize();

И бум, когда мы вызываем Fib (5), теперь мы выполняем поиск по словарю. 5 отсутствует в словаре, поэтому мы вызываем исходную функцию. Это вызывает Fib (4), который выполняет еще один поиск в словаре и пропускает. Это вызывает Fib (3) и так далее. Когда мы возвращаемся к вызову Fib (2) и Fib (3) во время секунд , результаты уже находятся в словаре, поэтому мы не вычисляем их повторно.

Написание версии с двумя аргументами:

static Func<A1, A2, R> Memoize(this Func<A1, A2, R>) { ... }

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

progress = progress.Memoize();

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

40
ответ дан 26 November 2019 в 20:49
поделиться

Решения отражаются по диагонали с северо-запада на юго-восток вашей сети. Итак, вы должны вычислять решения только для верхней правой половины сетки, а затем отражать их, чтобы получить вторую половину ...

1
ответ дан 26 November 2019 в 20:49
поделиться

Кстати, вы можете еще больше повысить свою производительность, осознав, что 2x3 будет иметь такое же количество путей, что и 3x2. Ваша функция запоминания, похоже, учитывает только строку, которая представляет собой точно столбцы x строки. Однако вы можете включить в свое запоминание, чтобы указать общие пути для ключа 2x3, а также 3x2.

Таким образом, когда вы запоминаете 4x2 и т. Д., Он автоматически заполняет 2x4 таким же количеством путей. Это сократит ваше время, так как вы уже вычислили все пути через эту область поверхности однажды, так зачем делать это снова?

5
ответ дан 26 November 2019 в 20:49
поделиться

Мое решение было неожиданным, но довольно легким для понимания:

Количество маршрутов к данному перекрестку в сетке - это сумма количества маршрутов к двум его соседям.

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

для x или y = 0: сетка [x, y] = 1
для x и y> = 1: сетка [x, y] = сетка [x-1, y] + сетка [x, y -1]

Итак, после перебора всех квадратов окончательный ответ содержится в сетке [20,20].

0
ответ дан 26 November 2019 в 20:49
поделиться

Вы фактически вычисляете каталонские числа , для которых доступна замкнутая формула с использованием ряда Тейлора.

Итак, одна программа, вычисляющая решение, могла бы вычислить биномиальные коэффициенты, которые сложно получить правильно, если у вас нет класса BigInt ...

3
ответ дан 26 November 2019 в 20:49
поделиться
Другие вопросы по тегам:

Похожие вопросы: