This commit is contained in:
krahets
2026-04-14 18:06:19 +08:00
parent 17b2a0b630
commit cf0747ba3e
131 changed files with 604 additions and 609 deletions
@@ -4464,22 +4464,22 @@
</ol>
<h2 id="1431">14.3.1 &nbsp; Определение задачи<a class="headerlink" href="#1431" title="Permanent link">&para;</a></h2>
<p>В целом, если задача содержит перекрывающиеся подзадачи, оптимальную подструктуру и удовлетворяет свойству отсутствия последствий, то она обычно подходит для решения с помощью динамического программирования. Однако извлечь все эти свойства напрямую из формулировки задачи бывает трудно. Поэтому на практике мы обычно ослабляем требования и <strong>сначала смотрим, подходит ли задача для решения методом поиска с возвратом (полного перебора)</strong>.</p>
<p><strong>Задачи, подходящие для поиска с возвратом, обычно удовлетворяют "модели дерева решений"</strong>. Такие задачи можно описать деревом, где каждый узел представляет одно решение, а каждый путь представляет последовательность решений.</p>
<p><strong>Задачи, подходящие для поиска с возвратом, обычно удовлетворяют «модели дерева решений»</strong>. Такие задачи можно описать деревом, где каждый узел представляет одно решение, а каждый путь представляет последовательность решений.</p>
<p>Иначе говоря, если в задаче есть четко выраженные решения и ответ порождается последовательностью таких решений, то она удовлетворяет модели дерева решений и обычно допускает решение через поиск с возвратом.</p>
<p>Поверх этого у задач динамического программирования есть и некоторые дополнительные "плюсы".</p>
<p>Поверх этого у задач динамического программирования есть и некоторые дополнительные «плюсы».</p>
<ul>
<li>В условии задачи фигурируют слова "максимальный", "минимальный", "наибольший", "наименьший" и другие формулировки оптимизации.</li>
<li>В условии задачи фигурируют слова «максимальный», «минимальный», «наибольший», «наименьший» и другие формулировки оптимизации.</li>
<li>Состояния задачи можно описать списком, многомерной матрицей или деревом, и между состоянием и соседними состояниями существует рекуррентная зависимость.</li>
</ul>
<p>Соответственно, существуют и некоторые "минусы".</p>
<p>Соответственно, существуют и некоторые «минусы».</p>
<ul>
<li>Цель задачи состоит в поиске всех возможных решений, а не одного оптимального решения.</li>
<li>В формулировке явно присутствуют признаки комбинаторного перечисления, и требуется вернуть сразу много конкретных вариантов.</li>
</ul>
<p>Если задача удовлетворяет модели дерева решений и имеет достаточно явные "плюсы", мы можем предположить, что это задача динамического программирования, а затем проверить это предположение уже в процессе решения.</p>
<p>Если задача удовлетворяет модели дерева решений и имеет достаточно явные «плюсы», мы можем предположить, что это задача динамического программирования, а затем проверить это предположение уже в процессе решения.</p>
<h2 id="1432">14.3.2 &nbsp; Этапы решения задачи<a class="headerlink" href="#1432" title="Permanent link">&para;</a></h2>
<p>Конкретный процесс решения задач динамического программирования зависит от природы и сложности задачи, но обычно включает следующие шаги: описание решений, определение состояний, построение таблицы <span class="arithmatex">\(dp\)</span> , вывод уравнения перехода состояния, определение граничных условий и порядка переходов.</p>
<p>Чтобы нагляднее показать этот процесс, рассмотрим классическую задачу "минимальная сумма пути".</p>
<p>Чтобы нагляднее показать этот процесс, рассмотрим классическую задачу «минимальная сумма пути».</p>
<div class="admonition question">
<p class="admonition-title">Question</p>
<p>Дана двумерная сетка <code>grid</code> размера <span class="arithmatex">\(n \times m\)</span> , в каждой клетке которой записано неотрицательное целое число, означающее стоимость прохождения через эту клетку. Робот стартует из левой верхней клетки и за один шаг может двигаться только вправо или вниз, пока не достигнет правой нижней клетки. Верните минимальную сумму пути от левой верхней клетки до правой нижней.</p>
@@ -4489,7 +4489,7 @@
<p align="center"> Рисунок 14-10 &nbsp; Пример данных для задачи о минимальной сумме пути </p>
<p><strong>Шаг 1: понять решения на каждом раунде, определить состояние и тем самым получить таблицу <span class="arithmatex">\(dp\)</span></strong></p>
<p>В этой задаче на каждом раунде решение состоит в том, чтобы из текущей клетки сделать один шаг вниз или вправо. Пусть индексы строки и столбца текущей клетки равны <span class="arithmatex">\([i, j]\)</span> ; тогда после шага вниз или вправо индексы становятся равными <span class="arithmatex">\([i+1, j]\)</span> или <span class="arithmatex">\([i, j+1]\)</span> . Значит, состояние должно включать два переменных индекса: строки и столбца, то есть состояние обозначается как <span class="arithmatex">\([i, j]\)</span> .</p>
<p>В этой задаче на каждом раунде решение состоит в том, чтобы из текущей клетки сделать один шаг вниз или вправо. Пусть индексы строки и столбца текущей клетки равны <span class="arithmatex">\([i, j]\)</span>. Тогда после шага вниз или вправо индексы становятся равными <span class="arithmatex">\([i+1, j]\)</span> или <span class="arithmatex">\([i, j+1]\)</span> . Значит, состояние должно включать два переменных индекса: строки и столбца, то есть состояние обозначается как <span class="arithmatex">\([i, j]\)</span> .</p>
<p>Подзадача, соответствующая состоянию <span class="arithmatex">\([i, j]\)</span> , такова: минимальная сумма пути от стартовой клетки <span class="arithmatex">\([0, 0]\)</span> до клетки <span class="arithmatex">\([i, j]\)</span> . Ее решение обозначается через <span class="arithmatex">\(dp[i, j]\)</span> .</p>
<p>На этом этапе мы получаем двумерную матрицу <span class="arithmatex">\(dp\)</span> , показанную на рисунке 14-11, размер которой совпадает с размером входной сетки <code>grid</code> .</p>
<p><img alt="Определение состояния и таблицы dp" class="animation-figure" src="../dp_solution_pipeline.assets/min_path_sum_solution_state_definition.png" /></p>
@@ -4498,7 +4498,7 @@
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>Как в динамическом программировании, так и в поиске с возвратом, решение задачи можно описать как последовательность решений, а состояние образуется всеми переменными решений. Оно должно содержать всю информацию, достаточную для вывода следующего состояния.</p>
<p>Каждому состоянию соответствует некоторая подзадача, и для хранения решений всех подзадач мы определяем таблицу <span class="arithmatex">\(dp\)</span> ; каждая независимая переменная состояния становится одним измерением таблицы <span class="arithmatex">\(dp\)</span> . По сути таблица <span class="arithmatex">\(dp\)</span> - это отображение от состояния к решению соответствующей подзадачи.</p>
<p>Каждому состоянию соответствует некоторая подзадача, и для хранения решений всех подзадач мы определяем таблицу <span class="arithmatex">\(dp\)</span>. Каждая независимая переменная состояния становится одним измерением таблицы <span class="arithmatex">\(dp\)</span> . По сути таблица <span class="arithmatex">\(dp\)</span> - это отображение от состояния к решению соответствующей подзадачи.</p>
</div>
<p><strong>Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния</strong></p>
<p>Для состояния <span class="arithmatex">\([i, j]\)</span> возможны только два источника: клетка сверху <span class="arithmatex">\([i-1, j]\)</span> и клетка слева <span class="arithmatex">\([i, j-1]\)</span> . Следовательно, оптимальная подструктура выглядит так: минимальная сумма пути до <span class="arithmatex">\([i, j]\)</span> определяется меньшим из двух значений - минимальной суммы пути до <span class="arithmatex">\([i-1, j]\)</span> и минимальной суммы пути до <span class="arithmatex">\([i, j-1]\)</span> .</p>
@@ -4525,7 +4525,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
<p>В динамическом программировании граничные условия используются для инициализации таблицы <span class="arithmatex">\(dp\)</span> , а в поиске - для обрезки.</p>
<p>Смысл порядка перехода состояния в том, чтобы к моменту вычисления текущей подзадачи все более мелкие подзадачи, от которых она зависит, уже были вычислены корректно.</p>
</div>
<p>После этого анализа мы уже можем напрямую написать код динамического программирования. Однако разложение на подзадачи - это мышление "сверху вниз", поэтому с точки зрения мышления более естественно реализовывать задачу в порядке "полный перебор <span class="arithmatex">\(\rightarrow\)</span> поиск с мемоизацией <span class="arithmatex">\(\rightarrow\)</span> динамическое программирование".</p>
<p>После этого анализа мы уже можем напрямую написать код динамического программирования. Однако разложение на подзадачи - это мышление «сверху вниз», поэтому с точки зрения мышления более естественно реализовывать задачу в порядке «полный перебор <span class="arithmatex">\(\rightarrow\)</span> поиск с мемоизацией <span class="arithmatex">\(\rightarrow\)</span> динамическое программирование».</p>
<h3 id="1-1">1. &nbsp; Метод 1: полный перебор<a class="headerlink" href="#1-1" title="Permanent link">&para;</a></h3>
<p>Начав со состояния <span class="arithmatex">\([i, j]\)</span> , мы непрерывно раскладываем его на меньшие состояния <span class="arithmatex">\([i-1, j]\)</span> и <span class="arithmatex">\([i, j-1]\)</span> . Рекурсивная функция при этом имеет следующие элементы.</p>
<ul>
@@ -4789,7 +4789,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
<p><div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=from%20math%20import%20inf%0A%0Adef%20min_path_sum_dfs%28grid%3A%20list%5Blist%5Bint%5D%5D%2C%20i%3A%20int%2C%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9C%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F%20%D1%81%D1%83%D0%BC%D0%BC%D0%B0%20%D0%BF%D1%83%D1%82%D0%B8%3A%20%D0%BF%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%22%22%22%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D1%8D%D1%82%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D1%8F%D1%8F%20%D0%BB%D0%B5%D0%B2%D0%B0%D1%8F%20%D1%8F%D1%87%D0%B5%D0%B9%D0%BA%D0%B0%2C%20%D0%B7%D0%B0%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D1%82%D1%8C%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%0A%20%20%20%20if%20i%20%3D%3D%200%20and%20j%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%20grid%5B0%5D%5B0%5D%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%8B%20%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B8%20%D0%B8%D0%BB%D0%B8%20%D1%81%D1%82%D0%BE%D0%BB%D0%B1%D1%86%D0%B0%20%D0%B2%D1%8B%D1%85%D0%BE%D0%B4%D1%8F%D1%82%20%D0%B7%D0%B0%20%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D1%8B%2C%20%D0%B2%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%2B%E2%88%9E%0A%20%20%20%20if%20i%20%3C%200%20or%20j%20%3C%200%3A%0A%20%20%20%20%20%20%20%20return%20inf%0A%20%20%20%20%23%20%D0%92%D1%8B%D1%87%D0%B8%D1%81%D0%BB%D0%B8%D1%82%D1%8C%20%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%83%D1%8E%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D0%BF%D1%83%D1%82%D0%B8%20%D0%B8%D0%B7%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B3%D0%BB%D0%B0%20%D0%B4%D0%BE%20%28i-1%2C%20j%29%20%D0%B8%20%28i%2C%20j-1%29%0A%20%20%20%20up%20%3D%20min_path_sum_dfs%28grid%2C%20i%20-%201%2C%20j%29%0A%20%20%20%20left%20%3D%20min_path_sum_dfs%28grid%2C%20i%2C%20j%20-%201%29%0A%20%20%20%20%23%20%D0%92%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%83%D1%8E%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D0%BF%D1%83%D1%82%D0%B8%20%D0%B8%D0%B7%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B3%D0%BB%D0%B0%20%D0%B4%D0%BE%20%28i%2C%20j%29%0A%20%20%20%20return%20min%28left%2C%20up%29%20%2B%20grid%5Bi%5D%5Bj%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20grid%20%3D%20%5B%5B1%2C%203%2C%201%2C%205%5D%2C%20%5B2%2C%202%2C%204%2C%202%5D%2C%20%5B5%2C%203%2C%202%2C%201%5D%2C%20%5B4%2C%203%2C%205%2C%202%5D%5D%0A%20%20%20%20n%2C%20m%20%3D%20len%28grid%29%2C%20len%28grid%5B0%5D%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%0A%20%20%20%20res%20%3D%20min_path_sum_dfs%28grid%2C%20n%20-%201%2C%20m%20-%201%29%0A%20%20%20%20print%28f%22%D0%9C%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F%20%D1%81%D1%83%D0%BC%D0%BC%D0%B0%20%D0%BF%D1%83%D1%82%D0%B8%20%D0%B8%D0%B7%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B3%D0%BB%D0%B0%20%D0%B2%20%D0%BF%D1%80%D0%B0%D0%B2%D1%8B%D0%B9%20%D0%BD%D0%B8%D0%B6%D0%BD%D0%B8%D0%B9%20%3D%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=from%20math%20import%20inf%0A%0Adef%20min_path_sum_dfs%28grid%3A%20list%5Blist%5Bint%5D%5D%2C%20i%3A%20int%2C%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9C%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F%20%D1%81%D1%83%D0%BC%D0%BC%D0%B0%20%D0%BF%D1%83%D1%82%D0%B8%3A%20%D0%BF%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%22%22%22%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D1%8D%D1%82%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D1%8F%D1%8F%20%D0%BB%D0%B5%D0%B2%D0%B0%D1%8F%20%D1%8F%D1%87%D0%B5%D0%B9%D0%BA%D0%B0%2C%20%D0%B7%D0%B0%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D1%82%D1%8C%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%0A%20%20%20%20if%20i%20%3D%3D%200%20and%20j%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%20grid%5B0%5D%5B0%5D%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%8B%20%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B8%20%D0%B8%D0%BB%D0%B8%20%D1%81%D1%82%D0%BE%D0%BB%D0%B1%D1%86%D0%B0%20%D0%B2%D1%8B%D1%85%D0%BE%D0%B4%D1%8F%D1%82%20%D0%B7%D0%B0%20%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D1%8B%2C%20%D0%B2%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%2B%E2%88%9E%0A%20%20%20%20if%20i%20%3C%200%20or%20j%20%3C%200%3A%0A%20%20%20%20%20%20%20%20return%20inf%0A%20%20%20%20%23%20%D0%92%D1%8B%D1%87%D0%B8%D1%81%D0%BB%D0%B8%D1%82%D1%8C%20%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%83%D1%8E%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D0%BF%D1%83%D1%82%D0%B8%20%D0%B8%D0%B7%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B3%D0%BB%D0%B0%20%D0%B4%D0%BE%20%28i-1%2C%20j%29%20%D0%B8%20%28i%2C%20j-1%29%0A%20%20%20%20up%20%3D%20min_path_sum_dfs%28grid%2C%20i%20-%201%2C%20j%29%0A%20%20%20%20left%20%3D%20min_path_sum_dfs%28grid%2C%20i%2C%20j%20-%201%29%0A%20%20%20%20%23%20%D0%92%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%83%D1%8E%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D0%BF%D1%83%D1%82%D0%B8%20%D0%B8%D0%B7%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B3%D0%BB%D0%B0%20%D0%B4%D0%BE%20%28i%2C%20j%29%0A%20%20%20%20return%20min%28left%2C%20up%29%20%2B%20grid%5Bi%5D%5Bj%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20grid%20%3D%20%5B%5B1%2C%203%2C%201%2C%205%5D%2C%20%5B2%2C%202%2C%204%2C%202%5D%2C%20%5B5%2C%203%2C%202%2C%201%5D%2C%20%5B4%2C%203%2C%205%2C%202%5D%5D%0A%20%20%20%20n%2C%20m%20%3D%20len%28grid%29%2C%20len%28grid%5B0%5D%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%0A%20%20%20%20res%20%3D%20min_path_sum_dfs%28grid%2C%20n%20-%201%2C%20m%20-%201%29%0A%20%20%20%20print%28f%22%D0%9C%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F%20%D1%81%D1%83%D0%BC%D0%BC%D0%B0%20%D0%BF%D1%83%D1%82%D0%B8%20%D0%B8%D0%B7%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B3%D0%BB%D0%B0%20%D0%B2%20%D0%BF%D1%80%D0%B0%D0%B2%D1%8B%D0%B9%20%D0%BD%D0%B8%D0%B6%D0%BD%D0%B8%D0%B9%20%3D%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>На рисунке 14-14 показано дерево рекурсии с корнем в <span class="arithmatex">\(dp[2, 1]\)</span> ; в нем содержатся перекрывающиеся подзадачи, и их число будет резко расти вместе с размером сетки <code>grid</code> .</p>
<p>На рисунке 14-14 показано дерево рекурсии с корнем в <span class="arithmatex">\(dp[2, 1]\)</span>. В нем содержатся перекрывающиеся подзадачи, и их число будет резко расти вместе с размером сетки <code>grid</code> .</p>
<p>По своей сути причина появления перекрывающихся подзадач такова: <strong>существует много разных путей от левого верхнего угла до одной и той же клетки</strong>.</p>
<p><img alt="Дерево рекурсии полного перебора" class="animation-figure" src="../dp_solution_pipeline.assets/min_path_sum_dfs.png" /></p>
<p align="center"> Рисунок 14-14 &nbsp; Дерево рекурсии полного перебора </p>