Перейти до основного вмісту

Пошук підмасиву з максимальною/мінімальною сумою

Тут ми розглядаємо задачу пошуку підмасиву з максимальною сумою, а також деякі її варіації (зокрема алгоритм для онлайн-розв'язання цієї задачі).

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

Дано масив чисел a[1n]a[1 \ldots n]. Потрібно знайти підмасив a[lr]a[l \ldots r] з максимальною сумою:

max1lrni=lra[i].\max_{ 1 \le l \le r \le n } \sum_{i=l}^{r} a[i].

Наприклад, якщо всі цілі числа в масиві a[]a[] невід'ємні, то відповіддю буде сам масив. Однак розв'язок стає нетривіальним, коли масив може містити як додатні, так і від'ємні числа.

Зрозуміло, що задача пошуку мінімального підмасиву по суті така сама — потрібно лише змінити знаки всіх чисел.

Коли підходить цей алгоритм?
  • Чи шукаємо неперервний підмасив (відрізок), а не довільну підмножину елементів?
  • Чи цільова функція — сума елементів (можливо, з додатними та від'ємними значеннями)?
  • Чи потрібно вкластися в один прохід O(n)O(n) замість перебору всіх O(n2)O(n^2) відрізків? (для пошуку максимального середнього з обмеженням на довжину — поєднай із бінарним пошуком)

Алгоритм 1

Тут ми розглянемо майже очевидний алгоритм. (Далі ми розглянемо інший алгоритм, який трохи важче придумати, але його реалізація ще коротша.)

Опис алгоритму

Алгоритм дуже простий.

Введемо для зручності позначення: s[i]=j=1ia[j]s[i] = \sum_{j=1}^{i} a[j]. Тобто масив s[i]s[i] — це масив часткових сум масиву a[]a[]. Також покладемо s[0]=0s[0] = 0.

Тепер ітеруватимемо по індексу r=1nr = 1 \ldots n і навчимося швидко знаходити оптимальне ll для кожного поточного значення rr, при якому досягається максимальна сума на підмасиві [l,r][l, r].

Формально це означає, що для поточного rr нам потрібно знайти таке ll (не більше за rr), щоб значення s[r]s[l1]s[r] - s[l-1] було максимальним. Після тривіального перетворення можна побачити, що нам потрібно знайти в масиві s[]s[] мінімум на відрізку [0,r1][0, r-1].

Звідси одразу отримуємо розв'язок: ми просто зберігаємо, де знаходиться поточний мінімум у масиві s[]s[]. Використовуючи цей мінімум, ми знаходимо поточний оптимальний індекс ll за O(1)O(1), а при переході від поточного індексу rr до наступного просто оновлюємо цей мінімум.

Очевидно, цей алгоритм працює за O(n)O(n) і є асимптотично оптимальним.

Реалізація

Щоб його реалізувати, нам навіть не потрібно явно зберігати масив часткових сум s[]s[] — нам знадобиться лише поточний елемент із нього.

Реалізація наведена для масивів з 0-індексацією, а не для 1-нумерації, як описано вище.

Спершу наведемо розв'язок, який знаходить просту числову відповідь, не знаходячи індексів шуканого відрізка:

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;
int 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;
}
}

Алгоритм 2

Тут ми розглянемо інший алгоритм. Його трохи важче зрозуміти, але він елегантніший за наведений вище, а його реалізація трохи коротша. Цей алгоритм запропонував Jay Kadane у 1984 році.

Опис алгоритму

Сам алгоритм такий. Пройдемося масивом і накопичуватимемо поточну часткову суму в деякій змінній ss. Якщо в якийсь момент ss стає від'ємним, ми просто присвоюємо s=0s=0. Стверджується, що максимум серед усіх значень, яких змінна ss набуває протягом роботи алгоритму, і буде відповіддю до задачі.

Доведення:

Розглянемо перший індекс, коли сума ss стає від'ємною. Це означає, що, починаючи з нульової часткової суми, ми зрештою отримуємо від'ємну часткову суму — отже, весь цей префікс масиву, як і будь-який його суфікс, має від'ємну суму. Тому цей підмасив ніколи не дає внеску в часткову суму жодного підмасиву, префіксом якого він є, і його можна просто відкинути.

Однак цього недостатньо, щоб довести коректність алгоритму. В алгоритмі ми фактично обмежені в пошуку відповіді лише такими відрізками, які починаються одразу після місць, де ставалося s<0s<0.

Але насправді розглянемо довільний відрізок [l,r][l, r], причому ll не знаходиться в такій «критичній» позиції (тобто l>p+1l > p+1, де pp — остання така позиція, в якій s<0s<0). Оскільки остання критична позиція знаходиться строго раніше за l1l-1, виходить, що сума a[p+1l1]a[p+1 \ldots l-1] невід'ємна. Це означає, що, перемістивши ll у позицію p+1p+1, ми збільшимо відповідь або, у крайньому випадку, не змінимо її.

