# 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)$.