E147. Поиск подотрезка mảngа с максимальной/минимальной суммой

e-maxx algorithm original: C/C++ #algorithm #array #emaxx #misc #search
Văn bản bài toán được dịch từ tiếng Nga theo ngôn ngữ giao diện. Mã không thay đổi.

Источник: e-maxx.ru/algo, страница PDF 484.

Здесь мы рассмотрим задачу о поиске подотрезка mảngа с максимальной суммой ("maximum subarray problem" на английском), а также некоторые её вариации (в том числе Thuật toán решения варианта этой задачи в режиме онлайн — описанный автором Thuật toánа — KADR (Ярослав Твердохлеб)).

Постановка задачи

Дан mảng чисел

. it is required find такой его подотрезок

, что сумма на нём максимальна:

НаVí dụ, если бы все числа mảngа

были бы неотрицательными, то в качестве ответа можно было бы взять весь mảng. Lời giải нетривиально, когда mảng может содержать как положительные, так и отрицательные числа. Понятно, что Bài toán о поиске минимального подотрезка — по сути та же самая, достаточно лишь изменить знаки всех чисел на противоположные.

Thuật toán 1

Здесь мы рассмотрим практически очевидный Thuật toán. (Дальше мы рассмотрим другой Thuật toán, который чуть сложнее придумать, однако его Cài đặt получается ещё короче.)

Описание Thuật toánа

Thuật toán весьма прост. Введём для удобства обозначение:

. Т.е. mảng

— это mảng частичных сумм

mảngа

. Также положим значение

.

Будем теперь перебирать индекс

, и научимся для каждого текущего значения

быстро

находить оптимальное

, при котором достигается максимальная сумма на подотрезке

.

Формально это означает, что нам надо для текущего

find такое

(не превосходящее

), чтобы

величина

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

find минимальное значение в mảngе

минимум на отрезке

. Отсюда мы сразу получаем Thuật toán решения: мы просто будем хранить, где в mảngе

находится текущий

минимум. Используя этот минимум, мы за

находим текущий оптимальный индекс

, а при переходе от

текущего индекса

к следующему мы просто обновляем этот минимум.

Очевидно, этот Thuật toán работает за

и асимптотически оптимален.

Cài đặt

Для реализации нам даже не понадобится явно хранить mảng частичных сумм

— от него нам будет

требоваться только текущий element. Cài đặt приводится в 0-индексированных mảngах, а не в 1-нумерации, как было описано выше. Приведём сначала Lời giải, которое находит просто численный ответ, не находя индексы искомого отрезка:

int ans = a[0],

sum = 0,

min_sum = 0;

for (int r=0; r<n; ++r) {

sum += a[r];

ans = max (ans, sum - min_sum);

min_sum = min (min_sum, sum);

} Теперь приведём полный вариант решения, который параллельно с numberвым Lời giảiм находит границы искомого отрезка:

int ans = a[0],

ans_l = 0,

ans_r = 0,

sum = 0,

min_sum = 0,

min_pos = -1;

for (int r=0; r<n; ++r) {

sum += a[r];

int cur = sum - min_sum;
if (cur > ans) {

ans = cur;

ans_l = min_pos + 1;

ans_r = r;

}

if (sum < min_sum) {

min_sum = sum;

min_pos = r;

} }

Thuật toán 2

Здесь мы рассмотрим другой Thuật toán. Его чуть сложнее понять, но зато он более элегантен, чем приведённый выше, и реализуется чуть-чуть короче. Этот Thuật toán был предложен Джеем Каgivenм (Jay Kadane) в 1984 г.

Описание Thuật toánа

Сам Thuật toán выглядит следующим образом. Будем идти по mảngу и накапливать в некоторой переменной

текущую частичную сумму. Если в какой-то момент

окажется отрицательной, то мы просто присвоим

. Утверждается, что максимум из всех значений переменной

, случившихся за Thời gian chạy, и будет ответом

на задачу. Докажем этот Thuật toán.

В самом деле, рассмотрим первый момент времени, когда сумма

стала отрицательной. Это означает, что, стартовав

с нулевой частичной суммы, мы в итоге пришли к отрицательной частичной сумме — значит, и весь этот префикс mảngа, равно как и любой его суффикс имеют отрицательную сумму. Следовательно, от всего этого префикса mảngа в дальнейшем не может быть никакой пользы: он может дать только отрицательную прибавку к ответу. Однако этого недостаточно для доказательства Thuật toánа. В Thuật toánе мы, фактически, ограничиваемся в поиске ответа только такими отрезками, которые начинаются непосредственно после мест, когда случалось .

Но, в самом деле, рассмотрим произвольный отрезок

, причём

не находится в такой "критической" позиции (т. е.

, где

— последняя такая позиция, в которой

). Поскольку последняя критическая

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

