# Домашнє завдання · Цикли

> Лекція: [lecture.html](lecture.html) · Практика: [practice.ipynb](practice.ipynb) · Тест: [quiz.html](quiz.html)

Три рівні. Кожен наступний спирається на попередній, але братися можна за будь-який.
Пиши в окремому файлі `homework_12.py` або в новому зошиті — головне, щоб код
запускався й друкував результат.

Дозволено з бібліотек: `math`, `sys`, `collections`. Більше нічого не треба.
Функцій (тема 14) і спискових включень (тема 13) ми ще не проходили — усі завдання
розвʼязуються звичайними циклами, і саме так їх і треба розвʼязувати.

---

## 🟢 Рівень 1 — База

### 1.1 · Свій кошик і свій чек

Створи власний список покупок (щонайменше **шість** позицій), словник цін до нього й
список кількостей тієї самої довжини. Потім:

1. одним циклом надрукуй пронумерований чек через `enumerate(…, start=1)` —
   у форматі `1. хліб × 2 = 57.00`;
2. другим циклом накопич суму до сплати;
3. третім циклом порахуй, **скільки** позицій дорожчі за середню ціну кошика.

**Зроблено, якщо:** чек друкується з нумерацією від 1, а наприкінці виконуються

```python
assert round(до_сплати, 2) == round(sum(ціни[н] * к for н, к in zip(покупки, кількості)), 2)
assert len(покупки) == len(кількості) == len(ціни)
```

(перший `assert` — єдине місце в цьому ДЗ, де можна взяти генератор: він тут не для
розвʼязку, а для перевірки твого циклу незалежним способом).

### 1.2 · Три форми `range` без підглядання

Не запускаючи код, випиши на папірці, що дадуть ці пʼять викликів, і лише потім
перевір себе:

```python
range(4)
range(1, 5)
range(0, 10, 3)
range(10, 0, -3)
range(5, 5)
```

**Зроблено, якщо:** для кожного виклику ти надрукував `list(...)` і `len(...)`, а для
двох останніх — письмово пояснив одним реченням, чому вийшло саме стільки значень.
Перевір формулу на всіх пʼятьох:

```python
import math
for старт, стоп, крок in [(0, 4, 1), (1, 5, 1), (0, 10, 3), (10, 0, -3), (5, 5, 1)]:
    очікувано = max(0, math.ceil((стоп - старт) / крок))
    assert len(range(старт, стоп, крок)) == очікувано
```

### 1.3 · Слово навпаки й палiндром

Візьми будь-яке слово. Одним циклом збери його перевернуту версію (як у практиці),
другим — порахуй, скільки в ньому голосних (`"аеєиіїоуюя"`).

**Зроблено, якщо:** виконуються

```python
assert навпаки == слово[::-1]
assert голосних == sum(1 for с in слово if с in "аеєиіїоуюя")
```

і твій код правильно каже про `"око"` — що це паліндром, а про `"мед"` — що ні.

---

## 🟡 Рівень 2 — Плюс

### 2.1 · Максимум без `max()`

Напиши цикл, який знаходить **найдорожчу** позицію в словнику цін: заведи
`найдорожча_назва` й `найдорожча_ціна` **до** циклу й оновлюй їх усередині.

Окремо подумай, чим ініціалізувати `найдорожча_ціна`. Нуль — погана ідея (а якщо всі
ціни відʼємні?). Варіант із `None` і перевіркою на першому кроці — хороша.

**Зроблено, якщо:** виконуються

```python
assert найдорожча_назва == max(ціни, key=ціни.get)
assert найдорожча_ціна == max(ціни.values())
```

а на **порожньому** словнику твій код не падає з помилкою, а друкує «кошик порожній».

### 2.2 · Скільки разів трапляється кожна літера

Візьми речення щонайменше на 60 символів. Одним циклом по символах побудуй словник
«літера → кількість», використовуючи ідіому `лічильник[с] = лічильник.get(с, 0) + 1`.
Пробіли не рахуй.

Потім **другим** циклом знайди три найчастіші літери — без `sorted` і без `Counter`,
просто тричі пройшовши по словнику й щоразу забираючи максимум.

**Зроблено, якщо:** виконується

```python
from collections import Counter
assert лічильник == dict(Counter(текст.replace(" ", "")))
assert трійка == [пара[0] for пара in Counter(текст.replace(" ", "")).most_common(3)]
```

і ти пояснив у коментарі, чому другий цикл довелось прогнати саме тричі, а не один раз.

### 2.3 · `while`, який точно завершиться

Гра «вгадай число» без введення з клавіатури: задумане число зашите в коді, а «гравець»
щоразу називає середину поточного проміжку (це **двійковий пошук**). Цикл крутиться,
поки не вгадав.

```python
задумане = 73
низ, верх = 1, 100
спроб = 0
```

**Зроблено, якщо:** цикл завершується, друкує кожну спробу й наприкінці виконуються

```python
assert спроба == задумане
assert спроб <= 7, "двійковий пошук на 100 числах не потребує більше 7 спроб"
```

