python - алгоритм суммы префиксов

Я пытаюсь понять идею концепции суммы префиксов, глядя на пример, представленный в уроке суммы префиксов от Codility здесь (Задача грибника)

Насколько я понимаю, вся концепция основана на простом свойстве, где для нахождения суммы всех элементов между двумя позициями A (pos_left, pos_right) массива A используется второй массив P, где все элементы последовательно суммируются и где искомые сумма вычисляется как
значение(P(pos_right + 1)) - значение(P(pos_left)).

A 1 2 3 4 5  6
P 0 1 3 6 10 15 21
sum of all elements between A[2] and A[5] = 3+ 4 + 5 = 12
or using the prefix sums"   P[5+1] - P[2] = 15 -3 = 12 

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

Глядя на пример, я не понимаю логики построения циклов. Может ли кто-нибудь прояснить механику этого алгоритма?

Во-вторых, индексация позиций в этом примере показалась мне очень запутанной и громоздкой. Является ли обычной практикой «сдвиг» вектора с префиксными суммами с нулем в начале? (тот факт, что подсчет элементов в векторах начинается по умолчанию с 0 в питоне, уже вызывает некоторую путаницу).

Решение

def prefix_sums(A):
  n = len(A)
  P = [0] * (n + 1)
  for k in xrange(1, n + 1):
      P[k] = P[k - 1] + A[k - 1]
  return P


def count_total(P, x, y):
    return P[y + 1] - P[x]

# A mushroom picker is at spot number k on the road and should perform m moves
def mushrooms(A, k, m):
    n = len(A)
    result = 0
    pref = prefix_sums(A)
    for p in xrange(min(m, k) + 1):   # going left
        left_pos = k - p
        right_pos = min(n - 1, max(k, k + m - 2 * p))
        result = max(result, count_total(pref, left_pos, right_pos))
    for p in xrange(min(m + 1, n - k)):
        right_pos = k + p
        left_pos = max(0, min(k, k - (m - 2 * p)))
        result = max(result, count_total(pref, left_pos, right_pos))
    return result   

Я запустил пример для небольшого массива A= [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] , выбрал позицию k=5 и диапазон m = 3. Я не понимаю логику создания диапазонов для проверки двумя циклами.

Я получаю следующие параметры для циклов

(p=, left_pos=, right_pos=)   
loop 1  (0,5,8), (1,4,6),(2,3,5),(3,2,5)
loop 2  (0,2,5), (1,4,6), (2,5,7), (3,5,8)

Диапазоны варьируются. Почему?

версия для отладки

def mushrooms2(A, k, m):
    n = len(A)
    result = 0
    pref = prefix_sums(A)
    l1 =min(m, k) + 1
    print 'loop p in xrange(min(m, k) + 1): %d' % l1
    for p in xrange(min(m, k) + 1):
        print 'p %d' % p
        print 'A= %r' % A
        print 'pref= %r' % pref
        left_pos = k - p
        right_pos = min(n - 1, max(k, k + m - 2 * p))
        result = max(result, count_total(pref, left_pos, right_pos))
        print 'left_pos = k - p= %d' % left_pos
        print 'right_pos= min(n-1,max(k,k+m-2*p))= %d' % right_pos
        print 'max'
        print '(result %d' % result
        print 'count_total(pref, left_pos, right_pos)) %r, %r, %r, %r' % (pref,left_pos, right_pos,count_total(pref, left_pos, right_pos))
        print 'result= %d' % result
        print 'next p'
    l2=min(m + 1, n - k)
    print   'loop xrange(min(m + 1, n - k)): %d' % l2
    for p in xrange(min(m + 1, n - k)):
        print 'p %d' % p
        print 'A= %r' % A
        print 'pref= %r' % pref
        right_pos = k + p
        left_pos = max(0, min(k, k - (m - 2 * p)))
        result = max(result, count_total(pref, left_pos, right_pos))
        print 'right_pos = k + p= %d' % right_pos
        print 'left_pos = max(0, min(k, k - (m - 2 * p)))= %d' % left_pos
        print 'max'
        print '(result %d' % result
        print 'count_total(pref, left_pos, right_pos)) %r, %r, %r, %r' % (pref,left_pos, right_pos,count_total(pref, left_pos, right_pos))
        print 'result= %d' % result
        print 'next p'
    print 'result %d' % result
    return result

person Chris    schedule 31.10.2016    source источник
comment
Индексация/срезы Python основаны на нуле. В зависимости от того, чего вы пытаетесь достичь, использование вычисляемых переменных цикла для срезов или индексов может быть продуктивным.   -  person wwii    schedule 31.10.2016


Ответы (2)


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

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

Что касается деталей циклов, то вместо того, чтобы думать о цикле с точки зрения переменных цикла (т.е. p), проще выяснить, что изменяется в ходе цикла и как используется p. В противном случае выяснение того, что находится в этих выражениях min и max, поначалу кажется слишком странным.

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

В частности, когда p == 0, левая конечная точка является начальным индексом (т.е. k), а когда p равно min(m, k), то это либо 0 (т.е. если k < m), либо (k - m). В первом случае это максимальное расстояние, которое может пройти левая конечная точка, так как она вышла бы за допустимый диапазон точек на дороге. В последнем случае количество ходов запрещает любое решение с left_pos меньшим, чем (k - m), поскольку невозможно перейти от k к этим индексам за m ходов.

