Add ru version (#1865)

* Add Russian docs site baseline

* Add Russian localized codebase

* Polish Russian code wording

* Update ru code translation.

* Update code translation and chapter covers.

* Fix pythontutor extraction.

* Add README and landing page.

* placeholder of profiles

* Use figures of English version

* Remove chapter paperbook
This commit is contained in:
Yudong Jin
2026-03-28 04:24:07 +08:00
committed by GitHub
parent 2ca570cc33
commit 772183705e
1958 changed files with 108186 additions and 0 deletions
@@ -0,0 +1,207 @@
# Задача о полном рюкзаке
В этом разделе сначала решим еще один распространенный вариант задачи о рюкзаке - полный рюкзак, а затем рассмотрим одну из его типичных специальных форм: задачу о размене монет.
## Задача о полном рюкзаке
!!! question
Даны $n$ предметов. Вес $i$-го предмета равен $wgt[i-1]$ , стоимость равна $val[i-1]$ . Также дан рюкзак вместимости $cap$ . **Каждый предмет можно выбирать многократно**. Найдите максимальную суммарную стоимость, которую можно поместить в рюкзак при заданной вместимости. Пример показан на рисунке ниже.
![Пример данных для задачи о полном рюкзаке](unbounded_knapsack_problem.assets/unbounded_knapsack_example.png)
### Идея динамического программирования
Задача о полном рюкзаке очень похожа на задачу о рюкзаке 0-1; **разница состоит только в том, что число выборов каждого предмета не ограничено**.
- В задаче о рюкзаке 0-1 каждого предмета существует только один экземпляр, поэтому после того как предмет $i$ помещен в рюкзак, выбирать можно только из первых $i-1$ предметов.
- В задаче о полном рюкзаке число экземпляров каждого предмета бесконечно, поэтому после того как предмет $i$ помещен в рюкзак, **выбирать все еще можно из первых $i$ предметов**.
При этом состояние $[i, c]$ в задаче о полном рюкзаке может изменяться двумя способами.
- **Не брать предмет $i$** : как и в задаче о рюкзаке 0-1, переход осуществляется в $[i-1, c]$ .
- **Взять предмет $i$** : в отличие от рюкзака 0-1 переход происходит в $[i, c-wgt[i-1]]$ .
Следовательно, уравнение перехода состояния принимает вид:
$$
dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
$$
### Реализация кода
Если сравнить код этой задачи с кодом задачи о рюкзаке 0-1, то окажется, что в переходе состояний меняется только одна деталь: вместо $i-1$ появляется $i$ ; все остальное остается таким же:
```src
[file]{unbounded_knapsack}-[class]{}-[func]{unbounded_knapsack_dp}
```
### Оптимизация пространства
Поскольку текущее состояние переходит из состояния слева и состояния сверху, **после оптимизации памяти каждую строку таблицы $dp$ нужно обходить слева направо**.
Этот порядок обхода как раз противоположен задаче о рюкзаке 0-1. Разницу удобно понять по рисунку ниже.
=== "<1>"
![Процесс динамического программирования после оптимизации памяти для полного рюкзака](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step1.png)
=== "<2>"
![unbounded_knapsack_dp_comp_step2](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step2.png)
=== "<3>"
![unbounded_knapsack_dp_comp_step3](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step3.png)
=== "<4>"
![unbounded_knapsack_dp_comp_step4](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step4.png)
=== "<5>"
![unbounded_knapsack_dp_comp_step5](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step5.png)
=== "<6>"
![unbounded_knapsack_dp_comp_step6](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step6.png)
Код реализации здесь довольно прост: достаточно просто убрать первое измерение массива `dp` :
```src
[file]{unbounded_knapsack}-[class]{}-[func]{unbounded_knapsack_dp_comp}
```
## Задача о размене монет
Задача о рюкзаке представляет собой целый класс задач динамического программирования, у которого есть множество вариантов, и одной из таких вариаций является задача о размене монет.
!!! question
Даны $n$ видов монет, номинал монеты $i$ равен $coins[i - 1]$ , а целевая сумма равна $amt$ . **Монеты каждого вида можно брать многократно**. Требуется найти минимальное число монет, которыми можно набрать целевую сумму. Если набрать сумму невозможно, верните $-1$ . Пример показан на рисунке ниже.
![Пример данных для задачи о размене монет](unbounded_knapsack_problem.assets/coin_change_example.png)
### Идея динамического программирования
**Задачу о размене монет можно рассматривать как частный случай задачи о полном рюкзаке** ; между ними существует следующая связь и следующие различия.
- Эти две задачи можно взаимно переводить друг в друга: "предмет" соответствует "монете", "вес предмета" соответствует "номиналу монеты", а "вместимость рюкзака" соответствует "целевой сумме".
- Цель оптимизации противоположна: в задаче о полном рюкзаке нужно максимизировать стоимость предметов, а в задаче о размене монет - минимизировать число монет.
- В задаче о полном рюкзаке ищется решение, не превышающее вместимость, а в задаче о размене монет требуется **ровно** набрать целевую сумму.
**Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу $dp$**
Подзадача, соответствующая состоянию $[i, a]$ , выглядит так: **минимальное число монет из первых $i$ видов, которыми можно набрать сумму $a$**. Решение этой подзадачи обозначается как $dp[i, a]$ .
Размер двумерной таблицы $dp$ равен $(n+1) \times (amt+1)$ .
**Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния**
По сравнению с задачей о полном рюкзаке здесь есть два отличия в уравнении перехода состояния.
- Нужно искать минимум, а не максимум, поэтому оператор $\max()$ заменяется на $\min()$ .
- Оптимизируемое значение - это число монет, а не суммарная стоимость, поэтому при выборе монеты нужно просто прибавить $1$ .
$$
dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
$$
**Шаг 3: определить граничные условия и порядок переходов**
Когда целевая сумма равна $0$ , минимальное число монет для ее набора равно $0$ , то есть весь первый столбец $dp[i, 0]$ заполняется нулями.
Когда монет нет, **невозможно набрать никакую целевую сумму $> 0$** ; это и есть недопустимое решение. Чтобы функция $\min()$ в уравнении перехода состояния могла распознавать и отбрасывать такие недопустимые решения, удобно использовать значение $+ \infty$ ; то есть всю первую строку $dp[0, a]$ нужно инициализировать значением $+ \infty$ .
### Реализация кода
Большинство языков программирования не предоставляет готовую переменную $+ \infty$ для целых чисел, поэтому обычно приходится заменять ее на максимальное значение типа `int` . Но тогда возникает риск переполнения: операция $+ 1$ в уравнении перехода может переполнить большое число.
Поэтому здесь мы используем число $amt + 1$ как обозначение недопустимого решения, потому что для набора суммы $amt$ максимум нужно не больше чем $amt$ монет. Перед возвратом результата проверяем, равно ли $dp[n, amt]$ значению $amt + 1$ ; если да, то возвращаем $-1$ , что означает невозможность набрать целевую сумму. Код приведен ниже:
```src
[file]{coin_change}-[class]{}-[func]{coin_change_dp}
```
Как показано на рисунке ниже, процесс динамического программирования для задачи о размене монет очень похож на задачу о полном рюкзаке.
=== "<1>"
![Процесс динамического программирования для задачи о размене монет](unbounded_knapsack_problem.assets/coin_change_dp_step1.png)
=== "<2>"
![coin_change_dp_step2](unbounded_knapsack_problem.assets/coin_change_dp_step2.png)
=== "<3>"
![coin_change_dp_step3](unbounded_knapsack_problem.assets/coin_change_dp_step3.png)
=== "<4>"
![coin_change_dp_step4](unbounded_knapsack_problem.assets/coin_change_dp_step4.png)
=== "<5>"
![coin_change_dp_step5](unbounded_knapsack_problem.assets/coin_change_dp_step5.png)
=== "<6>"
![coin_change_dp_step6](unbounded_knapsack_problem.assets/coin_change_dp_step6.png)
=== "<7>"
![coin_change_dp_step7](unbounded_knapsack_problem.assets/coin_change_dp_step7.png)
=== "<8>"
![coin_change_dp_step8](unbounded_knapsack_problem.assets/coin_change_dp_step8.png)
=== "<9>"
![coin_change_dp_step9](unbounded_knapsack_problem.assets/coin_change_dp_step9.png)
=== "<10>"
![coin_change_dp_step10](unbounded_knapsack_problem.assets/coin_change_dp_step10.png)
=== "<11>"
![coin_change_dp_step11](unbounded_knapsack_problem.assets/coin_change_dp_step11.png)
=== "<12>"
![coin_change_dp_step12](unbounded_knapsack_problem.assets/coin_change_dp_step12.png)
=== "<13>"
![coin_change_dp_step13](unbounded_knapsack_problem.assets/coin_change_dp_step13.png)
=== "<14>"
![coin_change_dp_step14](unbounded_knapsack_problem.assets/coin_change_dp_step14.png)
=== "<15>"
![coin_change_dp_step15](unbounded_knapsack_problem.assets/coin_change_dp_step15.png)
### Оптимизация пространства
Оптимизация пространства для задачи о размене монет выполняется так же, как и для полного рюкзака:
```src
[file]{coin_change}-[class]{}-[func]{coin_change_dp_comp}
```
## Задача о размене монет II
!!! question
Даны $n$ видов монет, номинал монеты $i$ равен $coins[i - 1]$ , а целевая сумма равна $amt$ . Монеты каждого вида можно брать многократно. **Найдите число различных комбинаций монет, которыми можно набрать целевую сумму**. Пример показан на рисунке ниже.
![Пример данных для задачи о размене монет II](unbounded_knapsack_problem.assets/coin_change_ii_example.png)
### Идея динамического программирования
По сравнению с предыдущей задачей теперь целью является число комбинаций. Поэтому подзадача меняется на следующую: **число комбинаций из первых $i$ видов монет, которыми можно набрать сумму $a$**. При этом таблица $dp$ по-прежнему остается двумерной матрицей размера $(n+1) \times (amt + 1)$ .
Число комбинаций для текущего состояния равно сумме числа комбинаций для двух решений: не брать текущую монету и брать текущую монету. Поэтому уравнение перехода состояния принимает вид:
$$
dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]]
$$
Когда целевая сумма равна $0$ , ее можно набрать, не выбирая ни одной монеты, поэтому весь первый столбец $dp[i, 0]$ нужно инициализировать единицами. Когда монет нет, невозможно набрать никакую сумму $>0$ , поэтому вся первая строка $dp[0, a]$ должна быть заполнена нулями.
### Реализация кода
```src
[file]{coin_change_ii}-[class]{}-[func]{coin_change_ii_dp}
```
### Оптимизация пространства
При оптимизации памяти способ остается тем же самым: достаточно убрать измерение, отвечающее за виды монет:
```src
[file]{coin_change_ii}-[class]{}-[func]{coin_change_ii_dp_comp}
```