Задача коммивояжёра (travelling salesman problem, TSP): дан полный взвешенный граф на $n$ вершинах. Найти гамильтонов цикл (цикл, проходящий по каждой вершине ровно один раз) минимальной длины. Расстояние между вершинами $u$ и $v$ обозначим $d_{u,v}$.
Свойства
Задача коммивояжёра NP-трудна. Всего различных маршрутов $(n-1)!$, поэтому полный перебор возможен лишь при малых $n$. Динамическим программированием задачу можно решить за $O(2^n \cdot n^2)$, что значительно быстрее $O(n!)$, хотя по-прежнему экспоненциально.
Естественная подзадача — нахождение «префикса» маршрута (первых нескольких рёбер, начиная из вершины 0). Состояние параметризуется парой $(A, v)$, где $A$ — подмножество вершин, содержащее вершину 0, $v \in A$ — номер последней вершины в префиксе.
Начальные состояния: $l[\{0\}, 0] = 0$; $l[A, 0] = \infty$ при $|A| > 1$. Переход — перебор последнего ребра:
#include<vector>usingnamespace std;
vector<int> computeSizes(int n) {
vector<int> sz(1<< n, 0);
for (int mask =1; mask < (1<< n); mask++)
sz[mask] = sz[mask >>1] + (mask &1);
return sz;
// В GCC: __builtin_popcount(mask) делает то же за O(1)
}
Сумма элементов подмножества
1
2
3
4
5
6
7
8
9
10
defsubset_sums(a: list[int]) -> list[int]:
"""s[mask] = сумма a[i] для i in mask. За O(2^n).""" n = len(a)
s = [0] * (1<< n)
x =0# номер старшего бита текущего maskfor mask in range(1, 1<< n):
if mask == (1<< (x +1)):
x +=1 s[mask] = s[mask - (1<< x)] + a[x]
return s
1
2
3
4
5
6
7
8
9
10
vector<longlong> subsetSums(vector<longlong>& a) {
int n = a.size();
vector<longlong> s(1<< n, 0);
int x =0;
for (int mask =1; mask < (1<< n); mask++) {
if (mask == (1<< (x +1))) x++;
s[mask] = s[mask - (1<< x)] + a[x];
}
return s;
}
Сумма по подмножествам (SOS DP)
Пусть для каждого подмножества $A$ дано число $f_A$; требуется для каждого $A$ вычислить $g(A) = \sum_{B \subseteq A} f_B$.
Наивный перебор всех пар $(A, B)$ работает за $O(4^n)$. Перебор только подмножеств $A$ в порядке убывания — за $O(3^n)$ (так как количество пар $(A, B)$ с $B \subseteq A$ равно $3^n$). Оптимальное решение через DP:
defsum_over_subsets(f: list[int], n: int) -> list[int]:
"""
Возвращает g[mask] = sum of f[B] for all B subset of mask.
Время O(2^n * n), память O(2^n).
""" g = list(f)
for k in range(n):
for mask in range(1<< n):
if (mask >> k) &1:
g[mask] += g[mask ^ (1<< k)]
return g
defsum_over_subsets_slow(f: list[int], n: int) -> list[int]:
"""Перебор подмножеств за O(3^n).""" g = [0] * (1<< n)
for A in range(1<< n):
g[A] = f[0] # пустое подмножество B = A
while B >0:
g[A] += f[B]
B = (B -1) & A
return g
// SOS DP: O(2^n * n)
vector<longlong> sumOverSubsets(vector<longlong>& f, int n) {
vector<longlong> g(f);
for (int k =0; k < n; k++)
for (int mask =0; mask < (1<< n); mask++)
if ((mask >> k) &1)
g[mask] += g[mask ^ (1<< k)];
return g;
}
// Перебор подмножеств: O(3^n)
vector<longlong> sumOverSubsetsSlow(vector<longlong>& f, int n) {
vector<longlong> g(1<< n, 0);
for (int A =0; A < (1<< n); A++) {
g[A] = f[0];
for (int B = A; B >0; B = (B -1) & A)
g[A] += f[B];
}
return g;
}
Свойства
Количество пар $(A, B)$ с $B \subseteq A$ равно $3^n$: каждый из $n$ элементов либо не входит в $A$ ($\notin A$), либо входит в $A \setminus B$, либо входит в $B$. SOS DP улучшает это до $O(2^n \cdot n)$ за счёт «добавления» элементов по одному.
deftsp(n: int, d: list[list[int]]) -> int:
"""
n — число вершин, d[u][v] — расстояние.
Вершина 0 — стартовая. Возвращает длину минимального гамильтонова цикла.
""" INF = float('inf')
full = (1<< n) -1# l[mask][v] = минимальная длина пути через вершины mask, заканчивающегося в v l = [[INF] * n for _ in range(1<< n)]
l[1][0] =0# начинаем в вершине 0, mask = {0} = 1for mask in range(1, 1<< n, 2): # только нечётные (содержат 0)for v in range(1, n):
ifnot (mask >> v) &1:
continue prev_mask = mask ^ (1<< v)
for u in range(n):
if u == v ornot (prev_mask >> u) &1:
continueif l[prev_mask][u] + d[u][v] < l[mask][v]:
l[mask][v] = l[prev_mask][u] + d[u][v]
return min(l[full][v] + d[v][0] for v in range(1, n))
#include<vector>#include<algorithm>#include<climits>usingnamespace std;
inttsp(int n, vector<vector<int>>& d) {
constint INF = INT_MAX /2;
int full = (1<< n) -1;
// l[mask][v] = мин. длина пути через mask, заканчивающегося в v
vector<vector<int>> l(1<< n, vector<int>(n, INF));
l[1][0] =0;
for (int mask =1; mask < (1<< n); mask +=2) { // нечётные
for (int v =1; v < n; v++) {
if (!((mask >> v) &1)) continue;
int prevMask = mask ^ (1<< v);
for (int u =0; u < n; u++) {
if (u == v ||!((prevMask >> u) &1)) continue;
if (l[prevMask][u] != INF)
l[mask][v] = min(l[mask][v], l[prevMask][u] + d[u][v]);
}
}
}
int res = INF;
for (int v =1; v < n; v++)
if (l[full][v] != INF)
res = min(res, l[full][v] + d[v][0]);
return res;
}
Восстановление ответа осуществляется стандартными методами: для каждого состояния запоминаем, из какого состояния в него был сделан оптимальный переход.