, то получается, что сумма

неотрицательна.

Это означает, что, сдвинув

в позицию

, мы увеличим ответ или, в крайнем случае, не изменим его. Так или иначе, но получается, что действительно при поиске ответа можно ограничиться только отрезками, начинающимися сразу после позиций, в которых оказывалось . Это доказывает правильность Thuật toánа.

Cài đặt

Как и в Thuật toánе 1, приведём сначала упрощённую реализацию, которая ищет только numberвой ответ, не находя границ искомого отрезка:

int ans = a[0],

sum = 0;

for (int r=0; r<n; ++r) {

sum += a[r];

ans = max (ans, sum);

sum = max (sum, 0);

} Полный вариант решения, с поддержанием индексов-границ искомого отрезка:

int ans = a[0],

ans_l = 0,

ans_r = 0,

sum = 0,

minus_pos = -1;

for (int r=0; r<n; ++r) {

sum += a[r];

if (sum > ans) {

ans = sum;

ans_l = minus_pos + 1;

ans_r = r;

}

if (sum < 0) {

sum = 0;

minus_pos = r;

} }

Смежные задачи

Поиск максимального/минимального подотрезка с Ràng buộcми

Если в условии задачи на искомый отрезок

накладываются дополнительные Ràng buộc (наVí dụ, что

длина

отрезка должна находиться в заданных пределах), то описанный Thuật toán скорее всего легко обобщается на эти случаи — так или иначе, Bài toán будет по-прежнему заключаться в поиске минимума в

mảngе

при заданных дополнительных Ràng buộcх.

Двумерный случай задачи: поиск максимальной/

минимальной подматрицы

Описанная в данной статье Bài toán естественно обобщается на большие размерности. НаVí dụ, в двумерном случае

она превращается в поиск такой подматрицы

заданной матрицы, которая имеет

максимальную сумму чисел в ней. Из описанного выше решения для одномерного случая легко получить Lời giải за

: переберём

и

,

и посчитаем mảng сумм с

по

в каждой строке матрицы; мы пришли к одномерной задаче поиска индексов

и

в этом mảngе, которую уже можно решать за линейное время. Более быстрые Thuật toánы решения этой задачи хотя и известны, однако они не сильно быстрее

, и

при этом весьма сложны (настолько сложны, что по скрытой константе многие из них уступают тривиальному Thuật toánу при всех разумных Ràng buộcх). По всей видимости, лучший из известных Thuật toánов работает

за

(T. Chan 2007 "More algorithms for all-pairs shortest paths in weighted graphs"). Этот Thuật toán Chan, а также многие другие результаты в данной области на самом деле описывают быстрое умножение матриц (где под умножением матриц подразумевается модифицированное умножение: вместо сложения используется минимум, а вместо умножения — сложение). Дело в том, что Bài toán о поиске подматрицы с наибольшей суммой сводится к задаче о поиске кратчайших путей между всеми парами вершин, а эта Bài toán, в свою очередь — сводится к такому умножению матриц.

Поиск подотрезка с максимальной/минимальной средней суммой

Эта Bài toán заключается в том, что надо find такой отрезок

, чтобы среднее значение на нём было максимальным:

Конечно, если на искомый отрезок

по условию не наложено других условий, то Lời giảiм всегда будет

являться отрезок длины

в точке-максимуме mảngа. Bài toán имеет смысл, только если

имеются дополнительные Ràng buộc (наVí dụ, длина искомого отрезка ограничена снизу). В таком случае применим стандартный приём при работе с Bài toánми о среднем значении: будем подбирать искомую максимальную среднюю величину двоичным поиском.

Для этого нам надо научиться решать такую подзадачу: given number

, и надо проверить, есть ли подотрезок mảngа

(конечно, удовлетворяющий всем дополнительным Ràng buộcм задачи), на котором среднее значение больше .

Чтобы решить эту подзадачу, отнимем

от каждого elementа mảngа

. Тогда наша подBài toán

фактически превращается в такую: есть или нет в данном mảngе подотрезок положительной суммы. А эту задачу мы уже умеем решать.

Таким образом, мы получили Lời giải за асимпотику

, где

— требуемая точность,

время решения подзадачи для mảngа длины

(которое может варьироваться в зависимости от

конкретных накладываемых дополнительных ограничений).

Lời giải задачи в режиме онлайн

Đề bài задачи таково: дан mảng из

чисел, а также given number

. Поступают запросы вида

, и в ответ на

запрос it is required find подотрезок отрезка

длины не менее

с максимально возможным средним арифметическим. Thuật toán решения этой задачи достаточно сложен. Автор данного Thuật toánа — KADR (Ярослав Твердохлеб) — описал данный Thuật toán в своём сообщении на форуме.