Так чи інакше, виходить, що при пошуку відповіді можна обмежитися лише відрізками, які починаються одразу після позицій, у яких з'явилося s<0s<0. Це доводить коректність алгоритму.

Реалізація

Як і в алгоритмі 1, спершу наведемо спрощену реалізацію, яка шукає лише числову відповідь без знаходження меж шуканого відрізка:

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;
int 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;
}
}

Пошук максимального/мінімального підмасиву з обмеженнями

Якщо умова задачі накладає додаткові обмеження на шуканий відрізок [l,r][l, r] (наприклад, що довжина rl+1r-l+1 відрізка має бути в заданих межах), то описаний алгоритм, найімовірніше, легко узагальнюється на ці випадки — так чи інакше, задача все одно зводитиметься до пошуку мінімуму в масиві s[]s[] із заданими додатковими обмеженнями.

Двовимірний випадок задачі: пошук максимальної/мінімальної підматриці

Описана в цій статті задача природно узагальнюється на більші розмірності. Наприклад, у двовимірному випадку вона перетворюється на пошук такої підматриці [l1r1,l2r2][l_1 \ldots r_1, l_2 \ldots r_2] заданої матриці, яка має максимальну суму чисел у ній.

Використовуючи розв'язок для одновимірного випадку, легко отримати розв'язок за O(n3)O(n^3) для двовимірного випадку: ми перебираємо всі можливі значення l1l_1 і r1r_1 та обчислюємо суми від l1l_1 до r1r_1 у кожному рядку матриці. Тепер у нас є одновимірна задача пошуку індексів l2l_2 і r2r_2 у цьому масиві, яку вже можна розв'язати за лінійний час.

Швидші алгоритми для розв'язання цієї задачі відомі, але вони не набагато швидші за O(n3)O(n^3) і дуже складні (настільки складні, що багато з них поступаються тривіальному алгоритму для всіх розумних обмежень за рахунок прихованої константи). Наразі найкращий відомий алгоритм працює за час O(n3log3lognlog2n)O\left(n^3 \frac{ \log^3 \log n }{ \log^2 n} \right) (T. Chan 2007 "More algorithms for all-pairs shortest paths in weighted graphs")

Цей алгоритм Chan, як і багато інших результатів у цій галузі, фактично описує швидке множення матриць (де під множенням матриць мається на увазі видозмінене множення: замість додавання використовується мінімум, а замість множення — додавання). Задачу пошуку підматриці з найбільшою сумою можна звести до задачі пошуку найкоротших шляхів між усіма парами вершин, а ця задача, своєю чергою, зводиться до такого множення матриць.

Пошук підмасиву з максимальним/мінімальним середнім

Ця задача полягає в пошуку такого відрізка a[l,r]a[l, r], щоб середнє значення було максимальним:

maxlr1rl+1i=lra[i].\max_{l \le r} \frac{ 1 }{ r-l+1 } \sum_{i=l}^{r} a[i].

Звісно, якщо на шуканий відрізок [l,r][l, r] не накладено жодних інших умов, то розв'язком завжди буде відрізок довжини 11 на максимальному елементі масиву. Задача має сенс лише за наявності додаткових обмежень (наприклад, якщо довжина шуканого відрізка обмежена знизу).

У цьому випадку ми застосовуємо стандартний прийом при роботі із задачами на середнє значення: ми шукатимемо шукане максимальне середнє значення за допомогою бінарного пошуку.

Для цього нам потрібно навчитися розв'язувати таку підзадачу: дано число xx, і нам потрібно перевірити, чи існує підмасив масиву a[]a[] (звісно, який задовольняє всі додаткові обмеження задачі), де середнє значення більше за xx.

Щоб розв'язати цю підзадачу, віднімемо xx від кожного елемента масиву a[]a[]. Тоді наша підзадача фактично перетворюється на таку: чи є в цьому масиві підмасиви з додатною сумою. А цю задачу ми вже вміємо розв'язувати.

Таким чином, ми отримали розв'язок з асимптотикою O(T(n)logW)O(T(n) \log W), де WW — потрібна точність, а T(n)T(n) — час розв'язання підзадачі для масиву довжини nn (який може змінюватися залежно від конкретних додаткових обмежень).

Розв'язання онлайн-задачі

Умова задачі така: дано масив із nn чисел і число LL. Надходять запити вигляду (l,r)(l,r), і у відповідь на кожен запит потрібно знайти підмасив відрізка [l,r][l, r] довжиною не менше за LL з максимально можливим середнім арифметичним.

Алгоритм розв'язання цієї задачі досить складний; його запропонував KADR (Ярослав Твердохліб).