Жадные алгоритмы - Алгоритмика
Жадные алгоритмы

Жадные алгоритмы

автор Андрей Гаркавый
редактор Сергей Слотин
опубликовано 2018
обновлено сент. 14, 2021

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

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

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

#Монетки

Задача. Монетная система некоторого государства состоит из монет достоинством a1=1<a2<a3<<ana_1 = 1 < a_2 < a_3 < \ldots < a_n, причём каждый следующий номинал делится на предыдущий. Требуется выдать сумму SS наименьшим возможным количеством монет.

Жадный алгоритм решения этой задачи заключается в том, что нужно сначала взять наибольшее количество монет достоинства ana_n (а именно, San\lfloor \frac{S}{a_n} \rfloor), затем для набора остатка взять наибольшее количество монет достоинства an1a_{n-1} и так далее. Так как a1=1a_1 = 1, в конце мы всегда наберем нужную сумму.

a = [1, 2, 5, 10, 50, 100, 500, 1000, 2000, 5000]

def solution(s):
    coins = []
    for x in reversed(a):
        coins += [x] * (s // x)
        s %= x
    return coins

Заметим, что без предположения, что номиналы монет делятся друг на друга, алгоритм не всегда выводит оптимальное решение, и в этом случае задачу остается решать динамическим программированием. Например, сумму в 24 рубля монетами в 1, 5 и 7 рублей жадный алгоритм разменивает как (7×3+1×3)(7 \times 3 + 1 \times 3), в то время как оптимальным будет (7×2+5×2)(7 \times 2 + 5 \times 2).

Корректность. Докажем корректность жадного алгоритма от противного: пусть максимальная монета имеет достоинство an<Sa_n < S, но в оптимальном ответе её нет.

Пусть в оптимальном ответе монета с номиналом aia_i встретилась bib_i раз:

S=aibi S = \sum a_i \cdot b_i

Если biai+1aib_i \geq \frac{a_{i+1}}{a_i}, то ai+1ai\frac{a_{i+1}}{a_i} монет можно заменить на одну монету достоинства ai+1a_{i+1}, а значит это не оптимальный ответ. Следовательно, biai+1ai1b_i \leq \frac{a_{i+1}}{a_i} - 1.

Теперь посчитаем, какой может быть максимальная сумма всех достоинств всех монет в оптимальном ответе, если мы не брали максимальные монеты:

S=a1b1+a2b2++an1bn1a1(a2a11)+a2(a3a21)++an1(anan11)=(a2a1)+(a3a2)++(anan1)=ana1<an \begin{aligned} S &= a_1 \cdot b_1 + a_2 \cdot b_2 + \ldots + a_{n-1} \cdot b_{n-1} \\ &\leq a_1 \cdot (\frac{a_2}{a_1} - 1) + a_2 \cdot (\frac{a_3}{a_2} - 1) + \ldots + a_{n-1} \cdot (\frac{a_n}{a_{n-1}} - 1) \\ &= (a_2 - a_1) + (a_3 - a_2) + \ldots + (a_n - a_{n-1}) \\ &= a_n - a_1 < a_n \end{aligned}

Отсюда делаем вывод, что если SanS \geq a_n, то в оптимальный ответ всегда придется взять максимальную монету размера aka_k, потому что все меньшие монеты просто не смогут оптимальном ответе давать так много. Аналогичным размышлением про наборы из меньших монет получаем такой же результат для всех S<anS < a_n, откуда следует корректность жадного алгоритма.

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

Упражнение. Верно ли, что алгоритм работает корректно для российской монетной системы (1, 2, 5…)?

#Рюкзак с делимыми предметами

Задача. Пусть есть рюкзак с вместимостью не более, чем WW грамм и nn предметов весом wiw_i грамм и стоимостью cic_i за грамм. Мы умеем отрезать от любого предмета целое количество грамм. Требуется набрать рюкзак максимальной стоимости.

Отсортируем предметы по убыванию «плотности ценности» ciwi\frac{c_i}{w_i} и будем брать их жадно в таком порядке. От последнего предмета, который не влезет полностью, возьмем часть.

Корректность. Мысленно разделим все предметы на wiw_i кусочков по 1 грамм, и при этом их ценность стала равна ciwi\frac{c_i}{w_i}. Понятно, что из кусочков одинакового веса 1 грамм всегда оптимально просто взять кусочки с максимальной ценностью, что мы фактически и делаем в нашем алгоритме.

Итоговая асимптотика O(nlogn)O(n \log n) на сортировку.

#Выбор заявок

Задача. Даны заявки на проведение занятий в некоторой аудитории. В каждой заявке указаны начало lil_i и конец rir_i занятия. Нужно из всех заявок оставить как можно больше таким образом, чтобы они не пересекались.

Здесь жадность становится не такой уже очевидной, потому что неясно, в каком порядке рассматривать заявки и как их «жадно» набирать.

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

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

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

struct activity {
    int l, r;
};

int solve(vector<activity> activities) {
    sort(activities.begin(), activities.end(), [](activity a, activity b) {
        return a.r < b.r;
    });
    int cnt = 0, last = 0;
    for (activity a : activities)
        if (a.l >= last)
            last = a.r, cnt++;
    return cnt;
}

Асимптотика O(nlogn)O(n \log n) на сортировку.