Семинар. Рекурсия и бинарный поиск
Семинар 4. Рекурсия и бинарный поиск
Лекция 4 дала рекурсию (репка, матрёшка, фракталы, генерация) и бинарный поиск. На паре сначала короткая разминка на простую рекурсию, потом бинпоиск по полуинтервалу $[l, r)$ на примерах, и только затем контест.
Ноутбук — тетрадь пары. Его семинаристу не отправляйте. Зачёт пары — выполнение обязательных задач № 4 (278) и № 5 (875) из контеста на algo.ai.msu.ru (задачи с LeetCode).
Заглушки печатают неверный ответ — так и задумано, их надо переписать.
Можно спрашивать соседа и семинариста. Готовое решение копировать не стоит: на зачёте рядом никого не будет.
Как устроен ноутбук
Как на семинарах 1–3: ячейки Markdown и Code; командный режим — Esc, правка — Enter; Shift+Enter выполняет ячейку.
Скачайте с сайта курса этот файл .ipynb. Отдельного файла тестов на этой неделе нет: упражнения смотрите по выводу, сдаёте контест на платформе.
Ячейки с комментарием «Ваш эксперимент» никто автоматически не проверяет.
Порядок работы
- Разминка: простая рекурсия — база, шаг, глубина.
- Почему «пополам» быстро, и мост от рекурсии к поиску.
- Полуинтервал $[l, r)$ на примерах.
- Массив:
lower_bound,upper_bound, диапазон равных. - Шкала без массива:
first_trueи монотонный предикат. - Поиск по ответу: проверка
ok(t), которая сама стоит линейное время. - Контест на algo.ai.msu.ru — две обязательные задачи, разминка и задачи повышенной сложности.
Повтор задач прошлых недель — в конце ноутбука, как запас. Merge sort и quick sort на этой паре не пишем: это семинар 5.
Разминка: простая рекурсия
На лекции рекурсия была на матрёшке и фракталах. Здесь несколько коротких функций, чтобы база и шаг стали привычными.
В каждой задаче проверяйте три вещи:
- база — случай, который решается без нового вызова;
- шаг — сведение к меньшему аргументу;
- уменьшение — параметр обязан двигаться к базе, иначе получите
RecursionError.
Обратный отсчёт и прямой
countdown(n) печатает $n, n-1, \ldots, 1$, затем строку Пуск!. countup(n) печатает $1, 2, \ldots, n$. Циклы не используем, только рекурсия.
Две функции отличаются местом печати: до рекурсивного вызова или после него. Это та же разница, что «открываем матрёшку» и «закрываем матрёшку» на лекции.
def countdown(n):
"""Печатает n, n-1, ..., 1, затем 'Пуск!'; n — целое, n >= 0."""
print("заглушка")
def countup(n):
"""Печатает 1, 2, ..., n; n — целое, n >= 0."""
print("заглушка")
countdown(3) # ожидается 3, 2, 1, Пуск!
print("---")
countup(3) # ожидается 1, 2, 3
Факториал и сумма цифр
$$F(0)=1,\qquad F(n)=n\cdot F(n-1),\quad n\ge 1.$$
Здесь функция возвращает значение, а не печатает: базовый случай тоже обязан вернуть число.
Затем напишите digit_sum(n): отделите последнюю цифру с помощью n % 10, а сумму остальных найдите рекурсивно для n // 10. Продумайте базу для n = 0.
def factorial(n):
"""n! по рекуррентной формуле; n — целое, n >= 0."""
return 0
def digit_sum(n):
"""Сумма десятичных цифр; n — целое, n >= 0. Подсказка: n % 10 и n // 10."""
return -1
print(factorial(0), factorial(1), factorial(5)) # ожидается 1 1 120
print(digit_sum(0), digit_sum(7), digit_sum(1234)) # ожидается 0 7 10
Глубина рекурсии и число вызовов
Это две разные величины. У countdown(n) каждый вызов делает ровно один вложенный: вызовов $n+1$ и глубина $n+1$ — «палка». Если вызовов два, получается дерево: число вызовов растёт гораздо быстрее, чем глубина.
Посмотрите на числах Фибоначчи: fib(0) = 0, fib(1) = 1, fib(n) = fib(n-1) + fib(n-2). Счётчик уже встроен, вам нужно только тело функции.
calls = 0
def fib(n):
"""n-е число Фибоначчи; n — целое, n >= 0."""
global calls
calls += 1
return 0
for n in (5, 10, 20):
calls = 0
value = fib(n)
print(f"n={n}: fib={value}, вызовов={calls}")
# ожидается fib(5)=5, fib(10)=55, fib(20)=6765
# глубина при этом всего n: число вызовов и глубина — не одно и то же
Мост к бинпоиску: уменьшать вдвое, а не на единицу
Рекурсия уменьшает аргумент. Если вычитать единицу, шагов будет $n$. Если делить пополам, шагов будет около $\log_2 n$. На этом стоит весь бинарный поиск.
Возведение в степень двумя способами:
$$a^n = a\cdot a^{n-1} \quad\text{— около } n \text{ умножений};$$
$$a^n = \bigl(a^{n/2}\bigr)^2 \text{ при чётном } n, \qquad a^n = a\cdot a^{n-1} \text{ при нечётном} \quad\text{— } O(\log n) \text{ умножений при } n\ge 2.$$
В чётном случае вычислите половинную степень один раз, сохраните результат и умножьте его на себя. Два одинаковых рекурсивных вызова уничтожат выигрыш.
def power_slow(a, n):
"""a ** n за n умножений; n — целое, n >= 0."""
return 0
def power_fast(a, n):
"""a ** n делением степени пополам; n — целое, n >= 0."""
return 0
print(power_slow(2, 10), power_fast(2, 10)) # ожидается 1024 1024
print(power_fast(3, 0), power_fast(3, 1)) # ожидается 1 3
print(power_fast(2, 100) == 2 ** 100) # ожидается True
# Ваш эксперимент: добавьте в обе функции счётчик умножений
# и сравните его при n = 100. Во сколько раз разница?
# При n = 1000 медленная версия может превысить лимит глубины рекурсии Python.
Почему «пополам» — это быстро
Линейный просмотр упорядоченного массива из $10^9$ элементов — миллиард сравнений. Деление пополам сводит задачу к десяткам шагов: сосчитайте, сколько раз миллиард делится пополам до единицы.
n = 10 ** 9
steps = 0
while n > 1:
n //= 2
steps += 1
print("шагов деления пополам для 10**9:", steps)
print("линейный проход сделал бы 10**9 сравнений")
Полуинтервал $[l, r)$
На этой неделе один договор, его не меняем.
Держим целые $l$ и $r$, непроверенные значения — $l, l+1, \ldots, r-1$. $l$ входит, $r$ не входит. При этом граница ответа лежит в замкнутом диапазоне $[l, r]$: сам $r$ тоже может быть ответом.
Ищем первый $t$ на шкале, где предикат стал истинным (и дальше не становится ложным):
- все значения строго левее $l$ — предикат ложен;
- все значения с $r$ и правее — предикат истинен, либо шкала уже кончилась.
Пока $l < r$:
- середина $m = (l + r) // 2$ попадает в $[l, r)$;
- если предикат на $m$ истинен — ответ не правее $m$, поэтому $r = m$;
- иначе $l = m + 1$.
Когда цикл остановился, $l = r$, и $l$ — первый истинный (если истинных не было, вернётся исходный правый конец).
После каждой итерации полуинтервал строго короче. Если оставить $m$ в диапазоне ($l = m$ при ложном предикате), цикл может не сузиться.
Тот же поиск на двух примерах
$a = [1, 2, 4, 4, 7]$, ищем первый индекс с $a[i] \ge 4$. Шкала — индексы, полуинтервал $[0, 5)$.
| Шаг | $l$ | $r$ | $m$ | $a[m]$ | $a[m] \ge 4$ | Что делаем |
|---|---|---|---|---|---|---|
| 1 | 0 | 5 | 2 | 4 | истина | $r = m = 2$ |
| 2 | 0 | 2 | 1 | 2 | ложь | $l = m + 1 = 2$ |
| 3 | 2 | 2 | — | — | — | $l = r$, стоп |
Ответ $l = 2$. При истинном предикате элемент на $m$ остаётся кандидатом ($r = m$, а не $m - 1$); при ложном — выбрасывается ($l = m + 1$).
Теперь тот же массив и $x = 3$, которого в массиве нет:
| Шаг | $l$ | $r$ | $m$ | $a[m]$ | $a[m] \ge 3$ | Что делаем |
|---|---|---|---|---|---|---|
| 1 | 0 | 5 | 2 | 4 | истина | $r = 2$ |
| 2 | 0 | 2 | 1 | 2 | ложь | $l = 2$ |
| 3 | 2 | 2 | — | — | — | стоп |
Ответ снова $2$ — это место, куда вставить тройку, чтобы массив остался упорядоченным. Цикл находит место вставки. Чтобы ответить «где ключ», дополнительно проверьте i < len(a) и a[i] == x; иначе ключа нет.
Проследите третий пример сами: $x = 8$ на том же массиве. Ответ должен получиться $5$, то есть за концом.
Массив: lower_bound и upper_bound
Неубывающий массив — частный случай шкалы: предикат на середине — это сравнение a[m] >= x.
lower_bound— первый индекс с $a[i] \ge x$. Если все элементы меньше $x$, ответ $n$.upper_bound— первый индекс с $a[i] > x$.
Равные $x$ занимают полуинтервал $[\texttt{lower\_bound}, \texttt{upper\_bound})$, а их количество — разность концов. Пустой массив: $[0, 0)$, цикл сразу останавливается, ответ $0$.
def lower_bound(a, x):
"""Первый индекс i с a[i] >= x; если такого нет — len(a)."""
return -1
print(lower_bound([1, 2, 4, 4, 7], 4)) # ожидается 2
print(lower_bound([1, 2, 4, 4, 7], 3)) # ожидается 2
print(lower_bound([1, 2, 4, 4, 7], 8)) # ожидается 5
print(lower_bound([7, 7, 7], 7)) # ожидается 0
print(lower_bound([], 1)) # ожидается 0
def upper_bound(a, x):
"""Первый индекс i с a[i] > x; если такого нет — len(a)."""
return -1
def equal_range(a, x):
"""Полуинтервал (первый равный, за последним равным)."""
return (-1, -1)
print(upper_bound([1, 2, 4, 4, 7], 4)) # ожидается 4
print(upper_bound([1, 2, 4, 4, 7], 7)) # ожидается 5
print(equal_range([1, 2, 4, 4, 7], 4)) # ожидается (2, 4): две четвёрки
print(equal_range([1, 2, 4, 4, 7], 3)) # ожидается (2, 2): равных нет
# Ваш эксперимент: все элементы больше x; массив из одного элемента;
# ключ меньше всех. Что должен вернуть каждый из двух поисков?
Когда цикл не сужается
Нарочно оставим $m$ в диапазоне при ложном предикате. Ограничьте число итераций, иначе ячейка зависнет.
def stuck_lower_bound(a, x, max_steps=20):
l, r = 0, len(a)
for step in range(max_steps):
if l >= r:
return l
m = (l + r) // 2
print(f"шаг {step}: l={l}, r={r}, m={m}")
if a[m] >= x:
r = m
else:
l = m # ошибка: должно быть m + 1
print("полуинтервал не сжался")
return None
stuck_lower_bound([1, 2, 4, 4, 7], 4)
Шкала без массива: first_true
В lower_bound предикат — это a[m] >= x. Но самому циклу неважно, откуда берётся ответ «да / нет». Вынесем предикат в функцию ok(t) — получится универсальный first_true, который ищет первое True на шкале $[l, r)$.
Так решают задачи, где массива значений нет вообще: есть только «уже можно / ещё нельзя» при данном $t$. Договоримся: если истинных нет, ответ равен исходному $r$.
def first_true(ok, l, r):
"""Первый t в [l, r), где ok(t) истинно; если таких нет — вернуть r."""
return -1
print(first_true(lambda t: t >= 4, 0, 10)) # ожидается 4
print(first_true(lambda t: t >= 0, 0, 5)) # ожидается 0
print(first_true(lambda t: False, 0, 5)) # ожидается 5
Медленный эталон для сверки
Пока свой цикл не отлажен, держите рядом заведомо правильную, но медленную версию: перебрать все $t$ слева направо. На контесте она получит TLE, а для сверки на маленькой шкале годится.
import random
def first_true_slow(ok, l, r):
"""Перебор всех t: медленно, зато очевидно правильно."""
for t in range(l, r):
if ok(t):
return t
return r
for _ in range(5):
threshold = random.randint(0, 25)
ok = lambda t: t >= threshold
fast, slow = first_true(ok, 0, 20), first_true_slow(ok, 0, 20)
print(f"порог {threshold:2}: быстро {fast:2}, медленно {slow:2}", "ok" if fast == slow else "РАСХОЖДЕНИЕ")
Предикат обязан быть монотонным: один переход с «ещё нельзя» на «уже можно». Если в середине дырка, тот же цикл имеет право соврать.
def not_monotonic(t):
return t in (2, 5)
# Запустите после того, как напишете first_true.
print(first_true(not_monotonic, 0, 8))
print([not_monotonic(t) for t in range(8)])
# цикл ищет «первый истинный на монотонной шкале» — здесь шкалы такой нет
Поиск по ответу
Шкала — не индексы, а сами возможные ответы: скорость, вместимость, длина, порог. Схема из двух шагов:
- написать
ok(t)— «при этом $t$ уже получается?»; - пустить
first_trueпо шкале $t$.
Почти вся работа — в ok. Начнём с разминки, где ok дешёвый: целый квадратный корень.
Максимальное целое $x \ge 0$ с $x^2 \le n$. Предикат «$t^2 > n$» ложен на префиксе шкалы и истинен после; первый такой $t$ минус единица — ответ. Правый конец берите заведомо истинным: подойдёт $[0, n+2)$. Никаких sqrt и float — на больших числах вещественная арифметика врёт.
def isqrt(n):
# максимальное x >= 0 с x * x <= n; полуинтервал [l, r)
return -1
print(isqrt(0)) # ожидается 0
print(isqrt(1)) # ожидается 1
print(isqrt(8)) # ожидается 2
print(isqrt(9)) # ожидается 3
print(isqrt(10)) # ожидается 3
Когда одна проверка стоит линейное время
Дальше ok(t) сам проходит по входу. Два типовых вида проверки:
- Сумма с округлением вверх. Кучки размеров $a_i$, скорость $v$ за час, за час берут только одну кучку: нужно $\sum_i \lceil a_i / v \rceil$ часов. Округление вверх целыми числами —
(x + v - 1) // v, безmath.ceil. - Жадное разрезание. Идём слева направо и набираем подряд идущие элементы, пока сумма не превышает
cap; как только очередной не влезает — начинаем новую часть. Результат — минимальное число частей. Здесь элементы положительные, массив непустой иcap >= max(a). Объясните, почему максимально длинная допустимая первая часть не увеличивает необходимое число частей.
Обе проверки монотонны: если справились при $t$, то справимся и при любом большем $t$. Проговорите это, прежде чем писать цикл: на неверной монотонности бинпоиск молча врёт.
Стоимость: одна проверка $O(n)$, проверок $O(\log M)$, где $M$ — число значений на шкале поиска ($M \ge 2$). Всего $O(n \log M)$. Линейный перебор всех $t$ вместо бинпоиска — типичный TLE.
def hours_needed(piles, v):
"""Сколько часов уйдёт на кучки piles при скорости v > 0 (одна кучка за раз)."""
return -1
def parts_needed(a, cap):
"""Минимум частей с суммой <= cap; a непустой, элементы > 0, cap >= max(a)."""
return -1
print(hours_needed([3, 6, 7, 11], 4)) # ожидается 8 = 1 + 2 + 2 + 3
print(hours_needed([3, 6, 7, 11], 11)) # ожидается 4 — по часу на кучку
print(parts_needed([1, 2, 3, 4, 5], 5)) # ожидается 4: [1,2] [3] [4] [5]
print(parts_needed([1, 2, 3, 4, 5], 15)) # ожидается 1
# Соберите поиск по ответу из своих first_true и hours_needed:
# минимальная целая скорость, при которой хватает h часов.
piles = [3, 6, 7, 11]
h = 8
print(first_true(lambda v: hours_needed(piles, v) <= h, 1, max(piles) + 1)) # ожидается 4
# Ваш эксперимент: то же для parts_needed — минимальная вместимость,
# при которой частей не больше заданного числа. Где начинается шкала?
Повышенная сложность: поиск пика
Это упражнение к необязательной задаче № 7 (162). Его можно оставить до выполнения обязательных задач контеста.
В непустом массиве, где соседи не равны, найдите любой локальный максимум: элемент больше обоих соседей; за краями считаем $-\infty$.
Здесь другое обоснование деления пополам: сравнение $a[m] > a[m+1]$ не обязано быть монотонным. Например, для [1, 3, 2, 4, 1] результаты сравнений соседей — False, True, False, True. Готовый first_true к этому предикату применять нельзя.
Для отдельного цикла поддерживаем непустой полуинтервал $[l, r)$ и инвариант: на его левой границе подъём ($a[l] > a[l-1]$), на правой — спуск ($a[r-1] > a[r]$), с условными $-\infty$ за краями массива. Поэтому внутри есть пик. Пока элементов больше одного, выбираем $m$ так, чтобы и $m$, и $m+1$ лежали внутри диапазона. При спуске оставляем $[l, m+1)$, при подъёме — $[m+1, r)$. Объясните, почему инвариант сохраняется.
Сначала обоснуйте существование пика, затем напишите цикл. Это поиск половины, содержащей ответ, а не поиск первого истинного монотонного предиката.
def find_peak(a):
"""Индекс любого локального максимума; соседи не равны, за краями -inf."""
return -1
print(find_peak([1, 2, 3, 1])) # ожидается 2
print(find_peak([1, 2, 1, 3, 5, 6, 4])) # ожидается 1 или 5
print(find_peak([5])) # ожидается 0
print(find_peak([1, 2])) # ожидается 1
Контест: что обязательно, а что по желанию
Задачи сдавайте в контест «Бинарный поиск» на algo.ai.msu.ru. Номера ниже соответствуют списку из семи задач; номера LeetCode помогают найти нужное условие.
Для зачёта семинара обязательны № 4 (278. First Bad Version) и № 5 (875. Koko Eating Bananas). Разминка и задачи повышенной сложности не обязательны и не заменяют эти две задачи.
| № | Задача | Статус на семинаре | Что из ноутбука |
|---|---|---|---|
| 1 | 704. Binary Search | Разминка, по необходимости | lower_bound, затем проверка границы массива и равенства ключу |
| 2 | 35. Search Insert Position | Разминка, по необходимости | lower_bound |
| 3 | 34. Find First and Last Position of Element in Sorted Array | Повышенная сложность, по желанию | Две границы; последний равный — upper_bound - 1; если ключа нет, вернуть [-1, -1] |
| 4 | 278. First Bad Version | Обязательная | first_true, где ok — готовая функция isBadVersion |
| 5 | 875. Koko Eating Bananas | Обязательная | hours_needed и поиск минимальной скорости |
| 6 | 1011. Capacity To Ship Packages Within D Days | Повышенная сложность, по желанию | parts_needed, обоснование жадной проверки и поиск минимальной вместимости |
| 7 | 162. Find Peak Element | Повышенная сложность ★, по желанию | Отдельный инвариант: выбранная половина содержит пик; предикат на соседях не монотонен |
Категории обозначают роль задач на этом занятии, а не рейтинг Easy / Medium на LeetCode.
Порядок работы: если нужно закрепить поиск в массиве, начните с № 1 или № 2. Если цикл уже получается, переходите сразу к № 4, затем № 5. После обязательных выбирайте № 3 или № 6; № 7 требует отдельного рассуждения. Решать все семь задач за пару не требуется. Недорешённые обязательные задачи завершите в том же контесте после занятия; остальные — по желанию.
Типичный WA: l = m вместо m + 1; перепутали первый допустимый и последний недопустимый ответ; перевёрнутый предикат; диапазон не охватывает нужный ответ; float и sqrt вместо точной целочисленной проверки. В нашем first_true после остановки l == r, поэтому вернуть l или r — одно и то же.
Типичный TLE: перебор всех значений ответа вместо бинпоиска; вложенные проходы по входу внутри ok(t). Один линейный проход при каждой проверке в задачах № 5 и № 6 — нормальная часть решения.
Шпаргалка
Рекурсия:
- база без нового вызова, шаг к меньшему аргументу, параметр двигается к базе;
- печать до вызова и после вызова дают разный порядок вывода;
- глубина и число вызовов — разные величины; два вызова на шаг дают дерево;
- уменьшать вдвое вместо единицы — это $\log_2 n$ шагов вместо $n$.
Бинпоиск:
- полуинтервал $[l, r)$: $l$ входит, $r$ нет. Цикл
while l < r; - предикат истинен на $m$ → $r = m$; иначе $l = m + 1$; ответ — $l$;
- шкала — индексы массива или значения ответа $t$;
- для
first_trueпредикат должен быть монотонным; поиск пика — отдельный алгоритм с инвариантом существования пика в выбранном диапазоне; - сначала
ok(t), потом цикл; считайте, сколько стоит одна проверка; bisectв жизни удобен; на паре цель — свой цикл.
Сдача
- Этот
.ipynbсеминаристу отправлять не нужно. - Для зачёта выполните № 4 — 278. First Bad Version и № 5 — 875. Koko Eating Bananas в контесте на algo.ai.msu.ru.
- № 1–2 — разминка, № 3, 6 и 7 — повышенная сложность: выполняются по желанию, для зачёта не требуются.
- Если обязательные задачи не закончены на паре, дорешайте их в том же контесте после занятия.
- Домашнее задание к лекции 4, если появится отдельным слотом, — не этот ноутбук.
Запас: повтор прошлых недель
Этот блок разбираем, только если контест пошёл быстро или группа просит. Эталоны не копируем: идея и крайний случай, код дописываем вместе с залом.
Домашку семинара 3 на паре не разбираем: дедлайн ещё не вышел. С семинара 3 — только то, что уже было на паре.
Семинар 2. Пара с ближайшей суммой
Массив уже неубывает. Нужна сумма двух элементов с разными индексами, ближайшая к заданному $S$. Если таких сумм несколько — любая из лучших. На $n \sim 10^5$ двойной цикл не успеет.
Край: отрицательные числа; две разные суммы на одном расстоянии до $S$.
def closest_pair_sum(a, s):
# a уже неубывает
return 0
print(closest_pair_sum([2, 7, 11, 15], 9)) # ожидается 9
print(closest_pair_sum([2, 7, 11, 15], 18)) # ожидается 18
print(closest_pair_sum([-10, 0, 0, 10], 0)) # ожидается 0
Семинар 2. Максимальная сумма пары на расстоянии
Даны $a$ и $K$. Среди пар с разницей индексов не меньше $K$ найдите наибольшую сумму. Числа могут быть отрицательными. Два вложенных цикла на большом $n$ не живут.
Край: все отрицательные; $K = 1$ (подходит любая пара с разными индексами).
def max_pair_sum_distance(a, k):
return 0
print(max_pair_sum_distance([5, 2, -1, 0, 3], 2)) # ожидается 8: 5 + 3, расстояние между индексами 4 >= 2
print(max_pair_sum_distance([1, 100, -50, 2, 3, -7], 3)) # ожидается 103
print(max_pair_sum_distance([-1, -2, -3, -4], 1)) # ожидается -3
Семинар 3. Пары (ключ, нагрузка)
Упорядочить пары $(k, p)$ по неубыванию только ключа $k$. При равных ключах относительный порядок пар как во входе. Если сравнить пару целиком, нагрузка $p$ может переставить равные ключи.
Это не бинпоиск. Разбираем, потому что на той паре на этом часто ломались, а домашку недели 3 ещё рано вскрывать.
def stable_by_key(pairs):
# pairs — список кортежей (k, p)
return pairs
print(stable_by_key([(2, 9), (1, 5), (2, 3)]))
# ожидается [(1, 5), (2, 9), (2, 3)] — при ключе 2 сначала 9, потом 3
Семинар 3. Ключи из малого диапазона
Массив целых из $0..1000$, длина до $10^5$ и больше. Выписать в неубывающем порядке. Квадратичная сортировка получит TLE.
Индекс массива снова может значить значение, не позицию — как частоты на семинаре 2.
Мост к сегодняшней теме: если бы диапазон значений был большим, функция $F(t) = \#\{a_i \le t\}$ неубывает по $t$. Первый $t$ с $F(t) \ge k$ ищут тем же бинпоиском по функции, не проходом по всем $t$. Это уже не условие той задачи, а задел.
def counting_sort(a):
# 0 <= a[i] <= 1000
return a
print(counting_sort([3, 1, 3, 2, 1, 3])) # ожидается [1, 1, 2, 3, 3, 3]
print(counting_sort([7, 7, 7])) # ожидается [7, 7, 7]