Аналогично можно объяснить присвоение right_pos в первом цикле. min включает (n-1), который является самым правым допустимым индексом, который может быть достигнут, и он служит для удержания правильной конечной точки в допустимых пределах. Внутренний оператор max содержит k, так как это наименьшее возможное значение для right_pos. (то есть из-за того, что k является отправной точкой) У него также есть выражение (k + m - 2 * p). Это выражение представляет следующий процесс:

  • Идите налево на p ходов.
  • Измените направление и идите вправо на p ходов, чтобы добраться до начальной точки.
  • Идите направо с оставшимися (m - 2p) ходами.

Второй цикл — это просто отражение первого цикла, и вы можете объяснить его, просто адаптировав мое объяснение первого цикла.

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

def prefix_sums(A):
    n = len(A)
    P = [0] * n
    P[0] = A[0]
    for k in xrange(1, n):
        P[k] = P[k - 1] + A[k]
    return P

def count_total(P, x, y):
    return (P[y] - P[x - 1] if x > 0 else P[y])

Фундаментальная идея вышеприведенной реализации заключается в том, что в P[x] у нас есть сумма A[0] + A[1] + ... + A[x].

person ilim    schedule 31.10.2016
comment
@ ilim ваша версия prefix_sums(A) возвращает ошибку, я думаю, это связано с тем, что в A всего n элементов, но цикл доходит до n+1, поэтому отсутствует аргумент для A[n+1] - person Chris; 31.10.2016
comment
Вы правы. Извиняюсь за недостаточно внимательное наблюдение. - person ilim; 01.11.2016
comment
@ Крис, ты уже пробовал? - person ilim; 02.11.2016
comment
@ilim Замечательное объяснение. Спасибо :) - person Paritosh Gupta; 03.05.2017
comment
@ParitoshGupta Рад помочь. - person ilim; 03.05.2017
comment
Должен ли я использовать p[y] - (p[x - 1] if x > 0 else 0) в счете, чтобы, если я хочу получить счет между 0-й позицией и 4-й позицией, он все равно работал, а не превышал границы? Я думаю, именно поэтому люди расширяют его с помощью [0] в начале, так что x-1 все еще работает. - person Andrew Backer; 23.04.2019
comment
@AndrewBacker Кажется, я упустил из виду этот пограничный случай. Починил это. - person ilim; 09.05.2019
comment
@ilim: Отличное объяснение! Спасибо! - person HMartyrossian; 06.06.2019

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

Трудное для понимания решение № 2 просто имитирует движение влево и вправо и все эти странно выглядящие вычисления только для получения левого и правого пределов области (так как вы действительно перемещаетесь внутри нее). Таким образом, каждая итерация означает один полный цикл с использованием 6 шагов.

Если вы двигаетесь влево, а затем вправо (p=0...M), у вас есть

  • 0 шагов влево, 6 шагов вправо (на самом деле 0 и 2 шага вызывают выход за границу массива), поэтому левая граница области находится в индексе 4, а правая граница - в индексе 6.
  • 1 шаг влево, 5 шагов вправо (на самом деле 1 и 3), поэтому левая граница находится в индексе 3, а правая граница - в индексе 6.
  • 2 шага влево, 4 шага вправо(на самом деле 2 и 4)...продолжаем расчеты

Вот моя версия PHP с упрощенным кодом и дополнительными переменными для облегчения понимания.

function prefix_sums(array $a)
{
    $n = count($a);
    $p = array_fill(0, $n + 1, 0);
    for ($i = 1; $i <= $n; $i++) {
        $p[$i] = $p[$i - 1] + $a[$i - 1];
    }
    return $p;
}

function count_total($p, $x, $y)
{
    return $p[$y + 1] - $p[$x];
}

function mushrooms(array $a, int $k, int $m)
{
    $n = count($a) - 1;
    $max = 0;
    $sums = prefix_sums($a);
    //start  moving to the left and then the right
    for ($p = 0; $p < $m; $p++) {
        $stepsLeft = $p;
        $realStepsLeft = min($k, $stepsLeft);
        $leftBorder = $k - $realStepsLeft;

        $stepsRight = $m - $stepsLeft;
        $realStepsRight = min($n - $leftBorder, $stepsRight);
        $rightBorder = $leftBorder + $realStepsRight;

        $max = max($max, count_total($sums, $leftBorder, $rightBorder));
    }
    //moving to the right and then the left
    for ($p = 0; $p < $m; $p++) {
        $stepsRight = $p;
        $realStepsRight = min($p, $n - $k);
        $rightBorder = $k + $realStepsRight;

        $stepsLeft = $m - $stepsRight;
        $realStepsLeft = min(($k + $realStepsRight), $stepsLeft);
        $leftBorder = $rightBorder - $realStepsLeft;

        $max = max($max, count_total($sums, $leftBorder, $rightBorder));
    }
    return $max;
}

assert(ASSERT_EXCEPTION, 1);
assert(mushrooms([2, 3, 7, 5, 1, 3, 9], 4, 6) == 25);

echo 'Success';
person illia permiakov    schedule 25.09.2019