Занятие 13. Рубежный контроль № 1

Занятие 13 — первый рубежный контроль: письменная работа по материалу занятий 1–12. Эта страница — всё, что нужно для подготовки: формат, карта тем со ссылками на конспекты, критерии и тренировочный вариант, устроенный ровно как боевой.

Формат

  • 90 минут, письменно, без электроники и конспектов.
  • 6 заданий, 26 баллов; вес указан у каждого задания, частичные баллы есть везде — пишите рассуждение, даже если ответ не добит.
  • Код — на C++, не псевдокод. Мелкие синтаксические неточности не штрафуются; логика — да.
  • В задачах на двоичный поиск и два указателя выписанный инвариант обязателен: решение без инварианта оценивается не выше половины баллов задания.
  • Два параллельных варианта, равной сложности.

Карта заданий

Тема Готовиться по Баллы
1 Сложность: оценить фрагмент, упорядочить функции роста занятию 2 4
2 Рекурренты: мастер-теорема и случай, где она не работает занятию 6 4
3 Чтение C++: что напечатает, почему не компилируется, найди ошибку занятиям 3, 7, 9, 11 5
4 Амортизация: доказательство банковским методом занятиям 8, 10 5
5 Двоичный поиск по ответу: предикат + инвариант + код занятию 12 5
6 Два указателя: инвариант и подсчёт за O(n) занятиям 10, 12 3

Как выглядит решение на полный балл

В задачах 5–6 проверяется схема рассуждения, а не только код: предикат → довод монотонности одной фразой → инвариант → сдвиги под инвариант → ответ и сложность. Инвариант — это и есть решение; код — его расшифровка.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Предикат: can(d) = [сумма floor(a_i / d) >= k]
// Монотонность: d растёт => каждое слагаемое не растёт
//   => can: true...true false...false
// Инвариант: can(l) = true, can(r) = false
long long l = 1, r = maxA + 1;
while (r - l > 1) {
    long long m = l + (r - l) / 2;
    if (can(m)) l = m;
    else        r = m;
}
return l;   // последний true
// Сложность: O(n log(max a))

Тот же код без трёх строк комментария — минус половина баллов задания.

Топ граблей прошлых лет

  1. «Два вложенных цикла ⇒ n log n». Глубины циклов не перемножаются — считается сумма работы. Внешний цикл с удвоением даёт геометрическую прогрессию $1 + 2 + 4 + \dots < 2n$ — это $\Theta(n)$.
  2. (l + r) / 2. Переполнение — девятилетний баг java.util.Arrays. Только l + (r - l) / 2; в предикатах проверяйте и m * m — спасает сравнение делением m <= n / m.
  3. for (auto x : v) x *= 2; — удваиваются копии, вектор не тронут. Нужен auto&.
  4. «Амортизация = всегда O(1)». Нет: отдельная операция бывает $\Theta(n)$; гарантия — на сумму по худшей серии операций, без всяких вероятностей.

Тренировочный вариант

Устроен ровно как боевой: те же темы, тот же вес, другие числа. Сначала решите на время — 90 минут без конспекта, — потом открывайте ответы.

Задание 1. Сложность (4 балла)

a) Оцените время работы как $\Theta$ от $n$, обоснуйте:

1
2
3
4
long long s = 0;
for (int i = 0; i < n; i += 2)
    for (int j = n; j > 1; j /= 2)
        s += j;

b) Упорядочите по скорости роста, отметьте равные: $2^{2\log_2 n}$, $n \log_2 n$, $\sqrt{n} \cdot \sqrt{n}$, $3^n$.

Ответы

a) $\Theta(n \log n)$: внешний цикл — $n/2$ итераций, внутренний — $\log_2 n$ на каждой, и внутренняя работа не зависит от $i$, поэтому здесь перемножить глубины можно: итого $\frac{n}{2}\log_2 n$. Сравните с ловушкой из граблей, где внутренняя работа растёт вместе с внешним счётчиком — там спасает только сумма.

b) $\sqrt{n}\cdot\sqrt{n} = n$ — медленнее всех; затем $n\log_2 n$; затем $2^{2\log_2 n} = n^2$; быстрее всех $3^n$. Равных классов среди перечисленных нет, но два выражения — «замаскированные» степени: $n$ и $n^2$.

Задание 2. Рекурренты (4 балла)

a) Решите основной теоремой (укажите $a$, $b$, $c$ и случай): $T(n) = 2\,T(n/4) + \Theta(\sqrt{n})$.

b) Из двух соотношений одно решается основной теоремой, другое — нет. Решите оба и объясните, какое и почему выпадает из теоремы: $T(n) = T(n/2) + \Theta(1)$ и $T(n) = T(n-2) + \Theta(1)$.

Ответы

a) $a = 2$, $b = 4$, $c = 1/2$. $\log_4 2 = 1/2 = c$ — случай равенства: $T(n) = \Theta(\sqrt{n}\,\log n)$.

b) $T(n) = T(n/2) + \Theta(1)$ — это двоичный поиск: $a = 1$, $b = 2$, $c = 0$, $\log_2 1 = 0 = c$ — снова равенство, $\Theta(\log n)$. А для $T(n) = T(n-2) + \Theta(1)$ теорема неприменима (размер уменьшается вычитанием): цепочка глубины $n/2$, итого $\Theta(n)$.

Задание 3. Чтение C++ (5 баллов)

a) Что напечатает программа и почему?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct C {
    int v;
    int  get() const { return v; }
    void set(int v)  { v = v; }
};
int main() {
    C c{10};
    c.set(42);
    std::cout << c.get();
}

