# HW 2 ## Task 1 Доказать, что $\sum_{i=0}^{\log n} \log(n / 2^i) = \Theta(\log^2 n)$ ### Док-во $$\sum_{i=0}^{\log(n)} log(n / 2^i) = \sum_{i=0}^{\log(n)} \log(n) - \sum_{i=0}^{\log(n)} \log(2^i) = $$ $$ = \log(n) * \sum_{i=0}^{\log(n)} 1 - \sum_{i=0}^{\log(n)} \log(2^i) = \log(n) * \log(n) - \sum_{i=0}^{\log(n)} i =$$ $$ = \log(n) * \log(n) - (0 + \log(n)) * \log(n) / 2 =$$ $$=\log(n) * \log(n) / 2 = \Theta(\log^2 n)$$ ## Task 2 Написать код алгоритма **scan** в **fork-join** модели с $\mathcal{O}(n)$ work и $\mathcal{O}(\log(n))$ span ### Scan ```python def scan_combine(a, l, r, left, f): if r - l == 1: return middle = l + (r - l) // 2 a[r - 1] = f(a[r - 1], a[middle - 1]) a[middle - 1] = left fork2join( lambda: scan_combine(a, l, middle, left, f), lambda: scan_combine(a, middle, r, a[r - 1], f) ) def scan_tree(a, l, r, f): if r - l == 1: return middle = l + (r - l) // 2 fork2join( lambda: scan_tree(a, l, middle, f), lambda: scan_tree(a, middle, r, f) ) a[r - 1] = f(a[middle - 1], a[r - 1]) # Scan in place (a - array, f - function, x - first element) def scan(a, f, x=0): n = len(a) scan_tree(a, 0, n, f) a[n - 1] = x scan_combine(a, 0, n, x, f) return a # Scan not in place (a - array, f - function, x - first element) def scan_2(a, f, x=0): b = a.copy() return scan(b, f, x) # Example def fork2join(f_a, f_b): f_a() f_b() a = [5, 3, 7, -2, 2, 0, 4, -5] scan(a, lambda a, b: a + b) print(a) # [0, 5, 8, 15, 13, 15, 15, 19] ``` ## Task 3 Опишите алгоритм нахождения всех простых до $N$ за $\mathcal{O}(N log log N)$ work и $\mathcal{O}(polylog N)$ span (желательно за $\mathcal{O}(log N * log log N)$ span). Тут можно пользоваться *parallel_for*, *map*, *scan* и *filter*. Псевдокод даже необязательно. ### Решение Описание алгоритма текстом + поясняющие вставки псевдокода (код дублирует текст). #### Обозначения ```python prime = [i for i in range(N)] ``` #### Цель: Приспособить алгоритм решета Эратосфена для выполнения паралельно. #### Идея: ##### Шаг 1. Запустим в два вложенных параллельных цикла алгоритм нахождения решета Эратосфена, только будем идти не до $n$, а до $\sqrt n$ в обоих циклах. ```python # work: N #(т.е. sqrt(N)^2) # span: log(sqrt(N)) def parallel_prime_step_1(n, prime): parallel_for i in range(2, sqrt(n)): if prime[i] > 0: parallel_for j in range(i * i, sqrt(n), i): prime[j] = 0 ``` Применим *filter* (x > 0) и получим набор простых чисел в диапазоне $0..\sqrt n$, обозначим за *set_of_primes* ##### Шаг 2. Осталось проверить оставшиеся числа, которых $(n - \sqrt n) \approx \mathcal{O}(n)$, на простоту. Сделаем *parallel_for* по *set_of_primes* (полученные простые числа из шага 1) и будем заполнять с помощью вложенного *parallel_for* оставшееся решето эратосфена от $\sqrt n$ до $n$. ```python # work: Nloglog(n) (Аналогично асимптотике последовательного решета Эратосфена) # Span: log(n) + log(n) (parallel_for с вложенным parallel_for) def parallel_prime_step_2(n, prime): # (sqrt(n) + x) // x * x - Ближайшее к sqrt(n) число делящееся на x без остатка parallel_for x in set_of_primes: parallel_for j in range((sqrt(n) + x) // x * x, n, x): prime[j] = 0 ``` Применим *filter* (x > 0) и получим набор простых чисел в диапазоне $\sqrt n..n$ и объединим с простыми числами из $0..\sqrt n$ ##### Итого * Всего * Шаг 1: * ```parallel_prime_step_1``` work: $N$ (т.е. $\sqrt N ^2$); span: $\log(\sqrt N)$; * ```filter``` work: $\sqrt N$; span: $\log(\sqrt N)$. * Шаг 2: * ```parallel_prime_step_2``` work: $N*\log(log(n))$; span: $\log(n) + \log(n)$; * ```filter``` work: $N$; span: $\log(N)$; * ```concat of arrays``` work: $N$; span: $1$. * Итого: * work: $N*\log(log(n))$; * span: $\log(n)$.