і ти письмово відповів на два питання: (1) який саме рядок у тілі наближає умову до
хибності; (2) що станеться, якщо забути оновлювати `низ` і `верх` — і чому це та сама
помилка, що й нульовий крок в інтерактиві 6.

### 2.4 · Таблиця, яку читає людина

Побудуй вкладеними циклами таблицю множення 9×9 із **шапкою**: перший рядок і перший
стовпець — це самі множники, відділені рискою.

**Зроблено, якщо:** таблиця вирівняна по колонках (підказка: `f"{x:>4}"`), а
збережений паралельно двовимірний список проходить

```python
assert таблиця[0][0] == 1 and таблиця[8][8] == 81
assert таблиця[5][2] == 6 * 3
assert len(таблиця) == 9 and len(таблиця[3]) == 9
```

---

## 🔴 Рівень 3 — Виклик

### 3.1 · Прибрати дорогі позиції на місці

Напиши цикл, який видаляє зі списку всі числа, більші за задану межу, **змінюючи
оригінальний список** — тобто не створюючи нового й не роблячи присвоєння
`список = щось_інше`. Ім'я має вказувати на той самий обʼєкт до й після.

Головна складність — там, де «дорогі» стоять **підряд**: саме на цьому ламається
наївний обхід.

**Зроблено, якщо:** виконуються всі перевірки

```python
дані = [10, 200, 300, 20]
той_самий = дані                 # другий ярлик на той самий обʼєкт
# ... твій код ...
assert дані == [10, 20]
assert той_самий == [10, 20], "ти створив новий список замість того, щоб змінити наявний"
assert id(дані) == id(той_самий)
```

і ще на трьох наборах: `[500, 600, 700]` → `[]`, `[1, 2, 3]` → `[1, 2, 3]`,
`[]` → `[]`. Плюс письмове пояснення (2–3 речення), чому наївний варіант
`for x in дані: if x > межа: дані.remove(x)` дає для `[10, 200, 300, 20]` результат
`[10, 300, 20]`.

### 3.2 · Свій `enumerate` і свій `zip`

Не використовуючи вбудовані `enumerate` і `zip`, збери такі самі результати
звичайними циклами:

- `мій_enumerate(послідовність, старт)` — список кортежів `(номер, елемент)`;
- `мій_zip(а, б)` — список кортежів, обрізаний по **найкоротшому**.

Функцій ми ще не вчили, тому просто напиши два блоки коду, які кладуть результат
у змінні `мій_enumerate` і `мій_zip`.

**Зроблено, якщо:** виконуються

```python
покупки = ["хліб", "молоко", "яблука", "мед", "сіль"]
кількості = [2, 1, 3, 1]

assert мій_enumerate == list(enumerate(покупки, start=1))
assert мій_zip == list(zip(покупки, кількості))
assert len(мій_zip) == 4, "обрізати треба по коротшому, а не по довшому"
```

і ти пояснив, звідки в твоєму `мій_zip` береться число 4 — яким саме виразом ти його
отримав.

### 3.3 · Найдовша серія підряд

Дано список результатів матчів, наприклад
`["П", "П", "Н", "П", "П", "П", "Л", "П"]` («П» — перемога). Одним проходом знайди
**найдовшу серію перемог підряд** та її початковий індекс. Одним — означає, що список
можна обійти рівно один раз, без вкладених циклів.

**Зроблено, якщо:** виконуються

```python
матчі = ["П", "П", "Н", "П", "П", "П", "Л", "П"]
assert найдовша_серія == 3
assert початок_серії == 3
```

а також на крайніх випадках: порожній список → серія 0; список суцільних «П» довжини 5
→ серія 5 з початком 0; список без жодної «П» → серія 0. І поясни письмово, навіщо тут
**дві** пари змінних — поточна серія й найкраща з побачених.

---

## Підказки

- **Накопичувач завжди створюють до циклу.** Це стосується і суми, і лічильника, і
  «найкращого з побачених». Якщо всередині циклу є рядок, який щоразу повертає змінну
  до початкового стану, — він стоїть не там.
- **`for i in range(len(...))` майже завжди зайвий.** Виняток один: коли індекс потрібен
  не для читання, а для **запису** в той самий список. У 3.1 це саме той випадок.
- **У 3.1 напрямок має значення.** Якщо йти з кінця (`range(len(дані) - 1, -1, -1)`),
  видалення елемента не зачіпає ті позиції, які ще не пройдені: зсуваються лише ті, що
  правіше, а ти туди вже не повернешся. Це найкоротший коректний розвʼязок.
- **У 2.3 обовʼязково друкуй кожну спробу.** Якщо цикл раптом зависне, ти одразу
  побачиш, на якому проміжку він застряг, — і це буде відповіддю на друге питання.
- **У 3.3 не намагайся оновлювати «найкращу» серію лише в кінці циклу.** Оновлюй її на
  кожному кроці, де поточна серія стала довшою. Тоді випадок «найдовша серія в самому
  кінці списку» не потребуватиме окремої перевірки після циклу.
- **Порожній вхід — перше, що варто перевірити.** Порожній список, порожній словник,
  `range`, який не дає жодного значення: у всіх трьох випадках тіло циклу не виконається
  жодного разу, і код має це пережити без помилки.
