Backtracking — это систематический перебор всех возможных решений с откатом при обнаружении тупика. Строим решение пошагово, отменяя последний шаг если он ведёт к невалидному состоянию.
package main
func subsets(nums []int) [][]int {
var result [][]int
var backtrack func(start int, current []int)
backtrack = func(start int, current []int) {
// Every state is a solution — copy current slice
tmp := make([]int, len(current))
copy(tmp, current)
result = append(result, tmp)
for i := start; i < len(nums); i++ {
current = append(current, nums[i])
backtrack(i+1, current)
current = current[:len(current)-1] // Undo
}
}
backtrack(0, []int{})
return result
}
// nums = [1, 2, 3]
// Result: [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]
static List<List<int>> Subsets(int[] nums)
{
var result = new List<List<int>>();
var current = new List<int>();
void Backtrack(int start)
{
result.Add([.. current]); // Every state is a solution — copy it
for (int i = start; i < nums.Length; i++)
{
current.Add(nums[i]);
Backtrack(i + 1);
current.RemoveAt(current.Count - 1); // Undo
}
}
Backtrack(0);
return result;
}
// nums = [1, 2, 3]
// Result: [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]
def subsets(nums: list[int]) -> list[list[int]]:
result: list[list[int]] = []
current: list[int] = []
def backtrack(start: int) -> None:
result.append(current.copy()) # Every state is a solution — copy it
for i in range(start, len(nums)):
current.append(nums[i])
backtrack(i + 1)
current.pop() # Undo
backtrack(0)
return result
# nums = [1, 2, 3]
# Result: [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]
package main
import "sort"
func subsetsWithDup(nums []int) [][]int {
sort.Ints(nums) // Sort to group duplicates
var result [][]int
var backtrack func(start int, current []int)
backtrack = func(start int, current []int) {
tmp := make([]int, len(current))
copy(tmp, current)
result = append(result, tmp)
for i := start; i < len(nums); i++ {
if i > start && nums[i] == nums[i-1] {
continue // Skip duplicates
}
current = append(current, nums[i])
backtrack(i+1, current)
current = current[:len(current)-1]
}
}
backtrack(0, []int{})
return result
}
static List<List<int>> SubsetsWithDup(int[] nums)
{
Array.Sort(nums); // Sort to group duplicates
var result = new List<List<int>>();
var current = new List<int>();
void Backtrack(int start)
{
result.Add([.. current]);
for (int i = start; i < nums.Length; i++)
{
if (i > start && nums[i] == nums[i - 1])
{
continue; // Skip duplicates
}
current.Add(nums[i]);
Backtrack(i + 1);
current.RemoveAt(current.Count - 1);
}
}
Backtrack(0);
return result;
}
def subsets_with_dup(nums: list[int]) -> list[list[int]]:
nums.sort() # Sort to group duplicates
result: list[list[int]] = []
current: list[int] = []
def backtrack(start: int) -> None:
result.append(current.copy())
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i - 1]:
continue # Skip duplicates
current.append(nums[i])
backtrack(i + 1)
current.pop()
backtrack(0)
return result
package main
func permutations(nums []int) [][]int {
var result [][]int
var backtrack func(current, remaining []int)
backtrack = func(current, remaining []int) {
if len(remaining) == 0 {
tmp := make([]int, len(current))
copy(tmp, current)
result = append(result, tmp)
return
}
for i := 0; i < len(remaining); i++ {
current = append(current, remaining[i])
// Build new remaining without element i
newRemaining := make([]int, 0, len(remaining)-1)
newRemaining = append(newRemaining, remaining[:i]...)
newRemaining = append(newRemaining, remaining[i+1:]...)
backtrack(current, newRemaining)
current = current[:len(current)-1]
}
}
backtrack([]int{}, nums)
return result
}
// nums = [1, 2, 3]
// Result: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]
static List<List<int>> Permutations(int[] nums)
{
var result = new List<List<int>>();
var current = new List<int>();
void Backtrack(List<int> remaining)
{
if (remaining.Count == 0)
{
result.Add([.. current]);
return;
}
for (int i = 0; i < remaining.Count; i++)
{
current.Add(remaining[i]);
// Build new remaining without element i
var newRemaining = new List<int>(remaining);
newRemaining.RemoveAt(i);
Backtrack(newRemaining);
current.RemoveAt(current.Count - 1);
}
}
Backtrack([.. nums]);
return result;
}
// nums = [1, 2, 3]
// Result: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]
def permutations(nums: list[int]) -> list[list[int]]:
result: list[list[int]] = []
current: list[int] = []
def backtrack(remaining: list[int]) -> None:
if not remaining:
result.append(current.copy())
return
for i, value in enumerate(remaining):
current.append(value)
# Build new remaining without element i
backtrack(remaining[:i] + remaining[i + 1 :])
current.pop()
backtrack(nums)
return result
# nums = [1, 2, 3]
# Result: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]
# The stdlib equivalent: list(itertools.permutations(nums))
package main
func permutationsSwap(nums []int) [][]int {
var result [][]int
var backtrack func(start int)
backtrack = func(start int) {
if start == len(nums) {
tmp := make([]int, len(nums))
copy(tmp, nums) // Copy by value
result = append(result, tmp)
return
}
for i := start; i < len(nums); i++ {
nums[start], nums[i] = nums[i], nums[start]
backtrack(start + 1)
nums[start], nums[i] = nums[i], nums[start]
}
}
backtrack(0)
return result
}
static List<List<int>> PermutationsSwap(int[] nums)
{
var result = new List<List<int>>();
void Backtrack(int start)
{
if (start == nums.Length)
{
result.Add([.. nums]); // Copy by value
return;
}
for (int i = start; i < nums.Length; i++)
{
(nums[start], nums[i]) = (nums[i], nums[start]);
Backtrack(start + 1);
(nums[start], nums[i]) = (nums[i], nums[start]);
}
}
Backtrack(0);
return result;
}
def permutations_swap(nums: list[int]) -> list[list[int]]:
result: list[list[int]] = []
def backtrack(start: int) -> None:
if start == len(nums):
result.append(nums.copy()) # Copy by value
return
for i in range(start, len(nums)):
nums[start], nums[i] = nums[i], nums[start]
backtrack(start + 1)
nums[start], nums[i] = nums[i], nums[start]
backtrack(0)
return result
## Задача 3: Combinations
<?php
declare(strict_types=1);
/**
* @return list<list<int>>
*/
function combine(int $n, int $k): array
{
$result = [];
$backtrack = function (int $start, array $current) use (&$backtrack, &$result, $n, $k): void {
if (count($current) === $k) {
$result[] = $current;
return;
}
// Optimization: stop if not enough elements remain
for ($i = $start; $i <= $n - ($k - count($current)) + 1; $i++) {
$current[] = $i;
$backtrack($i + 1, $current);
array_pop($current);
}
};
$backtrack(1, []);
return $result;
}
// n=4, k=2
// Result: [[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]
package main
func combine(n, k int) [][]int {
var result [][]int
var backtrack func(start int, current []int)
backtrack = func(start int, current []int) {
if len(current) == k {
tmp := make([]int, k)
copy(tmp, current)
result = append(result, tmp)
return
}
// Optimization: stop if not enough elements remain
for i := start; i <= n-(k-len(current))+1; i++ {
current = append(current, i)
backtrack(i+1, current)
current = current[:len(current)-1]
}
}
backtrack(1, []int{})
return result
}
// n=4, k=2
// Result: [[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]
static List<List<int>> Combine(int n, int k)
{
var result = new List<List<int>>();
var current = new List<int>();
void Backtrack(int start)
{
if (current.Count == k)
{
result.Add([.. current]);
return;
}
// Optimization: stop if not enough elements remain
for (int i = start; i <= n - (k - current.Count) + 1; i++)
{
current.Add(i);
Backtrack(i + 1);
current.RemoveAt(current.Count - 1);
}
}
Backtrack(1);
return result;
}
// n=4, k=2
// Result: [[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]
def combine(n: int, k: int) -> list[list[int]]:
result: list[list[int]] = []
current: list[int] = []
def backtrack(start: int) -> None:
if len(current) == k:
result.append(current.copy())
return
# Optimization: stop if not enough elements remain
for i in range(start, n - (k - len(current)) + 2):
current.append(i)
backtrack(i + 1)
current.pop()
backtrack(1)
return result
# n=4, k=2
# Result: [[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]
# The stdlib equivalent: list(itertools.combinations(range(1, n + 1), k))
package main
import "strings"
func solveNQueens(n int) [][]string {
var result [][]string
board := make([][]byte, n)
for i := range board {
board[i] = []byte(strings.Repeat(".", n))
}
cols := map[int]bool{}
diag1 := map[int]bool{} // row - col
diag2 := map[int]bool{} // row + col
var backtrack func(row int)
backtrack = func(row int) {
if row == n {
snapshot := make([]string, n)
for i, r := range board {
snapshot[i] = string(r)
}
result = append(result, snapshot)
return
}
for col := 0; col < n; col++ {
if cols[col] || diag1[row-col] || diag2[row+col] {
continue
}
board[row][col] = 'Q'
cols[col] = true
diag1[row-col] = true
diag2[row+col] = true
backtrack(row + 1)
board[row][col] = '.'
delete(cols, col)
delete(diag1, row-col)
delete(diag2, row+col)
}
}
backtrack(0)
return result
}
static List<List<string>> SolveNQueens(int n)
{
var result = new List<List<string>>();
// C# strings are immutable, so the board rows are char arrays
var board = new char[n][];
for (int i = 0; i < n; i++)
{
board[i] = new string('.', n).ToCharArray();
}
var cols = new HashSet<int>();
var diag1 = new HashSet<int>(); // row - col
var diag2 = new HashSet<int>(); // row + col
void Backtrack(int row)
{
if (row == n)
{
result.Add(board.Select(r => new string(r)).ToList());
return;
}
for (int col = 0; col < n; col++)
{
if (cols.Contains(col) || diag1.Contains(row - col) || diag2.Contains(row + col))
{
continue;
}
board[row][col] = 'Q';
cols.Add(col);
diag1.Add(row - col);
diag2.Add(row + col);
Backtrack(row + 1);
board[row][col] = '.';
cols.Remove(col);
diag1.Remove(row - col);
diag2.Remove(row + col);
}
}
Backtrack(0);
return result;
}
def solve_n_queens(n: int) -> list[list[str]]:
result: list[list[str]] = []
# Python strings are immutable, so rows are lists of characters
board = [["."] * n for _ in range(n)]
cols: set[int] = set()
diag1: set[int] = set() # row - col
diag2: set[int] = set() # row + col
def backtrack(row: int) -> None:
if row == n:
result.append(["".join(r) for r in board])
return
for col in range(n):
if col in cols or row - col in diag1 or row + col in diag2:
continue
board[row][col] = "Q"
cols.add(col)
diag1.add(row - col)
diag2.add(row + col)
backtrack(row + 1)
board[row][col] = "."
cols.remove(col)
diag1.remove(row - col)
diag2.remove(row + col)
backtrack(0)
return result
package main
func exist(board [][]byte, word string) bool {
rows, cols := len(board), len(board[0])
var backtrack func(r, c, idx int) bool
backtrack = func(r, c, idx int) bool {
if idx == len(word) {
return true
}
if r < 0 || r >= rows || c < 0 || c >= cols ||
board[r][c] != word[idx] {
return false
}
temp := board[r][c]
board[r][c] = '#' // Mark as visited
found := backtrack(r+1, c, idx+1) ||
backtrack(r-1, c, idx+1) ||
backtrack(r, c+1, idx+1) ||
backtrack(r, c-1, idx+1)
board[r][c] = temp // Undo
return found
}
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if backtrack(r, c, 0) {
return true
}
}
}
return false
}
static bool Exist(char[][] board, string word)
{
int rows = board.Length;
int cols = board[0].Length;
bool Backtrack(int r, int c, int idx)
{
if (idx == word.Length)
{
return true;
}
if (r < 0 || r >= rows || c < 0 || c >= cols
|| board[r][c] != word[idx])
{
return false;
}
char temp = board[r][c];
board[r][c] = '#'; // Mark as visited
bool found = Backtrack(r + 1, c, idx + 1)
|| Backtrack(r - 1, c, idx + 1)
|| Backtrack(r, c + 1, idx + 1)
|| Backtrack(r, c - 1, idx + 1);
board[r][c] = temp; // Undo
return found;
}
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
if (Backtrack(r, c, 0))
{
return true;
}
}
}
return false;
}
def exist(board: list[list[str]], word: str) -> bool:
rows, cols = len(board), len(board[0])
def backtrack(r: int, c: int, idx: int) -> bool:
if idx == len(word):
return True
if not (0 <= r < rows and 0 <= c < cols) or board[r][c] != word[idx]:
return False
temp = board[r][c]
board[r][c] = "#" # Mark as visited
found = (
backtrack(r + 1, c, idx + 1)
or backtrack(r - 1, c, idx + 1)
or backtrack(r, c + 1, idx + 1)
or backtrack(r, c - 1, idx + 1)
)
board[r][c] = temp # Undo
return found
return any(
backtrack(r, c, 0) for r in range(rows) for c in range(cols)
)
## Оптимизация: Pruning (отсечение)
<?php
declare(strict_types=1);
// Without pruning: check all branches
// With pruning: cut obviously invalid branches
// Example in Combination Sum:
/**
* @param list<int> $candidates
* @return list<list<int>>
*/
function combinationSumPruned(array $candidates, int $target): array
{
sort($candidates); // Sort for pruning
$result = [];
$backtrack = function (int $start, array $current, int $remaining) use (&$backtrack, &$result, $candidates): void {
if ($remaining === 0) {
$result[] = $current;
return;
}
for ($i = $start; $i < count($candidates); $i++) {
if ($candidates[$i] > $remaining) {
break; // Pruning! All further elements are larger
}
$current[] = $candidates[$i];
$backtrack($i, $current, $remaining - $candidates[$i]);
array_pop($current);
}
};
$backtrack(0, [], $target);
return $result;
}
package main
import "sort"
// Without pruning: check all branches
// With pruning: cut obviously invalid branches
// Example in Combination Sum:
func combinationSumPruned(candidates []int, target int) [][]int {
sort.Ints(candidates) // Sort for pruning
var result [][]int
var backtrack func(start int, current []int, remaining int)
backtrack = func(start int, current []int, remaining int) {
if remaining == 0 {
tmp := make([]int, len(current))
copy(tmp, current)
result = append(result, tmp)
return
}
for i := start; i < len(candidates); i++ {
if candidates[i] > remaining {
break // Pruning! All further elements are larger
}
current = append(current, candidates[i])
backtrack(i, current, remaining-candidates[i])
current = current[:len(current)-1]
}
}
backtrack(0, []int{}, target)
return result
}
// Without pruning: check all branches
// With pruning: cut obviously invalid branches
// Example in Combination Sum:
static List<List<int>> CombinationSumPruned(int[] candidates, int target)
{
Array.Sort(candidates); // Sort for pruning
var result = new List<List<int>>();
var current = new List<int>();
void Backtrack(int start, int remaining)
{
if (remaining == 0)
{
result.Add([.. current]);
return;
}
for (int i = start; i < candidates.Length; i++)
{
if (candidates[i] > remaining)
{
break; // Pruning! All further elements are larger
}
current.Add(candidates[i]);
Backtrack(i, remaining - candidates[i]);
current.RemoveAt(current.Count - 1);
}
}
Backtrack(0, target);
return result;
}
# Without pruning: check all branches
# With pruning: cut obviously invalid branches
# Example in Combination Sum:
def combination_sum_pruned(candidates: list[int], target: int) -> list[list[int]]:
candidates.sort() # Sort for pruning
result: list[list[int]] = []
current: list[int] = []
def backtrack(start: int, remaining: int) -> None:
if remaining == 0:
result.append(current.copy())
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break # Pruning! All further elements are larger
current.append(candidates[i])
backtrack(i, remaining - candidates[i])
current.pop()
backtrack(0, target)
return result
## Шпаргалка: какой шаблон использовать
Задача
start с i+1?
Повторы?
Сортировка?
Subsets
i + 1
Нет
Нет
Subsets II
i + 1
Skip dup
Да
Permutations
Все элементы
Нет
Нет
Combinations
i + 1
Нет
Нет
Combination Sum
i (повторы)
Да
Да (pruning)
Запомни: Backtracking = рекурсия + выбор + откат. Три ключевых вопроса: 1) Что является решением? (базовый случай) 2) Какие выборы на каждом шаге? (цикл) 3) Как откатить? (pop/remove). Pruning ускоряет в разы: отсекай невалидные ветви как можно раньше.
Итоги
Backtracking = DFS по дереву решений
Шаблон: выбор -> рекурсия -> откат
Subsets: добавляй каждое состояние в результат
Permutations: используй все элементы, без start
N-Queens: sets для столбцов и диагоналей
Pruning: сортировка + break при невалидном условии
Проверь себя
В задаче N-Queens, зачем нужны три множества: cols, diag1 (row-col), diag2 (row+col)?
В задаче Combination Sum с candidates = [2, 3, 6, 7] и target = 7, почему рекурсивный вызов использует `$i` а не `$i + 1`?
Что такое pruning (отсечение) в backtracking и какой эффект оно даёт?
Как в Subsets II (с дубликатами) избегают повторных подмножеств?
Сколько подмножеств (subsets) у множества [1, 2, 3]?