C# lời giải

bản nháp tự động, xem lại trước khi gửi
using System;
using System.Collections.Generic;
using System.Linq;

public static class AlgorithmDraft
{
    // Auto-generated C# draft from the original e-maxx C/C++ listing. Review before production use.
    int ans = a[0],
            sum = 0,
            min_sum = 0;
    for (int r=0; r<n; ++r) {
            sum += a[r];
            ans = max (ans, sum - min_sum);
            min_sum = min (min_sum, sum);
    }
    int ans = a[0],
            ans_l = 0,
            ans_r = 0,
            sum = 0,
            min_sum = 0,
            min_pos = -1;
    for (int r=0; r<n; ++r) {
            sum += a[r];
            int cur = sum - min_sum;
            if (cur > ans) {
                    ans = cur;
                    ans_l = min_pos + 1;
                    ans_r = r;
            }
            if (sum < min_sum) {
                    min_sum = sum;
                    min_pos = r;
            }
    }
    int ans = a[0],
            sum = 0;
    for (int r=0; r<n; ++r) {
            sum += a[r];
            ans = max (ans, sum);
            sum = max (sum, 0);
    }
    int ans = a[0],
            ans_l = 0,
            ans_r = 0,
            sum = 0,
            minus_pos = -1;
    for (int r=0; r<n; ++r) {
            sum += a[r];
            if (sum > ans) {
                    ans = sum;
                    ans_l = minus_pos + 1;
                    ans_r = r;
            }
            if (sum < 0) {
                    sum = 0;
                    minus_pos = r;
            }
    }
}

C++ lời giải

đã khớp/gốc
int ans = a[0],
        sum = 0,
        min_sum = 0;
for (int r=0; r<n; ++r) {
        sum += a[r];
        ans = max (ans, sum - min_sum);
        min_sum = min (min_sum, sum);
}
int ans = a[0],
        ans_l = 0,
        ans_r = 0,
        sum = 0,
        min_sum = 0,
        min_pos = -1;
for (int r=0; r<n; ++r) {
        sum += a[r];
        int cur = sum - min_sum;
        if (cur > ans) {
                ans = cur;
                ans_l = min_pos + 1;
                ans_r = r;
        }
        if (sum < min_sum) {
                min_sum = sum;
                min_pos = r;
        }
}
int ans = a[0],
        sum = 0;
for (int r=0; r<n; ++r) {
        sum += a[r];
        ans = max (ans, sum);
        sum = max (sum, 0);
}
int ans = a[0],
        ans_l = 0,
        ans_r = 0,
        sum = 0,
        minus_pos = -1;
for (int r=0; r<n; ++r) {
        sum += a[r];
        if (sum > ans) {
                ans = sum;
                ans_l = minus_pos + 1;
                ans_r = r;
        }
        if (sum < 0) {
                sum = 0;
                minus_pos = r;
        }
}

Java lời giải

bản nháp tự động, xem lại trước khi gửi
import java.util.*;
import java.math.*;

public class AlgorithmDraft {
    // Auto-generated Java draft from the original e-maxx C/C++ listing. Review before production use.
    int ans = a[0],
            sum = 0,
            min_sum = 0;
    for (int r=0; r<n; ++r) {
            sum += a[r];
            ans = max (ans, sum - min_sum);
            min_sum = min (min_sum, sum);
    }
    int ans = a[0],
            ans_l = 0,
            ans_r = 0,
            sum = 0,
            min_sum = 0,
            min_pos = -1;
    for (int r=0; r<n; ++r) {
            sum += a[r];
            int cur = sum - min_sum;
            if (cur > ans) {
                    ans = cur;
                    ans_l = min_pos + 1;
                    ans_r = r;
            }
            if (sum < min_sum) {
                    min_sum = sum;
                    min_pos = r;
            }
    }
    int ans = a[0],
            sum = 0;
    for (int r=0; r<n; ++r) {
            sum += a[r];
            ans = max (ans, sum);
            sum = max (sum, 0);
    }
    int ans = a[0],
            ans_l = 0,
            ans_r = 0,
            sum = 0,
            minus_pos = -1;
    for (int r=0; r<n; ++r) {
            sum += a[r];
            if (sum > ans) {
                    ans = sum;
                    ans_l = minus_pos + 1;
                    ans_r = r;
            }
            if (sum < 0) {
                    sum = 0;
                    minus_pos = r;
            }
    }
}

Материал разбит как Thuật toánическая Bài toán: изучить постановку, понять асимптотику и реализовать Thuật toán на выбранном языке.

Vacancies for this task

việc làm đang hoạt động with overlapping task tags are đã hiển thị.

Tất cả việc làm
Chưa có việc làm đang hoạt động.