Лекция 18

Параллельные алгоритмы

(Конспект: К. Орлова)

18.1 Параллельные вычисления

Введем следующие обозначения:

• c(n) — количество процессоров,

• T (n) — время работы,

• W (n) = c(n)T (n) — общая работа. Заметим, что если у нас есть программа, написанная для c(n) процессоров, то мы всегда сможем переписать ее для меньшего количества c_{1}(n); полученная программа будет работать время O(W (n)/c_{1}(n)).

Модель вычислений можно выбрать, например, такую: RAM-машина с c(n) процессорами; у каждого — одна и та же программа; нулевой регистр у каждого свой (перед началом работы там записан номер процессора), остальные — общие. У нас может возникнуть проблема коллизий, то есть два процессора решат одновременно записать данные в один регистр. Ее можно решить двумя способами:

• запретить такие програмы;

• записывать то, что записал процессор с большим номером.

18.2 Достижимость в графе

Пусть A — матрица смежности. Если мы умножим ее булевски на себя, то получим те ребра, которые достижимы за два шага. Если в матрицу A записать на диагональ единички, то квадрат даст нам вершины, достижимые за два или менее шагов. Матрицу A^{2} умножим ее еще раз на себя: мы получим вершины, достижимые за четыре или менее шагов, и т. д. Итого: нам нужен алгоритм, возводящий A в (n − 1)-ую (или большую) степень. Будем считать^{1}, что n − 1 = 2^{k}.

1Ясно, что это несущественно.

A A A A … A A \/ \/ \/

A^{2} A^{2} … A^{2} ..................

\/

A^{n−1}

n процессоров сделают сие действо за O(log(n)) операций над матрицами. Но произведение матриц не есть элементарная операция. Пусть мы хотим умножить матрицы B и C, из булево произведение — D; тогда

d_{ij} = ∨_{k=1}^{n} b_{ik} ∧ c_{kj} можно вычислить следующим способом:

b_{i1} ∧ c_{1j} b_{i2} ∧ c_{2j} … b_{in} ∧ c_{nj} \ / ... \ / \ / \ / \/ \/ \ / \ / \ /

d_{ij}

При этом каждый из элементов d_{ij} мы вычисляем на своем наборе процессоров. За O(log(n)) операций и n^{3} процессоров мы перемножим две матрицы. Всего же мы управимся за время O(log^{3} n) и O(n^{4}) процессоров.

18.3 Максимальное по включению независимое множество

Определение 18.1. V′ ⊆ V — независимое множество в графе (V, E), если ∀u, v ∈ V′ {u, v} ∉ E.

Определение 18.2. Обозначим через deg(v) степень (количество соседей) вершины v.

Алгоритм. Вход: граф G = (V, E). Выход: независимое множество S.

Инициализация: S := ∅.

На каждой итерации мы будем делать следующее.

1. ∀v ∈ V:

если deg(v) = 0, то добавить v в S;

если deg(v) ≠ 0, то пометить v c вероятностью 1/(2deg(v)).

2. ∀e ∈ E:

если оба конца ребра e помечены, то снять пометку с того, у которого степень меньше.

3. Запихать в S все помеченные вершины.

4. V := V \ {S ∪ соседи вершин из S } (при этом надо и E обновить

соответственно). Повторять эти шаги будем, пока граф не станет пустым.

Анализ алгоритма. Корректность алгоритма очевидна. Шаги этого алгоритма можно выполнять параллельно для всех вершин или ребер, при этом (при разумной реализации) на один шаг будет тратиться время O(log n). Остается выяснить, сколько будет итераций: если их также O(log n), то мы получили искомый �быстрый� параллельный алгоритм. В дальнейшем этот факт не будет доказан полностью; однако, будут приведены соображения, подтверждающие это интуитивно.

Будем называть вершину хорошей, если ⩾ 1/3 ее соседей имеют степень меньшую, чем степень этой вершины.

Лемма 18.1. Пусть v — хорошая вершина. Тогда вероятность, что один из ее соседей помечен ⩾ 1 − e^{−1/6}.

Доказательство. Действительно, вероятность, что это не так, не превосходит (1 − 1/2deg(v))^{deg(v)/3}, что, в свою очередь, не больше e^{−1/6}.

Лемма 18.2. Вероятность снятия пометки — не более 1/2.

Доказательство. Данная вероятность не превосходит вероятности того, что помечен один из соседей большей степени; что не меньше, чем deg(v)· 1/(2deg(v)), то есть 1/2.

Следствие 18.1. Если v — хорошая, то с вероятностью ⩾ (1−e^{−1/6})/2 хотя бы один из ее соседей попадет в S.

Ребро e будем называть хорошим, если хотя бы один из его концов — хороший.

Лемма 18.3. Количество хороших ребер ⩾ |E|/2.

Доказательство. Направим ребра из вершин с меньшей степенью в вершины с большей^{2} (теперь наш граф — ориентированный; пусть deg_{in}(v) и deg_{out}(v) — входная и выходная степени вершины v соответственно).

Через E(V_{1}, V_{2}) обозначим количество ребер, идущих из вершин множества V_{1} в вершины множества V_{2}; пусть V_{g} — множество всех хороших вершин, V_{b} — множество всех плохих вершин.

2И как угодно для ребер, соединяющих вершины одинаковой степени.

2E(V_{b}, V_{b}) + E(V_{b}, V_{g}) + E(V_{g}, V_{b}) ⩽

∑ ; v — плохаяdeg(v) ⩽

3 ∑ ; v — плохая(deg_{out}(v) − deg_{in}(v)) =

3(E(V_{b}, V_{g}) + E(V_{b}, V_{b}) − E(V_{g}, V_{b}) − E(V_{b}, V_{b})) ⩽

3(E(V_{b}, V_{g}) + E(V_{g}, V_{b})).

Сократим начало и конец этого неравенства. Итого, у нас хороших ребер больше, чем плохих, а значит, их больше половины.

Хороших ребер больше половины; каждое из них пропадает с вероятностью ⩾ (1 − e^{−1/6})/2; значит, мат. ожидание количества пропавших ребер ⩾ (1 − e^{−1/6})|E|/4. Выходит, что на каждой итерации количество ребер сокращается в константное число раз^{3}; значит, алгоритм будет совершать лишь логарифмическое число итераций.

3Увы, лишь в среднем. Поэтому последующий вывод доказывается не так просто.