Уроки D2-D7 (14 файлов): деревья/куча, epoll, потоки, gdb, графы, L2-L3, ДП, TCP, bash, ядро, docker, мок-интервью + cards_src 613
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
# D5, часть 1. Динамическое программирование (урок)
|
||||
|
||||
Это не проверка, а урок: сначала разбираем механизм, потом решаешь задачи. Код в разборах
|
||||
можно копировать и запускать — это образец, а не готовый ответ на задачу.
|
||||
|
||||
## 1. Когда ДП вообще применимо
|
||||
|
||||
ДП применимо, если задача одновременно даёт два свойства:
|
||||
|
||||
- **Оптимальная подструктура** — оптимальное решение всей задачи собирается из оптимальных
|
||||
решений подзадач (для LCS: если последние символы совпали, LCS всей строки — это LCS без
|
||||
последних символов плюс 1; оптимум подзадачи не пересчитывается заново).
|
||||
- **Перекрывающиеся подзадачи** — одни и те же подзадачи встречаются многократно при наивной
|
||||
рекурсии (для чисел Фибоначчи `fib(5)` вызывает `fib(3)` дважды, `fib(2)` трижды и так
|
||||
далее — без запоминания результат пересчитывается экспоненциально много раз).
|
||||
|
||||
Если подзадачи не пересекаются (например, обычное бинарное дерево решений без общих
|
||||
поддеревьев), рекурсия остаётся рекурсией — мемоизация не ускоряет её, кэшировать нечего.
|
||||
Если нет оптимальной подструктуры (жадный локальный выбор не гарантирует глобальный оптимум),
|
||||
ДП даёт неверный ответ — тогда нужен либо перебор, либо доказанный жадный алгоритм.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Два условия применимости ДП? — оптимальная подструктура + перекрывающиеся подзадачи
|
||||
- core | Почему наивный рекурсивный `fib(n)` работает за экспоненциальное время? — одни и те же подзадачи (`fib(k)` для одного и того же k) пересчитываются заново много раз
|
||||
- core | Что ломается, если применить ДП-переход к задаче без оптимальной подструктуры? — переход не отражает реальную зависимость оптимумов, ответ будет неверным независимо от таблицы
|
||||
|
||||
Почему дальше: раз задача сводится к подзадачам, нужно определить, что именно является
|
||||
«состоянием» подзадачи и как один переход выражается через предыдущие.
|
||||
|
||||
## 2. Состояние, переход, мемоизация vs табуляция
|
||||
|
||||
**Состояние** — минимальный набор параметров, однозначно определяющий подзадачу (для рюкзака:
|
||||
номер предмета + оставшийся вес; для LCS: позиции в обеих строках). **Переход** — формула,
|
||||
выражающая ответ для состояния через ответы уже решённых состояний.
|
||||
|
||||
Два способа посчитать одно и то же:
|
||||
|
||||
- **Мемоизация (top-down)** — обычная рекурсия по формуле перехода, но перед вычислением
|
||||
проверяем кэш (`unordered_map` или массив), а после вычисления кладём результат в кэш.
|
||||
Считает только реально нужные состояния, но каждый вызов — это кадр стека.
|
||||
- **Табуляция (bottom-up)** — заполняем таблицу итеративно от базовых случаев к целевому,
|
||||
без рекурсии вообще. Требует явно определить порядок заполнения (что должно быть посчитано
|
||||
раньше, чем понадобится).
|
||||
|
||||
```c++
|
||||
// мемоизация
|
||||
long long fib_memo(int n, std::vector<long long>& cache) {
|
||||
if (n <= 1) return n;
|
||||
if (cache[n] != -1) return cache[n];
|
||||
return cache[n] = fib_memo(n - 1, cache) + fib_memo(n - 2, cache);
|
||||
}
|
||||
|
||||
// табуляция
|
||||
long long fib_tab(int n) {
|
||||
std::vector<long long> dp(n + 1);
|
||||
dp[0] = 0; if (n >= 1) dp[1] = 1;
|
||||
for (int i = 2; i <= n; ++i) dp[i] = dp[i - 1] + dp[i - 2];
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
Оба варианта — O(число состояний) по времени (каждое состояние считается ровно один раз, а
|
||||
не экспоненциально много) и O(число состояний) по памяти (нужно где-то хранить ответ для
|
||||
каждого состояния). У мемоизации есть дополнительный риск, которого нет у табуляции:
|
||||
рекурсия на входе с большим n (например, `fib_memo(1'000'000, ...)`) кладёт по кадру на
|
||||
каждый уровень — стек ограничен (типично несколько МБ), и глубокая рекурсия падает с
|
||||
переполнением стека (SIGSEGV) там, где итеративная табуляция отработает без проблем.
|
||||
|
||||
**Ловушки**
|
||||
- Забыть проверить кэш перед вычислением в мемоизации → рекурсия становится обычной наивной
|
||||
→ возврат к экспоненциальному времени, видно по таймауту на больших n.
|
||||
- Взять слишком глубокую рекурсию для мемоизации (n порядка 10⁵–10⁶) → переполнение стека →
|
||||
падение по SIGSEGV, а не по логической ошибке.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Сложность по времени мемоизации/табуляции? — O(число состояний)
|
||||
- core | Чем мемоизация рискует, а табуляция — нет? — переполнением стека при большой глубине рекурсии
|
||||
- core | Что нужно определить в ДП-задаче до написания кода? — состояние (параметры подзадачи) и переход (формула через уже решённые состояния)
|
||||
|
||||
Почему дальше: раз табуляция явно хранит таблицу, логично спросить, всегда ли нужна вся
|
||||
таблица целиком, или память можно ужать.
|
||||
|
||||
## 3. Уменьшение памяти: O(n) → O(1) или O(n)
|
||||
|
||||
Если переход использует только последние 1–2 строки/значения таблицы, всю таблицу хранить не
|
||||
нужно — достаточно «скользящего окна» из нужного числа последних значений.
|
||||
|
||||
Лестница (Climbing Stairs: сколько способов подняться на n ступеней шагами по 1 или 2):
|
||||
переход `dp[i] = dp[i-1] + dp[i-2]` использует только два предыдущих значения — O(1) памяти.
|
||||
|
||||
```c++
|
||||
int climb_stairs(int n) { // способов дойти до ступени n
|
||||
if (n <= 2) return n;
|
||||
int prev2 = 1, prev1 = 2;
|
||||
for (int i = 3; i <= n; ++i) {
|
||||
int curr = prev1 + prev2;
|
||||
prev2 = prev1;
|
||||
prev1 = curr;
|
||||
}
|
||||
return prev1; // O(n) время, O(1) память
|
||||
}
|
||||
```
|
||||
|
||||
House Robber (нельзя грабить два соседних дома подряд, максимизировать сумму): тот же приём.
|
||||
`dp[i] = max(dp[i-1], dp[i-2] + nums[i])` — не ограбить дом i (взять лучший результат без
|
||||
него) или ограбить (взять лучший результат через один плюс текущий дом).
|
||||
|
||||
```c++
|
||||
int rob(const std::vector<int>& nums) {
|
||||
int prev2 = 0, prev1 = 0;
|
||||
for (int x : nums) {
|
||||
int curr = std::max(prev1, prev2 + x);
|
||||
prev2 = prev1;
|
||||
prev1 = curr;
|
||||
}
|
||||
return prev1; // O(n) время, O(1) память
|
||||
}
|
||||
```
|
||||
|
||||
Рюкзак 0/1 ужимается не до O(1), а до O(W) (одна строка по весу вместо таблицы n×W) — переход
|
||||
там зависит от целой предыдущей строки, а не от 1–2 чисел; подробно в следующем разделе.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Сложность по памяти Climbing Stairs при развёрнутых `prev1`/`prev2`? — O(1)
|
||||
- core | Почему рюкзак 0/1 нельзя ужать до O(1), только до O(W)? — переход `dp[i][w]` зависит от целой предыдущей строки по весу, а не от 1–2 соседних чисел
|
||||
|
||||
Почему дальше: рюкзак — задача, где переход по весу нельзя писать «вперёд» без потери
|
||||
корректности; разберём, почему именно назад.
|
||||
|
||||
## 4. Рюкзак 0/1: почему обратный проход по весу
|
||||
|
||||
Задача: n предметов с весом `weight[i]` и ценностью `value[i]`, вместимость W, каждый предмет
|
||||
берётся не более одного раза, максимизировать суммарную ценность.
|
||||
|
||||
```c++
|
||||
int knapsack01(const std::vector<int>& weight, const std::vector<int>& value, int W) {
|
||||
std::vector<int> dp(W + 1, 0);
|
||||
for (size_t i = 0; i < weight.size(); ++i)
|
||||
for (int w = W; w >= weight[i]; --w) // обратный проход!
|
||||
dp[w] = std::max(dp[w], dp[w - weight[i]] + value[i]);
|
||||
return dp[W]; // O(n·W) время, O(W) память
|
||||
}
|
||||
```
|
||||
|
||||
Если одномерный массив `dp[w]` переиспользуется для всех предметов подряд (без второго
|
||||
измерения по номеру предмета), то при проходе весов **вперёд** (`w` от `weight[i]` до `W`)
|
||||
значение `dp[w - weight[i]]`, использованное в переходе, могло уже быть обновлено этим же
|
||||
предметом i на текущей итерации — то есть предмет i фактически используется дважды, и задача
|
||||
незаметно превращается в рюкзак с неограниченным числом копий предмета (unbounded knapsack).
|
||||
Проход **назад** гарантирует, что `dp[w - weight[i]]` берётся из состояния «до предмета i» —
|
||||
старое значение ещё не тронуто текущей итерацией внешнего цикла.
|
||||
|
||||
**Ловушки**
|
||||
- Пройти веса вперёд в одномерном 0/1-рюкзаке → предмет учитывается несколько раз → ответ
|
||||
завышен относительно эталона, видно на тесте с одним дорогим предметом.
|
||||
- Перепутать размер массива (`W` вместо `W+1`) → индекс `dp[W]` вне границ → UB/ASAN heap-buffer-overflow.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Временная сложность 0/1-рюкзака с одномерным массивом? — O(n·W)
|
||||
- core | Почему в одномерном 0/1-рюкзаке веса обходят от W к weight[i], а не наоборот? — иначе `dp[w-weight[i]]` уже обновлён текущим предметом в этой же итерации, предмет посчитается дважды
|
||||
- deep | Как называется вариант рюкзака, в который случайно превращается 0/1-рюкзак при прямом проходе весов? — unbounded knapsack (неограниченное число копий предмета)
|
||||
|
||||
Почему дальше: та же ловушка с порядком циклов — не по направлению, а по тому, что снаружи, а
|
||||
что внутри — по-другому проявляется в задаче про размен монет.
|
||||
|
||||
## 5. Монеты: порядок циклов меняет смысл ответа
|
||||
|
||||
Coin Change (минимальное число монет для суммы `amount`, каждая монета берётся неограниченное
|
||||
число раз — это уже unbounded knapsack):
|
||||
|
||||
```c++
|
||||
int coin_change(const std::vector<int>& coins, int amount) {
|
||||
std::vector<int> dp(amount + 1, INT_MAX);
|
||||
dp[0] = 0;
|
||||
for (int a = 1; a <= amount; ++a)
|
||||
for (int c : coins)
|
||||
if (c <= a && dp[a - c] != INT_MAX)
|
||||
dp[a] = std::min(dp[a], dp[a - c] + 1);
|
||||
return dp[amount] == INT_MAX ? -1 : dp[amount]; // O(amount · coins.size())
|
||||
}
|
||||
```
|
||||
|
||||
Для минимума порядок циклов не влияет на корректность — минимум не зависит от того, в каком
|
||||
порядке предметы разрешено переиспользовать. Но для **подсчёта числа способов** (Coin Change
|
||||
II) порядок циклов меняет сам смысл ответа:
|
||||
|
||||
```c++
|
||||
// количество КОМБИНАЦИЙ: {1,2} и {2,1} — один и тот же способ
|
||||
long long change_combinations(int amount, const std::vector<int>& coins) {
|
||||
std::vector<long long> dp(amount + 1, 0);
|
||||
dp[0] = 1;
|
||||
for (int c : coins) // внешний цикл — монета
|
||||
for (int a = c; a <= amount; ++a)
|
||||
dp[a] += dp[a - c];
|
||||
return dp[amount];
|
||||
}
|
||||
|
||||
// количество ПЕРЕСТАНОВОК: {1,2} и {2,1} — разные способы
|
||||
long long change_permutations(int amount, const std::vector<int>& coins) {
|
||||
std::vector<long long> dp(amount + 1, 0);
|
||||
dp[0] = 1;
|
||||
for (int a = 1; a <= amount; ++a) // внешний цикл — сумма
|
||||
for (int c : coins)
|
||||
if (c <= a) dp[a] += dp[a - c];
|
||||
return dp[amount];
|
||||
}
|
||||
```
|
||||
|
||||
Монета снаружи цикла фиксирует «в каком порядке монеты рассматриваются» раз и навсегда для
|
||||
всех сумм — эквивалентные по составу, но переставленные последовательности монет схлопываются
|
||||
в один и тот же результат, отсюда комбинации. Сумма снаружи цикла для каждого `a` заново
|
||||
перебирает все монеты как «последнюю добавленную» — одна и та же комбинация монет, добавленная
|
||||
в разном порядке, считается несколько раз, отсюда перестановки.
|
||||
|
||||
**Ловушки**
|
||||
- Перепутать порядок циклов в задаче «сколько способов» → вместо количества комбинаций
|
||||
получается количество перестановок (число сильно больше ожидаемого) → расходится с
|
||||
эталонным ответом на тесте с 2+ разными монетами.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Сложность Coin Change (минимум монет) по времени? — O(amount · число_номиналов)
|
||||
- core | Какой порядок циклов в Coin Change II даёт число комбинаций, а какой — перестановок? — монета снаружи/сумма внутри → комбинации; сумма снаружи/монета внутри → перестановки
|
||||
|
||||
Почему дальше: рюкзак и монеты — одномерные ДП по числу. Следующий класс задач — ДП по двум
|
||||
строкам сразу, с двумерной таблицей.
|
||||
|
||||
## 6. LCS и Edit Distance: таблица (n+1)×(m+1)
|
||||
|
||||
**LCS** (длиннейшая общая подпоследовательность двух строк, не обязательно непрерывная):
|
||||
|
||||
```c++
|
||||
int lcs_length(const std::string& a, const std::string& b) {
|
||||
int n = a.size(), m = b.size();
|
||||
std::vector<std::vector<int>> dp(n + 1, std::vector<int>(m + 1, 0));
|
||||
for (int i = 1; i <= n; ++i)
|
||||
for (int j = 1; j <= m; ++j)
|
||||
dp[i][j] = (a[i - 1] == b[j - 1])
|
||||
? dp[i - 1][j - 1] + 1
|
||||
: std::max(dp[i - 1][j], dp[i][j - 1]);
|
||||
return dp[n][m]; // O(n·m) время и память
|
||||
}
|
||||
```
|
||||
|
||||
Строка 0 и столбец 0 — базовый случай «одна из строк пустая», отсюда размер `(n+1)×(m+1)`, а
|
||||
не `n×m`. Если последние символы совпадают, они точно входят в оптимальную LCS — переход к
|
||||
`dp[i-1][j-1]+1`. Если нет — общая подпоследовательность не может использовать оба последних
|
||||
символа одновременно, берём лучшее из «отбросить последний символ a» и «отбросить последний
|
||||
символ b».
|
||||
|
||||
**Edit Distance** (минимум вставок/удалений/замен, чтобы превратить строку a в b):
|
||||
|
||||
```c++
|
||||
int edit_distance(const std::string& a, const std::string& b) {
|
||||
int n = a.size(), m = b.size();
|
||||
std::vector<std::vector<int>> dp(n + 1, std::vector<int>(m + 1));
|
||||
for (int i = 0; i <= n; ++i) dp[i][0] = i; // удалить все i символов
|
||||
for (int j = 0; j <= m; ++j) dp[0][j] = j; // вставить все j символов
|
||||
for (int i = 1; i <= n; ++i)
|
||||
for (int j = 1; j <= m; ++j)
|
||||
dp[i][j] = (a[i - 1] == b[j - 1])
|
||||
? dp[i - 1][j - 1]
|
||||
: 1 + std::min({dp[i - 1][j - 1], // замена
|
||||
dp[i - 1][j], // удаление
|
||||
dp[i][j - 1]}); // вставка
|
||||
return dp[n][m]; // O(n·m) время и память
|
||||
}
|
||||
```
|
||||
|
||||
Тот же размер таблицы `(n+1)×(m+1)` и та же причина: строка/столбец 0 — превращение пустой
|
||||
строки в префикс другой строки чисто вставками или удалениями.
|
||||
|
||||
**Ловушки**
|
||||
- Завести таблицу `n×m` вместо `(n+1)×(m+1)` → нет места для базового случая «пустой префикс»
|
||||
→ неверные значения на границе или выход за границы массива.
|
||||
- В Edit Distance забыть инициализировать нулевую строку/столбец → сравнение с мусорными
|
||||
значениями → неверный ответ без падения программы (тихая ошибка).
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Размер таблицы LCS/Edit Distance для строк длины n и m? — (n+1)×(m+1)
|
||||
- base | Временная сложность LCS и Edit Distance? — O(n·m)
|
||||
- core | Почему при несовпадении последних символов в LCS берут max(dp[i-1][j], dp[i][j-1])? — оба последних символа одновременно в общую подпоследовательность войти не могут, значит хотя бы один из них можно отбросить без потери оптимальности
|
||||
|
||||
Почему дальше: LCS/Edit Distance решают за O(n·m) через явную таблицу. Следующая классическая
|
||||
задача — LIS — решается за O(n²) той же схемой, но улучшается до O(n log n) совсем другим
|
||||
приёмом.
|
||||
|
||||
## 7. LIS (НВП): O(n²) и O(n log n)
|
||||
|
||||
Длиннейшая строго возрастающая подпоследовательность массива (не обязательно непрерывная).
|
||||
Сигнатура из `tasks/05_lis`: `int lis_length(const std::vector<int>& a)`.
|
||||
|
||||
**Наивное O(n²)**: `dp[i]` — длина LIS, заканчивающейся ровно на элементе `a[i]`.
|
||||
|
||||
```c++
|
||||
int lis_naive(const std::vector<int>& a) {
|
||||
int n = a.size();
|
||||
std::vector<int> dp(n, 1);
|
||||
int best = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
for (int j = 0; j < i; ++j)
|
||||
if (a[j] < a[i]) dp[i] = std::max(dp[i], dp[j] + 1);
|
||||
best = std::max(best, dp[i]);
|
||||
}
|
||||
return best; // O(n²)
|
||||
}
|
||||
```
|
||||
|
||||
На 100 000 элементах (ограничение задачи `05_lis`) это 10¹⁰ операций — не укладывается в 2
|
||||
секунды внутреннего теста, нужен другой алгоритм.
|
||||
|
||||
**O(n log n) через массив «хвостов»**:
|
||||
|
||||
```c++
|
||||
int lis_length(const std::vector<int>& a) {
|
||||
std::vector<int> tails; // tails[k] = минимальный возможный последний
|
||||
// элемент возрастающей подпоследовательности длины k+1
|
||||
for (int x : a) {
|
||||
auto it = std::lower_bound(tails.begin(), tails.end(), x);
|
||||
if (it == tails.end()) tails.push_back(x); // x больше всех хвостов — новая длина
|
||||
else *it = x; // заменить первый хвост ≥ x на x
|
||||
}
|
||||
return tails.size(); // O(n log n)
|
||||
}
|
||||
```
|
||||
|
||||
`tails` не хранит саму подпоследовательность — только минимально возможное значение
|
||||
последнего элемента для каждой достижимой длины. `tails` всегда отсортирован по построению:
|
||||
если `x` заменяет элемент на позиции `it`, новое значение `x` меньше старого (иначе
|
||||
`lower_bound` нашёл бы другую позицию) и больше всех элементов слева от `it` (иначе
|
||||
`lower_bound` остановился бы раньше) — порядок не нарушается ни при замене, ни при добавлении
|
||||
в конец. `lower_bound` ищет первый элемент ≥ x, что даёт строго возрастающую LIS (`task.md`
|
||||
требует именно строгую); для нестрогой (неубывающей) подпоследовательности нужен
|
||||
`upper_bound`. Длина `tails` в конце равна длине LIS, хотя сам массив LIS обычно не является.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Наивная сложность LIS через dp[i]? — O(n²)
|
||||
- base | Сложность LIS через массив хвостов и бинарный поиск? — O(n log n)
|
||||
- core | Что хранится в `tails[k]`? — минимально возможный последний элемент возрастающей подпоследовательности длины k+1
|
||||
- core | Почему `tails` остаётся отсортированным после каждой замены/добавления? — новое значение всегда меньше заменяемого и больше всех элементов левее позиции, найденной `lower_bound`
|
||||
- core | `lower_bound` или `upper_bound` нужен для строго возрастающей LIS? — `lower_bound`
|
||||
- deep | Сколько операций у наивного O(n²) LIS на 100 000 элементах и почему это не укладывается в тест? — порядка 10¹⁰, тест роняет прогон при времени > 2 с
|
||||
|
||||
Почему дальше: LIS через хвосты — пример, где ДП-таблица заменяется одним отсортированным
|
||||
массивом и бинарным поиском; та же идея (жертвовать явной таблицей ради log-фактора)
|
||||
встречается и в других задачах на подпоследовательности.
|
||||
|
||||
Ссылки на задачи этого дня: `tasks/05_lis` — реализовать `lis_length` за O(n log n); наивная
|
||||
O(n²) версия не пройдёт по времени на 100 000 элементах.
|
||||
|
||||
<details>
|
||||
<summary>Проверь себя</summary>
|
||||
|
||||
1. Массив весов `[1, 3, 4]`, ценностей `[15, 20, 30]`, вместимость `W=4`. Какой ответ даст
|
||||
0/1-рюкзак и почему это не «взять предметы 1 и 2» (вес 1+3=4)?
|
||||
<details><summary>Ответ</summary>35 — рюкзак действительно может выбрать предметы с весами
|
||||
1 и 3 (суммарный вес 4, ценность 15+20=35); предмет весом 4 отдельно даёт только 30, что
|
||||
меньше. Ответ 35, а не 30 — если получилось 30, значит в переборе не сравнили оба варианта
|
||||
заполнения веса 4.</details>
|
||||
|
||||
2. Почему для Coin Change II (подсчёт числа способов, не минимума) важно, какой цикл
|
||||
снаружи — монета или сумма, — а для Coin Change (минимум монет) не важно?
|
||||
<details><summary>Ответ</summary>Минимум не зависит от порядка, в котором монеты
|
||||
рассматриваются — это просто оптимум по всем комбинациям. Подсчёт способов чувствителен к
|
||||
порядку: монета снаружи фиксирует относительный порядок монет и схлопывает перестановки
|
||||
одной комбинации в один результат (комбинации); сумма снаружи пересчитывает каждую монету
|
||||
как потенциально «последнюю» для каждой суммы заново, из-за чего одна комбинация
|
||||
монет считается один раз на каждую перестановку (перестановки).</details>
|
||||
|
||||
3. Для массива `[3, 1, 4, 1, 5, 9, 2, 6]` пройдите LIS через `tails` вручную. Чему равен
|
||||
`tails` после обработки первых пяти элементов (`3, 1, 4, 1, 5`)?
|
||||
<details><summary>Ответ</summary>`[1, 4, 5]`: 3 → `[3]`; 1 заменяет 3 → `[1]`; 4 больше
|
||||
всех → `[1,4]`; 1 заменяет первый элемент ≥1 (сам 1) → `[1,4]` без изменений; 5 больше
|
||||
всех → `[1,4,5]`.</details>
|
||||
|
||||
4. Мемоизация `fib_memo(n, cache)` вызвана с n = 200 000. Почему это скорее упадёт по
|
||||
SIGSEGV, чем отработает медленно?
|
||||
<details><summary>Ответ</summary>Каждый рекурсивный вызов держит кадр стека, пока не
|
||||
вернётся его результат; глубина рекурсии здесь равна n, а размер стека потока ограничен
|
||||
(типично несколько МБ) — при n=200 000 кадров стек переполняется раньше, чем вычисление
|
||||
успевает завершиться. Табуляция (`fib_tab`) того же n отработает без проблем — там нет
|
||||
рекурсии, только цикл.</details>
|
||||
|
||||
</details>
|
||||
|
||||
## Материалы
|
||||
|
||||
- cppreference, `std::vector` — https://en.cppreference.com/w/cpp/container/vector
|
||||
- cppreference, `std::lower_bound` — https://en.cppreference.com/w/cpp/algorithm/lower_bound
|
||||
- cppreference, `std::max` — https://en.cppreference.com/w/cpp/algorithm/max
|
||||
- cppreference, `std::min` — https://en.cppreference.com/w/cpp/algorithm/min
|
||||
- cppreference, лимиты `<climits>` (`INT_MAX`) — https://en.cppreference.com/w/cpp/types/climits
|
||||
Reference in New Issue
Block a user