b) Автор положил в заголовок utils.h строку static int counter = 0; и включил его в три .cpp-файла, ожидая общий счётчик. Что получилось на самом деле?

c) Для каждой строки: компилируется ли, и если да — какой тип:

1
2
3
4
enum class Level : std::uint8_t { Low, High };
auto a = Level::Low;          // (1)
int  b = Level::High;         // (2)
auto c = 7 / 2.0;             // (3)
Ответы

a) 10. В set параметр v затеняет поле: v = v — самоприсваивание параметра, поле не тронуто (clang предупредит: -Wself-assign). Лечение: this->v = v или другое имя параметра.

b) Три независимых счётчика — по одному в каждой единице трансляции: static у переменной уровня файла означает внутреннюю линковку, каждый .cpp получает свою копию. Общий счётчик — это inline int counter = 0; (C++17) или extern-объявление с одним определением.

c) (1) компилируется, тип Level — auto выводит тип перечисления. (2) не компилируется: у enum class нет неявной конверсии в int, нужен static_cast<int>(Level::High). (3) компилируется, тип double: деление int на double идёт в double — и это 3.5, а не 3.

Задание 4. Амортизация (5 баллов)

Двоичный счётчик на $k$ битах поддерживает одну операцию — инкремент: он перещёлкивает младшие единицы в нули и первую попавшуюся ноль в единицу.

a) Докажите банковским методом, что инкремент имеет амортизированную стоимость $O(1)$: укажите тариф и объясните, почему баланс не уходит в минус.

b) Отдельный инкремент может перещёлкнуть все $k$ бит. Сформулируйте точно, что гарантирует амортизированная оценка — и чего она не гарантирует.

Ответы

a) Тариф — 2 монеты на инкремент: одна оплачивает установку единицы (ровно один бит за инкремент переходит 0 → 1), вторая кладётся на этот бит — она оплатит его будущее обнуление. Каждое обнуление бита списывает монетку, лежащую на нём с момента установки, — банк неотрицателен. Серия из $m$ инкрементов стоит ≤ $2m$.

b) Гарантия: любая последовательность из $m$ инкрементов (со стартового нуля) выполняется за $O(m)$ суммарно. Не гарантируется: стоимость отдельного вызова (бывает $\Theta(k)$) и ничего «в среднем по случайным входам» — усреднение здесь по операциям худшей серии, вероятностей нет.

Задание 5. Двоичный поиск по ответу (5 баллов)

Найдите наибольшее целое $k$ такое, что $k^3 \le n$, для $n$ до $10^{18}$.

a) Предикат, довод монотонности, инвариант.

b) Функция long long icbrt(long long n); сложность. Осторожно: $m^3$ для середины $m$ может не влезать в long long — как проверять без переполнения?

Ответы

a) Предикат ok(k) = $[k^3 \le n]$; монотонность: $k$ растёт ⇒ $k^3$ растёт ⇒ ok: true…true false…false; ищем последний true. Инвариант: ok(l) = true, ok(r) = false; старт $l = 0$ ($0 \le n$), $r = 10^6 + 1$ (при $n \le 10^{18}$ куб $10^6$ — ровно $10^{18}$, дальше заведомо больше). Ответ — l.

b)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
long long icbrt(long long n) {
    long long l = 0, r = 1'000'001;
    // инвариант: l^3 <= n < r^3
    while (r - l > 1) {
        long long m = l + (r - l) / 2;
        // m^3 <= n  без переполнения: делим, а не умножаем
        if (m <= n / m / m) l = m;
        else                r = m;
    }
    return l;
}

Сложность $O(\log 10^6) \approx 20$ итераций. Проверка m <= n / m / m эквивалентна $m^3 \le n$ с точностью до целочисленного округления вниз — и не переполняется. (Вариант: ограничить $r$ значением $2 \cdot 10^6$ и считать __int128.) Обратите внимание: границу $r$ мы обосновали — «ответом может быть граница диапазона» из чек-листа занятия 12.

Задание 6. Два указателя (3 балла)

Массив из $n$ положительных чисел. Найдите максимальную длину подотрезка с суммой $\le M$ за $O(n)$. Инвариант окна обязателен.

Ответ

Это задача с семинара 12 — окно [l, r]:

1
2
3
4
5
6
long long sum = 0; int best = 0, l = 0;
for (int r = 0; r < n; r++) {
    sum += a[r];
    while (sum > M) sum -= a[l++];
    best = std::max(best, r - l + 1);
}

Инвариант: после внутреннего while сумма на $[l, r]$ не превосходит $M$, и $l$ — минимально возможное для данного $r$ (сдвигать левее нельзя — сумма была бы больше $M$… точнее: любое меньшее $l$ давало сумму $> M$, положительность элементов гарантирует, что сумма при расширении влево только растёт). Оба указателя движутся только вправо: $r$ — по циклу, $l$ — только внутрь; суммарно ≤ $2n$ шагов ⇒ $O(n)$ — амортизация из занятия 10.

Стратегия на 90 минут

Время Что делаем
0–10 прочитать все задания, пометить лёгкие
10–35 собрать быстрые баллы: задания 1–3
35–70 тяжёлая тройка 4–6, начиная с самой «своей»
70–85 проверка: краевые случаи, инварианты на месте?
85–90 буфер и чистовик

Застряли на десять минут — переключитесь: балл в соседней задаче дешевле. Проверка краёв — как на семинаре 12: массив из двух элементов, массив из одинаковых, пустая структура.

После контрольной

Разбор обоих вариантов — в начале занятия 14; апелляции — неделя после объявления результатов, приходить с работой и конкретным пунктом критериев. Дальше по курсу — умные указатели: занятие 11 обещало «delete на всех путях сделают за нас», посмотрим, как именно.