Загрузил rodzher.zet

Практика по алгоритмам Кучи и перебор

Реклама
Первый курс, осенний семестр 2016/17
Практика по алгоритмам #8
Кучи и перебор
7 ноября
Собрано 12 ноября 2019 г. в 20:26
Содержание
1. Кучи и перебор
1
2. Разбор задач практики
3
3. Домашнее задание
3.1. Обязательная часть . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2. Дополнительная часть . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
6
7
Алгоритмы, осень 2019/20
Практика #8. Кучи и перебор.
Кучи и перебор
1. Задачи на перебор
a) Дана строка длины 𝑛 6 20.
Посчитайте число
(︀𝑛)︀ различных подпоследовательностей длины ровно 𝑘.
b) 1. Посчитайте 𝑘 , используя только сложения и вычитания. Воспользуйтесь рекурсией.
2. Добейтесь того, чтобы время работы было 𝒪(𝑛2 ).
c) Выведите все строки, состоящие из букв 𝑎 и 𝑏 длины ровно 𝑛, где нет подстроки 𝑎𝑏𝑎𝑏.
d) Выведите все возрастающие массивы длины 𝑘, состоящие из целых чисел от 1 до 𝐶.
e) Выведите все массивы длины 𝑛 из чисел 1, 2, 3, в которых двоек не меньше чем единиц.
2. Статистика в бинарной куче
Дана бинарная min-куча. Найти 𝑘-ую статистику за:
a) 𝒪(𝑘 log 𝑛)
b) 𝒪(𝑘 2 )
c) 𝒪(𝑘 log 𝑘)
3. Оптимизации куч
a) Есть куча, которая умеет Add, Merge, ExtractMin за 𝒪(log 𝑛), Build за 𝒪(𝑛).
На ее основе построить структуру данных, которая умеет все то же, но Add за 𝒪(1).
b) Есть куча, которая умеет Add за 𝒪(1), Merge, ExtractMin за 𝒪(log 𝑛), Build за 𝒪(𝑛).
На ее основе построить структуру данных, которая умеет все то же, но Merge за 𝒪(1).
4. Куча Фибоначчи
a) Что будет, если в куче Фибоначчи делать DecreaseKey просто через SiftUp?
b) Получить 𝒪(𝑛) операциями над кучей бамбук из 𝑛 вершин.
c) Оценить сложность худшего случая операций Add, Merge, ExtractMin, DecreaseKey в куче
Фибоначчи.
d) Что будет, если разрешить отрезать сколько угодно детей?
e) Что будет, если разрешить отрезать двух детей?
f) (*) Придумайте, как сделать так, чтобы ExtractMin работал за амортизированное
𝒪(log 𝐶), где 𝐶 – число различных значений в куче.
g) (*) Разрешим удалять только самого большого ребенка.
Придумайте, как теперь делать DecreaseKey за амортизированное 𝒪(1).
Докажите, что теперь в худшем случае он работает за 𝒪(log 𝑛).
1/7
Алгоритмы, осень 2019/20
Практика #8. Кучи и перебор.
5. (*) Статистика в массиве
Найти 𝑘-ую статистику за 𝑛 + 𝒪(𝑘 log 𝑛) сравнений.
6. (*) Счетчик Кнута
Избыточный двоичный счетчик – счетчик, где в каждом разряде может стоять 𝑑𝑖 ∈ {0, 1, 2}.
Значение числа – сумма 𝑑𝑖 2𝑖 . Научитесь за 𝒪(1) делать операции.
∙ inc – прибавить к числу 1.
∙ get(i) – посмотреть i-й разряд счетчика.
А теперь сделайте так, чтобы добавление в биномиальную кучу работало за 𝒪(1) в худшем.
(**) Добавьте в структуру ещё одну операцию:
∙ inc(i) – прибавить к числу 2𝑖 .
7. (*) Перебор
Даны три массива 𝑎, 𝑏, 𝑐. Длины до 106 . Рассмотрим все тройки (𝑎𝑖 , 𝑏𝑗 , 𝑐𝑘 ). Значение тройки –
сумма элементов. Просуммируйте значения 𝑘 6 106 минимальных по значению троек.
(**) А если массивов много, а 106 – суммарная длина?
2/7
Алгоритмы, осень 2019/20
Практика #8. Кучи и перебор.
Разбор задач практики
1. Задачи на перебор
a) Мы можем рекурсивно перебрать все подпоследовательности. Каждый символ или берём
в ответ, или нет. Чтобы проверять различность, добавим их все в set.
vector<int> a(n); // исходные данные
vector<int> res(k); // текущая подпоследовательность
set<vector<int>> s; // ответ на задачу - s.size()
int calc(int ni, int ki) {
if (ni == n) {
// это место можно улучшить, строить так, чтобы к концу было ровно k
if (ki == k) s.insert(res);
return;
}
calc(ni+1, ki); // пропускаем
if (ki < k)
res[ki] = a[i], calc(ni+1, ki+1); // берём
}
(︀ )︀ (︀ )︀ (︀𝑛−1)︀
b) 𝑛𝑘 = 𝑛−1
+ 𝑘 . Считаем рекурсивно.
𝑘−1
В рекурсии много раз вычисляем одно и то же ⇒ будем запоминать уже посчитанные
результаты и проверять “если уже посчитано, можно сразу вернуть результат”.
Для каждой пары 1 6 𝑖 6 𝑛, 1 6 𝑗 6 𝑘 считаем функции не более одного раза ⇒ 𝒪(𝑛𝑘).
int binom(int n, int k) {
if (n < k) return 0;
if (n == 1) return 1;
if (result[n][k] != -1) return result[n][k];
return result[n][k] = binom(n-1, k-1) + binom(n-1, k);
}
c) Рекурсивно перебираем строки, по ходу рекурсии сразу выходим, если суффикс = 𝑎𝑏𝑎𝑏.
Решение оптимально, так как работает за 𝒪(ответа). Быстрее вывести не получится.
d) Очень похоже на пункт (a), правда? Напишем ещё одну версию решения.
vector<int> res(k); // исходные данные
int calc(int i) {
if (i == k) { cout << res; return; }
for (res[i] = i ? res[i-1] + 1 : 0; res[i] + (k-i) < n; res[i]++)
calc(i+1);
}
e) Ещё одна рекурсия. Опять же важно написать за 𝒪(ответа).
Отсечение: if (cnt2+n-i < cnt1) return;
3/7
Алгоритмы, осень 2019/20
Практика #8. Кучи и перебор.
2. Статистика в бинарной куче
a) 𝒪(𝑘 log 𝑛). 𝑘 раз вынимаем минимум.
b) 𝒪(𝑘 2 ). Минимум в корне. Следующий в одном из его детей. Третий либо в детях меньшего
сына, либо в большем сыне.
Итого у нас на шаге 𝑖 есть 𝑖 кандидатов, выбираем минимального и делаем его детей
кандидатами. Кандидаты хранятся в списке, а минимум мы ищем линейным проходом.
c) 𝒪(𝑘 log 𝑘). То же, что в b, но храним кандидатов в (другой) куче вместо списка.
3. Оптимизации куч
a) Add за 𝒪(1). Добавляет новые элементы в отдельный список freshItems.
Если пришел ExtractMin или Merge, делаем Merge(q, Build(freshItems)).
Потенциал 𝜙 = |freshItems|.
b) Merge за 𝒪(1). У нас была структура Q, строим структуру B.
struct<T> B {
T min;
Q<B<T>> q;
};
Если держать кучу Q структур B, то Merge двух структур B – это Add в структуру Q за 𝒪(1).
ExtractMin: bmin = q.ExtractMin(), min = bmin.min, q = Merge(q, bmin.q);
Вышло 𝒪(log 𝑛).
4. Куча Фибоначчи
a) Если делать DecreaseKey просто через SiftUp, то есть не резать детей, то каждое дерево
будет обычным биномиальным. SiftUp за высоту биномиального дерева = 𝒪(log 𝑛).
b) Строим по индукции. База 𝑘=1.
Переход. Есть бамбук из 𝑘 вершин, его ранг по определению равен степени корня = 1.
Хотим подвесить его к другому дереву ранга 1, и отрезать от того лишнее:
root = root.Add(-1).Add(-2).Add(-3).ExtractMin() // добавили
root = root.DecreaseKey(node[-1],-4).ExtractMin() // отрезали лишнее
c) Add и Merge всегда 𝒪(1).
ExtractMin в худшем случае за Ω(𝑛): добавили 𝑛 элементов, потом ExtractMin.
DecreaseKey в худшем случае за Ω(𝑛). Сделали бамбук с отростками на каждом уровне.
Вызвали DecreaseKey от каждого отростка, теперь все вершины бамбука помечены. Теперь DecreaseKey от его нижней вершины.
d) Если отрезать сколько угодно детей, то DecreaseKey работает за честное 𝒪(1), ведь нам
не надо рекурсивно резать предков.
Но теперь дерево ранга 𝑘 может быть размера (𝑘 + 1): корень и 𝑘 детей.
Тогда max ранг может быть Ω(𝑛) вместо 𝒪(log 𝑛) ⇒ после ExctractMin останется не
𝒪(log 𝑛) корней, амортизированная оценка сломалась.
√
Но! Ранг равен числу детей ⇒ size(𝑟) > 𝑟 + 1 ⇒ может быть
√ не более 𝒪( 𝑛) разных
рангов. Итого амортизированное время ExctractMin равно 𝒪( 𝑛).
√
Можно построить пример, где будет достигаться Ω( 𝑛).
Есть одно дерево ранга 𝑘. Обрежем у него всех правнуков, ранги детей не изменились.
Режем всех детей. Теперь у нас деревья рангов 0, 1, . . . , 𝑘 − 1, в них вершин 1, 2, . . . , 𝑘.
Суммарно Θ(𝑘 2 ).
4/7
Алгоритмы, осень 2019/20
Практика #8. Кучи и перебор.
Если все время делать ExtractMin из самого маленького дерева, поиск нового будет все
время Ω(𝑘).
e) Если резать двух детей, то будут те же оценки, что и в обычной куче Фибоначчи.
Для этого надо показать, что размер дерева экспоненциальный от ранга.
Покажем size(𝑟) > 𝐶 𝑟−2 . База 𝑟 = 0, 1.
−2
𝑟−2 −1)
𝑟−4 −𝐶 −2
= 1 + 𝐶 𝐶−1
.
Переход: size(𝑟) > 1 + 𝐶 0−2 + 𝐶 1−2 + . . . + 𝐶 (𝑟−3)−2 = 1 + 𝐶 (𝐶
𝐶−1
√
𝐶 𝑟−4 −𝐶 −2
𝑟−2
Например, для 𝐶 = 2 верно 1 + 𝐶−1 > 𝐶
при 𝑟 > 1.
f) (*) ExtractMin за 𝒪(log 𝐶). Храним в куче пары ⟨𝑥, count(𝑥)⟩.
Также понадобится хеш-таблица, которая по значению дает узел с этим значением.
С Add всё просто. Merge не получится, и ладно.
DecreaseKey. Уменьшили 𝑥 → 𝑦. Уменьшим счетчик 𝑥, увеличим счетчик 𝑦. Если 𝑦 еще
не было, добавим.
Проблема: если счетчик 𝑥 занулился, то у нас есть лишний узел в куче, портит асимптотику. Чтобы ее решить, нужен хороший потенциал... TODO
g) (*) Здесь мы придумываем тонкую кучу (Thin Heap). Современный аналог Фибоначчи.
Если можно удалять только самого большого ребенка, то у вершины ранга 𝑘 должны
быть дети ровно рангов 0, 1, . . . , 𝑘−2, 𝑘−1. Если вершина помечена, то последнего сына
нет. Назовем такую вершину тонкой. Будем хранить детей 𝑣 в векторе 𝑐𝑣 в порядке
возрастания ранга. Делаем DecreaseKey(x). Пусть 𝑣 – отец 𝑥, а 𝑖[𝑥] – позиция 𝑚 в списке
детей 𝑣. Таки отрезаем 𝑥. В списке детей 𝑣 на позиции 𝑖[𝑚] образовалась дырень.
Возьмём старшего брата 𝑥 (𝑧=𝑐𝑣 [𝑖[𝑥]+1]) и подумаем, как бы закрыть дырень.
∙ Если 𝑧 толстый, отрезаем у 𝑧 самого старшего сына, суём в дырень.
𝑧 стал тонким, всем хорошо.
∙ Если 𝑧 уже тонкий, уменьшим его ранг на 1, он от этого потолстеет, а мы целиком
засунем 𝑧 в дырень. Проблема (дырень) сместилась вправо. Продолжим её рекурсивно
решать.
∙ Если @𝑧 (дошли до конца 𝑐𝑣 ), то 𝑣 прохудилась, от чего или стала тонкой, или её пора
рекурсивным вызовом отрезать.
При исправлениях движемся по дереву либо вправо, либо вверх. При этом растет исходный ранг обрабатываемой вершины ⇒ один DecreaseKey в худшем случае работает за
𝒪(log 𝑛). В среднем всё ещё за 𝒪(1).
5. (*) Счетчик Кнута
Инвариант: между любыми двумя двойками есть ноль.
Раскрытие младшей двойки создало ноль. Этот ноль разделяет следующую за ней двойку и
новую двойкой в 0 разряде, если та появилась.
Варианты (младшие разряды справа):
. . . 2 . . . 02 . . . → . . . 2 . . . 10 . . .,
. . . 2 . . . 0 . . . 12 . . . → . . . 2 . . . 0 . . . 20 . . ..
Как получить младшую двойку? Хранить список позиций с двойками.
5/7
Алгоритмы, осень 2019/20
Практика #8. Кучи и перебор.
Домашнее задание
3.1. Обязательная часть
1. (3) Числа Стирлинга
Если интересно, числа Стирлинга второго рода – количество способов разбиения множества
из 𝑛 элементов на 𝑘 непустых подмножеств. Сейчас нам важно, что считать их можно по
формуле:
⎧
⎪
⎨𝑘𝑆(𝑛−1, 𝑘) + 𝑆(𝑛−1, 𝑘−1) для 𝑛 > 0, 𝑘 > 0
𝑆(𝑛,𝑘) = 1
для 𝑛 = 𝑘 = 0
⎪
⎩
0
иначе
Ваша задача – написать функцию int S(int n, int k), которая считает число Стирлинга
для 𝑛, 𝑘 6 100, предполагая, что ответ помещается в int.
2. (2) Мы наконец научились решать тест!
Вспомним задачу из вступительного теста.
Сколько способов из клетки 0 дойти до клетки 𝑛, на каждом шаге прыгая вперёд на 1, 3
или 4 клетки? В тесте 𝑛 было 11. Обозначим такое количество за 𝑓𝑛 и заметим, что 𝑓0 = 1,
а ∀𝑛 > 0, 𝑓𝑛 = 𝑓𝑛−1 + 𝑓𝑛−3 + 𝑓𝑛−4 . Найти 𝑓30 . Ответ на задачу – число.
3. (2) Разминка перед перебором
Выведите за 𝒪(размера ответа) все последовательности из 𝑛 плюс-минус единиц,
суммы которых лежат в [𝐿, 𝑅].
4. (3) Перебор!
Предложите алгоритм, который выводит в произвольном порядке все разбиения множества
{1, 2, . . . , 𝑛} на подмножества за 𝒪(длины ответа).
6/7
Алгоритмы, осень 2019/20
Практика #8. Кучи и перебор.
3.2. Дополнительная часть
1. (3) Суммы всего
Дано множество из 𝑛 6 105 целых чисел и число 𝑆. Найдите за 𝒪(ответа) количество подмножеств, сумма элементов в которых не превышает 𝑆.
a) Число положительные целые
b) Числа целые
7/7
Скачать