Пошук підмасиву з максимальною/мінімальною сумою
Тут ми розглядаємо задачу пошуку підмасиву з максимальною сумою, а також деякі її варіації (зокрема алгоритм для онлайн-розв'язання цієї задачі).
Постановка задачі
Дано масив чисел . Потрібно знайти підмасив з максимальною сумою:
Наприклад, якщо всі цілі числа в масиві невід'ємні, то відповіддю буде сам масив. Однак розв'язок стає нетривіальним, коли масив може містити як додатні, так і від'ємні числа.
Зрозуміло, що задача пошуку мінімального підмасиву по суті така сама — потрібно лише змінити знаки всіх чисел.
- Чи шукаємо неперервний підмасив (відрізок), а не довільну підмножину елементів?
- Чи цільова функція — сума елементів (можливо, з додатними та від'ємними значеннями)?
- Чи потрібно вкластися в один прохід замість перебору всіх відрізків? (для пошуку максимального середнього з обмеженням на довжину — поєднай із бінарним пошуком)
Алгоритм 1
Тут ми розглянемо майже очевидний алгоритм. (Далі ми розглянемо інший алгоритм, який трохи важче придумати, але його реалізація ще коротша.)
Опис алгоритму
Алгоритм дуже простий.
Введемо для зручності позначення: . Тобто масив — це масив часткових сум масиву . Також покладемо .
Тепер ітеруватимемо по індексу і навчимося швидко знаходити оптимальне для кожного поточного значення , при якому досягається максимальна сума на підмасиві .
Формально це означає, що для поточного нам потрібно знайти таке (не більше за ), щоб значення було максимальним. Після тривіального перетворення можна побачити, що нам потрібно знайти в масиві мінімум на відрізку .
Звідси одразу отримуємо розв'язок: ми просто зберігаємо, де знаходиться поточний мінімум у масиві . Використовуючи цей мінімум, ми знаходимо поточний оптимальний індекс за , а при переході від поточного індексу до наступного просто оновлюємо цей мінімум.
Очевидно, цей алгоритм працює за і є асимптотично оптимальним.
Реалізація
Щоб його реалізувати, нам навіть не потрібно явно зберігати масив часткових сум — нам знадобиться лише поточний елемент із нього.
Реалізація наведена для масивів з 0-індексацією, а не для 1-нумерації, як описано вище.
Спершу наведемо розв'язок, який знаходить просту числову відповідь, не знаходячи індексів шуканого відрізка:
- C++
- Python
- TypeScript
- Go
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);
}
ans, total, min_sum = a[0], 0, 0
for r in range(n):
total += a[r]
# найкращий відрізок із кінцем у r: поточна сума мінус мінімальний префікс
ans = max(ans, total - min_sum)
min_sum = min(min_sum, total)
let ans = a[0], sum = 0, minSum = 0;
for (let r = 0; r < n; ++r) {
sum += a[r];
// найкращий відрізок із кінцем у r: поточна сума мінус мінімальний префікс
ans = Math.max(ans, sum - minSum);
minSum = Math.min(minSum, sum);
}
ans, sum, minSum := a[0], 0, 0
for r := 0; r < n; r++ {
sum += a[r]
// найкращий відрізок із кінцем у r: поточна сума мінус мінімальний префікс
ans = max(ans, sum-minSum)
minSum = min(minSum, sum)
}
Тепер наведемо повну версію розв'язку, яка додатково знаходить також межі шуканого відрізка:
- C++
- Python
- TypeScript
- Go
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;
}
}
ans, ans_l, ans_r = a[0], 0, 0
total, min_sum, min_pos = 0, 0, -1
for r in range(n):
total += a[r]
cur = total - min_sum
if cur > ans:
ans = cur
ans_l = min_pos + 1 # відрізок починається одразу після мінімуму
ans_r = r
if total < min_sum:
min_sum = total
min_pos = r
let ans = a[0], ansL = 0, ansR = 0;
let sum = 0, minSum = 0, minPos = -1;
for (let r = 0; r < n; ++r) {
sum += a[r];
const cur = sum - minSum;
if (cur > ans) {
ans = cur;
ansL = minPos + 1; // відрізок починається одразу після мінімуму
ansR = r;
}
if (sum < minSum) {
minSum = sum;
minPos = r;
}
}
ans, ansL, ansR := a[0], 0, 0
sum, minSum, minPos := 0, 0, -1
for r := 0; r < n; r++ {
sum += a[r]
cur := sum - minSum
if cur > ans {
ans = cur
ansL = minPos + 1 // відрізок починається одразу після мінімуму
ansR = r
}
if sum < minSum {
minSum = sum
minPos = r
}
}
Алгоритм 2
Тут ми розглянемо інший алгоритм. Його трохи важче зрозуміти, але він елегантніший за наведений вище, а його реалізація трохи коротша. Цей алгоритм запропонував Jay Kadane у 1984 році.
Опис алгоритму
Сам алгоритм такий. Пройдемося масивом і накопичуватимемо поточну часткову суму в деякій змінній . Якщо в якийсь момент стає від'ємним, ми просто присвоюємо . Стверджується, що максимум серед усіх значень, яких змінна набуває протягом роботи алгоритму, і буде відповіддю до задачі.
Доведення:
Розглянемо перший індекс, коли сума стає від'ємною. Це означає, що, починаючи з нульової часткової суми, ми зрештою отримуємо від'ємну часткову суму — отже, весь цей префікс масиву, як і будь-який його суфікс, має від'ємну суму. Тому цей підмасив ніколи не дає внеску в часткову суму жодного підмасиву, префіксом якого він є, і його можна просто відкинути.
Однак цього недостатньо, щоб довести коректність алгоритму. В алгоритмі ми фактично обмежені в пошуку відповіді лише такими відрізками, які починаються одразу після місць, де ставалося .
Але насправді розглянемо довільний відрізок , причому не знаходиться в такій «критичній» позиції (тобто , де — остання така позиція, в якій ). Оскільки остання критична позиція знаходиться строго раніше за , виходить, що сума невід'ємна. Це означає, що, перемістивши у позицію , ми збільшимо відповідь або, у крайньому випадку, не змінимо її.
Так чи інакше, виходить, що при пошуку відповіді можна обмежитися лише відрізками, які починаються одразу після позицій, у яких з'явилося . Це доводить коректність алгоритму.
Реалізація
Як і в алгоритмі 1, спершу наведемо спрощену реалізацію, яка шукає лише числову відповідь без знаходження меж шуканого відрізка:
- C++
- Python
- TypeScript
- Go
int ans = a[0], sum = 0;
for (int r = 0; r < n; ++r) {
sum += a[r];
ans = max(ans, sum);
sum = max(sum, 0);
}
ans, total = a[0], 0
for r in range(n):
total += a[r]
ans = max(ans, total)
total = max(total, 0) # якщо сума стала від'ємною — починаємо заново
let ans = a[0], sum = 0;
for (let r = 0; r < n; ++r) {
sum += a[r];
ans = Math.max(ans, sum);
sum = Math.max(sum, 0); // якщо сума стала від'ємною — починаємо заново
}
ans, sum := a[0], 0
for r := 0; r < n; r++ {
sum += a[r]
ans = max(ans, sum)
sum = max(sum, 0) // якщо сума стала від'ємною — починаємо заново
}
Повний розв'язок, який підтримує індекси меж відповідного відрізка:
- C++
- Python
- TypeScript
- Go
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;
}
}
ans, ans_l, ans_r = a[0], 0, 0
total, minus_pos = 0, -1
for r in range(n):
total += a[r]
if total > ans:
ans = total
ans_l = minus_pos + 1 # відрізок починається після останнього "обнулення"
ans_r = r
if total < 0:
total = 0
minus_pos = r
let ans = a[0], ansL = 0, ansR = 0;
let sum = 0, minusPos = -1;
for (let r = 0; r < n; ++r) {
sum += a[r];
if (sum > ans) {
ans = sum;
ansL = minusPos + 1; // відрізок починається після останнього "обнулення"
ansR = r;
}
if (sum < 0) {
sum = 0;
minusPos = r;
}
}
ans, ansL, ansR := a[0], 0, 0
sum, minusPos := 0, -1
for r := 0; r < n; r++ {
sum += a[r]
if sum > ans {
ans = sum
ansL = minusPos + 1 // відрізок починається після останнього "обнулення"
ansR = r
}
if sum < 0 {
sum = 0
minusPos = r
}
}
Пов'язані задачі
Пошук максимального/мінімального підмасиву з обмеженнями
Якщо умова задачі накладає додаткові обмеження на шуканий відрізок (наприклад, що довжина відрізка має бути в заданих межах), то описаний алгоритм, найімовірніше, легко узагальнюється на ці випадки — так чи інакше, задача все одно зводитиметься до пошуку мінімуму в масиві із заданими додатковими обмеженнями.
Двовимірний випадок задачі: пошук максимальної/мінімальної підматриці
Описана в цій статті задача природно узагальнюється на більші розмірності. Наприклад, у двовимірному випадку вона перетворюється на пошук такої підматриці заданої матриці, яка має максимальну суму чисел у ній.
Використовуючи розв'язок для одновимірного випадку, легко отримати розв'язок за для двовимірного випадку: ми перебираємо всі можливі значення і та обчислюємо суми від до у кожному рядку матриці. Тепер у нас є одновимірна задача пошуку індексів і у цьому масиві, яку вже можна розв'язати за лінійний час.
Швидші алгоритми для розв'язання цієї задачі відомі, але вони не набагато швидші за і дуже складні (настільки складні, що багато з них поступаються тривіальному алгоритму для всіх розумних обмежень за рахунок прихованої константи). Наразі найкращий відомий алгоритм працює за час (T. Chan 2007 "More algorithms for all-pairs shortest paths in weighted graphs")
Цей алгоритм Chan, як і багато інших результатів у цій галузі, фактично описує швидке множення матриць (де під множенням матриць мається на увазі видозмінене множення: замість додавання використовується мінімум, а замість множення — додавання). Задачу пошуку підматриці з найбільшою сумою можна звести до задачі пошуку найкоротших шляхів між усіма парами вершин, а ця задача, своєю чергою, зводиться до такого множення матриць.
Пошук підмасиву з максимальним/мінімальним середнім
Ця задача полягає в пошуку такого відрізка , щоб середнє значення було максимальним:
Звісно, якщо на шуканий відрізок не накладено жодних інших умов, то розв'язком завжди буде відрізок довжини на максимальному елементі масиву. Задача має сенс лише за наявності додаткових обмежень (наприклад, якщо довжина шуканого відрізка обмежена знизу).
У цьому випадку ми застосовуємо стандартний прийом при роботі із задачами на середнє значення: ми шукатимемо шукане максимальне середнє значення за допомогою бінарного пошуку.
Для цього нам потрібно навчитися розв'язувати таку підзадачу: дано число , і нам потрібно перевірити, чи існує підмасив масиву (звісно, який задовольняє всі додаткові обмеження задачі), де середнє значення більше за .
Щоб розв'язати цю підзадачу, віднімемо від кожного елемента масиву . Тоді наша підзадача фактично перетворюється на таку: чи є в цьому масиві підмасиви з додатною сумою. А цю задачу ми вже вміємо розв'язувати.
Таким чином, ми отримали розв'язок з асимптотикою , де — потрібна точність, а — час розв'язання підзадачі для масиву довжини (який може змінюватися залежно від конкретних додаткових обмежень).
Розв'язання онлайн-задачі
Умова задачі така: дано масив із чисел і число . Надходять запити вигляду , і у відповідь на кожен запит потрібно знайти підмасив відрізка довжиною не менше за з максимально можливим середнім арифметичним.
Алгоритм розв'язання цієї задачі досить складний; його запропонував KADR (Ярослав Твердохліб).