Добрый день. На первом курсе бакалавриата Академического университета читается годовой курс алгоритмов. Каждая лекция сопровождается семинаром, на котором мы разбираем алгоритмические задачи. Практические семинары проходят в небольших группах. В этом семестре я читаю лекции и веду практику у одной из групп.
Сегодня хочу поделиться с Вами двумя задачами с этих семинаров.
Задача 1. На прямой даны n отрезков, нужно выбрать максимальное по размеру подмножество непересекающихся.
Задача 2. На окружности даны n дуг (отрезков), нужно выбрать максимальное по размеру подмножество непересекающихся.
Первая задача — один из примеров на тему «жадные алгоритмы». Решение можно посмотреть внизу. Если коротко, предполагается решение сортировкой за O(sort+n). Здесь за sort я обозначаю время на сортировку массива длины n. Кстати, sort — необязательно nlogn (например, bucket sort, radix sort, входные данные из ограниченного диапазона).
Вторая задача имеет красивое вероятностное решение за O(sort+n). Не смотря на свою похожесть на первую, вторая задача в лоб жадностью не решается. Вернее лично мне неизвестно жадное решение, если кто-нибудь из читателей придумает и поделится, буду рад послушать. А чтобы проще было думать, я сразу опишу несколько неработающих идей, наиболее часто всплывающих при попытках решать эту задачу:
Решение задачи 1.
У каждого отрезка i есть левый конец L[i] и правый конец R[i]. Отсортируем отрезки в порядке возрастания R[i]. В таком порядке будем перебирать отрезки. Очередной отрезок будем добавлять в ответ, если его левая граница больше максимума правых границ уже добавленных отрезков.
Решение задачи 2.
Предупреждаю, решение длинное и гораздо сложнее предыдущего. С другой стороны принципиально сложных идей тут нет, так что всё можно понять без начальной подготовки.
Для начала скажем, как задаются дуги на окружности. Пусть окружность — зацикленный отрезок [0..M), а дуги (отрезки) окружности задаются парами L[i]..R[i]. Если R[i] < L[i], отрезок проходит через точку M.
Основная идея решения — найти точку, в которой можно разрезать окружность. Вернее так: выбрать отрезок i, который мы точно возьмём в ответ. Как только такой отрезок i зафиксирован, окружность можно разрезать, например в точке R[i].
Представляя все объекты на окружности, решать задачу не очень удобно. Поэтому, используя приём «удвоение окружности», перейдём на прямую.
1. Если у какого-то отрезка R[i] < L[i], то сделаем R[i] += M.
2. Для любого отрезка L[i]..R[i] породим парный ему L[i]+M..R[i]+M
Теперь у нас есть 2n отрезков на прямой, а окружности с разрезом в точке x соответствует отрезок этой прямой [x..x+M), где 0 <= x < M.
Решение задачи 2 за O(sort + n2). Отсортировать один раз отрезки по правому концу, затем перебрать n возможных точек разреза R[i], решая для каждой задачу за O(n), так же как Задачу 1.
Решение задачи 2 за O(sort + nk), где k — размер множества-ответа на задачу. Оставим сортировку.
А сразу после сортировки воспользуемся методом двух указателей и за O(n) для каждого отрезка i насчитаем следующий отрезок next[i], который мы бы взяли нашей жадностью (жадность: L[next[i]] > R[i], из таких next[i] = min).
Теперь сделаем интересное наблюдение: в зависимости от точки разреза размер полученного нами множества отличается от оптимального не более чем на 1. То есть, если мы разрезали в первой попавшейся точке и получили множество размера k, нам достаточно найти множество размера k-1, и оно точно будет оптимальным.
Решение: перебираем первый отрезок, делаем k-2 шагов вперёд с помощью ссылок next, если расстояние между самой правой и самой левой точкой меньше M, мы нашли k-1 непересекающихся дуг.
Решение за O(sort + n)
Возьмём раз случайную точку i = random [0..n-1]. Для каждого i за O(k) попробуем a = next[i], как в коде выше.
Поскольку, если ответ из k-1 отрезков существует, нам достаточно было попасть в какой-то один из этих k-1, то вероятность того, что за попыток мы ни разу не попали равна при стремящимся к бесконечности. Заметим, что если = Θ(1), то предыдущий алгоритм за O(nk) нас полностью устраивал. Т.е. мы получили вероятностный алгоритм, время работы которого O(sort + n). Чтобы достигнуть лучшей ошибки e-T, достаточно попробовать не , а точек, время работы соответственно будет O(sort + Tn).
Не всегда на определённую тему достаточно уже готовых задач, регулярно приходится придумывать новые. Только что мы наблюдали забавный эффект: берём классическую простую задачу на сортировку, заменяем «прямую» на «окружность», получаем сложную задачу на метод двух указателей и вероятностную идею. Многие красивые учебные задачи так и придумываются. Кстати, попробуйте решить ещё две задачи:
Задача 3. Даны n отрезков на прямой, выбрать максимальное по размеру подмножество отрезков такое, что каждая точка покрыта не более чем k раз (мы её только что решали для k=1).
Задача 4. Даны n дуг (отрезков) на окружности… и та же самая задача.
Если задачи Вам понравились, по ссылкам осень и весна можно найти ещё порядка сотни задач за осенний и весенний семестр этого года. К некоторым задачам прилагается разбор.
Сегодня хочу поделиться с Вами двумя задачами с этих семинаров.
Задача 1. На прямой даны n отрезков, нужно выбрать максимальное по размеру подмножество непересекающихся.
Задача 2. На окружности даны n дуг (отрезков), нужно выбрать максимальное по размеру подмножество непересекающихся.
Первая задача — один из примеров на тему «жадные алгоритмы». Решение можно посмотреть внизу. Если коротко, предполагается решение сортировкой за O(sort+n). Здесь за sort я обозначаю время на сортировку массива длины n. Кстати, sort — необязательно nlogn (например, bucket sort, radix sort, входные данные из ограниченного диапазона).
Вторая задача имеет красивое вероятностное решение за O(sort+n). Не смотря на свою похожесть на первую, вторая задача в лоб жадностью не решается. Вернее лично мне неизвестно жадное решение, если кто-нибудь из читателей придумает и поделится, буду рад послушать. А чтобы проще было думать, я сразу опишу несколько неработающих идей, наиболее часто всплывающих при попытках решать эту задачу:
- Пусть на окружности есть точка, не покрытая ни одним отрезком, тогда окружность можно разрезать в этой точке и получить задачу про прямую, которую мы уже умеем решать!
Да. Но таких точек может не быть. Более того, каждая точка окружности может быть покрыта 2, 10, или даже Θ(n) отрезками - Разрезать окружность по точке, покрытой минимальным количеством отрезков!
Не работает. Представьте себе 3 разбиения окружности на 10 отрезков. Всего 30 отрезков. В двух из этих «разбиений» отрезки слегка накладываются друг на друга. Нам нужно угадать третье из разбиений и точкой разреза выбрать конец одного из именно этих 10 отрезков. - Выгодно взять в ответ самый короткий отрезок!
Нет. Даже на прямой уже не правда: (0,49) (50,100) (48,51) - Выгодно выкинуть из множества самый длинный отрезок, который кого-нибудь пересекает!
Нет. Смотрите предыдущий пример. - А ещё...
Есть много неработающих идей, остановим перечисление на уже озвученных.
Решения задач
Решение задачи 1.
У каждого отрезка i есть левый конец L[i] и правый конец R[i]. Отсортируем отрезки в порядке возрастания R[i]. В таком порядке будем перебирать отрезки. Очередной отрезок будем добавлять в ответ, если его левая граница больше максимума правых границ уже добавленных отрезков.
Решение задачи 2.
Предупреждаю, решение длинное и гораздо сложнее предыдущего. С другой стороны принципиально сложных идей тут нет, так что всё можно понять без начальной подготовки.
Для начала скажем, как задаются дуги на окружности. Пусть окружность — зацикленный отрезок [0..M), а дуги (отрезки) окружности задаются парами L[i]..R[i]. Если R[i] < L[i], отрезок проходит через точку M.
Основная идея решения — найти точку, в которой можно разрезать окружность. Вернее так: выбрать отрезок i, который мы точно возьмём в ответ. Как только такой отрезок i зафиксирован, окружность можно разрезать, например в точке R[i].
Представляя все объекты на окружности, решать задачу не очень удобно. Поэтому, используя приём «удвоение окружности», перейдём на прямую.
1. Если у какого-то отрезка R[i] < L[i], то сделаем R[i] += M.
2. Для любого отрезка L[i]..R[i] породим парный ему L[i]+M..R[i]+M
Теперь у нас есть 2n отрезков на прямой, а окружности с разрезом в точке x соответствует отрезок этой прямой [x..x+M), где 0 <= x < M.
Решение задачи 2 за O(sort + n2). Отсортировать один раз отрезки по правому концу, затем перебрать n возможных точек разреза R[i], решая для каждой задачу за O(n), так же как Задачу 1.
Решение задачи 2 за O(sort + nk), где k — размер множества-ответа на задачу. Оставим сортировку.
А сразу после сортировки воспользуемся методом двух указателей и за O(n) для каждого отрезка i насчитаем следующий отрезок next[i], который мы бы взяли нашей жадностью (жадность: L[next[i]] > R[i], из таких next[i] = min).
// Код метода двух указателей
b = 0;
for (a = 0; a < n; a++) {
while (L[b] <= R[a]) b++; // за всё время b увеличится не более 2n раз
next[a] = b;
}
Теперь сделаем интересное наблюдение: в зависимости от точки разреза размер полученного нами множества отличается от оптимального не более чем на 1. То есть, если мы разрезали в первой попавшейся точке и получили множество размера k, нам достаточно найти множество размера k-1, и оно точно будет оптимальным.
Решение: перебираем первый отрезок, делаем k-2 шагов вперёд с помощью ссылок next, если расстояние между самой правой и самой левой точкой меньше M, мы нашли k-1 непересекающихся дуг.
for (a = 0; a < n; a++) {
b = a;
for (i = 0; i < k - 2; i++) b = next[b];
if (R[b] - L[a] < M) ; // Успех, нашли k-1 отрезков! Это a, next[a], next[next[a]] ... b
}
Решение за O(sort + n)
Возьмём раз случайную точку i = random [0..n-1]. Для каждого i за O(k) попробуем a = next[i], как в коде выше.
Поскольку, если ответ из k-1 отрезков существует, нам достаточно было попасть в какой-то один из этих k-1, то вероятность того, что за попыток мы ни разу не попали равна при стремящимся к бесконечности. Заметим, что если = Θ(1), то предыдущий алгоритм за O(nk) нас полностью устраивал. Т.е. мы получили вероятностный алгоритм, время работы которого O(sort + n). Чтобы достигнуть лучшей ошибки e-T, достаточно попробовать не , а точек, время работы соответственно будет O(sort + Tn).
Эпилог
Не всегда на определённую тему достаточно уже готовых задач, регулярно приходится придумывать новые. Только что мы наблюдали забавный эффект: берём классическую простую задачу на сортировку, заменяем «прямую» на «окружность», получаем сложную задачу на метод двух указателей и вероятностную идею. Многие красивые учебные задачи так и придумываются. Кстати, попробуйте решить ещё две задачи:
Задача 3. Даны n отрезков на прямой, выбрать максимальное по размеру подмножество отрезков такое, что каждая точка покрыта не более чем k раз (мы её только что решали для k=1).
Задача 4. Даны n дуг (отрезков) на окружности… и та же самая задача.
Ещё больше задач
Если задачи Вам понравились, по ссылкам осень и весна можно найти ещё порядка сотни задач за осенний и весенний семестр этого года. К некоторым задачам прилагается разбор.