Meet-in-the-middle
Оглавление
Запись занятия
Четверка с суммой ноль
В массиве размера $n$ ($n \leq 2000$) необходимо найти четыре числа, сумма которых равна $0$ (числа могут быть использованы несколько раз).
Решение
Четыре числа $a, b, c, d$ с суммой $0$ прекрасно деляется на две части: $a+b = -(c+d)$. То есть если мы зафиксировали сумму первых двух чисел, то мы знаем, какой должна быть сумма вторых двух чисел. Это позволяет решить задачу за $O(n^2 \log n)$:
- Сохраним суммы всех пар во множестве за $O(n^2 \log n)$
- Переберем все пары ещё раз и проверим, есть ли пара чисел с противоположной суммой. Этот шаг тоже работает за $O(n^2 \log n)$
Количество возрастающих подпоследовательностей длины 3
Дана последовательность $a_1, a_2, \ldots, a_n$. Найдите количество возрастающих подпоследовательностей длины $3$.
Решение для $n \leq 2000$
Будем перебирать центральный элемент в последовательности. Тогда пусть центральный элемент равен $a_i$, сколько последовательностей с таким элементов в середине есть? То есть нужно найти количество пар $(j, k)$: $j < i < k$ и $a_j < a_i \leq a_k$. Но по сути задачи для $j$ и $k$ не зависят друг от друга, поэтому можно независимо перебрать количество подходящих $j$ и $k$ суммарно за $O(n)$.
Бонус: решите задачу при ограничениях $n \leq 10^5$.
Часто техника meet-in-the-middle (дословно, «встреча в середине») применяется и в задачах с полным перебором. Рассмотрим какую-нибудь задачу на перебор подмножеств, которую мы умеем решать за $O(2^n \cdot poly(n))$, где $poly(n)$ — какой-то полином от размера задачи $n$. Метод meet-in-the-middle позволяет соптимизировать перебор до $O(2^{n/2} \cdot poly(n))$ в большом классе таких задач.
Задача о рюкзаке
Рассмотрим задачу о рюкзаке — нужно выбрать подмножество $a_i$ с суммарным весом $w$, однако в нашей задаче веса предметов могут быть большими: $1 \leq a_i \leq 10^9$.
Решение для $n \leq 25$
Тогда перебор всех подмножеств можно осуществить следующим образом:
1
2
3
4
5
6
7
8
9
10
11
bool find_subset(int *a, int n, int w)
for (int mask = 0; mask < (1 << n); mask++) {
int s = 0;
for (int i = 0; i < n; i++)
if (mask >> i & 1)
s += a[i];
if (s == w)
return true;
}
return false;
}
Здесь мы перебираем все подмножества и каждое проверяем за $O(n)$, что дает асимптотику $O(2^n \cdot n)$.
В теории можно избавиться от проверки за $O(n)$, если перебирать маску рекурсивно и поддерживать текущую сумму на префиксе, возможно добавляя во время спуска только один элемент. Однако мы погонимся за более мощной оптимизацией.
Решение для $n \leq 50$
Разделим массив на две части. Заметим, что искомое подмножество имеет какое-то количество элементов из левой половины и какое-то количество элементов из правой (возможно, нулевое). Попытаемся посчитать все суммы слева и справа по отдельности и найти пару, дающую нужную общую сумму.
Сначала посчитаем суммы для всех подмножеств среди первых $l = \lfloor \frac{n}{2} \rfloor$ элементов и положим в хеш-таблицу:
1
2
3
4
5
6
7
8
9
10
11
unordered_set<int> t;
int l = n / 2;
for (int mask = 0; mask < (1 << l); mask++) {
int s = 0;
for (int i = 0; i < n; i++)
if (mask >> i & 1)
s += a[i];
t.insert(s);
}
Затем переберем все суммы среди оставшихся $r = n - l = \lceil \frac{n}{2} \rceil$ элементов и для каждой попытаемся найти подходящую половину (с суммой $s_l = w - s_r$) через предподсчитанную хеш-таблицу:
1
2
3
4
5
6
7
8
9
10
int r = n - l;
for (int mask = 0; mask < (1 << r); mask++) {
int s = 0;
for (int i = 0; i < r; i++)
if (mask >> i & 1)
s += a[l + i];
if (t.count(w - s))
return true;
}
Обе фазы (а значит и сам алгоритм) работают за $O(2^{n/2} \cdot n)$: мы перебираем $2^{n/2}$ подмножеств и для каждого за $O(n)$ считаем сумму и делаем запрос добавления / проверки наличия в хеш-таблицу за $O(1)$.
Заметим, что оба перебора всё ещё можно так же соптимизировать в $O(n)$ раз через трюк с рекурсией.
Модификации рюкзака
Можно ли разделить предметы на четыре кучки равного веса за $O(n2^n)$?
Можно ли разделить предметы на $k$ кучек равного веса за $O(n2^n)$?
Количество клик
Дан граф из $n$ $(n \leq 40)$ вершин. Нужно найти количество клик — подграфов, в котором все вершины связаны со всеми.
Решение
Сначала научимся решать задачу полным перебором. Пусть у нас есть матрица смежности графа. Как быстро проверить, что подмножество вершин $m$ является кликой?
За $O(n^2)$ можно пройтись по всем парам включенных вершин и для каждой проверить, есть ли единичка в матрице смежности. Проверку можно соптимизировать до $O(n)$, посчитав маску $m’$, равную побитовому «И» строчек матрицы смежности, соответствующих вершинам $m$. Теперь, если $m$ является подмножеством $m’$, то есть
\[m \; \& \; m' = m\]то подграф $m$ является кликой: для всех его вершин есть ребро из всех других.
Воспользуемся этим трюком для слияния ответов в meet-in-the-middle. Разделим граф на две части, найдем для левой все клики и пометим их маски единицами в специальном массиве is_clique[mask]
размера $2^{n/2}$.
Теперь будем перебирать подграфы второй половины, и для каждой клики нам нужно найти количество клик левой половины, являющихся подграфами пересечения списков смежности для правой половины ($m’$ из проверки выше).
Чтобы сделать это быстро, предподсчитаем поверх массива is_clique
динамику «как много подмасок данной маски являются кликами». Эту динамику можно посчитать за $O(2^{n/2})$, если для каждой маски $m$ рассмотреть два варианта — когда первая вершина $v$ включена в клику и когда не включена:
Итоговая асимптотика алгоритма будет $O(2^{n/2} \cdot n)$.
Взломай хеш
Найдите строку $s$ из строчных латинских букв с полиномиальным хешом, равным $X$, где основание хеша равно $26$, а модуль $M$ ($M \leq 10^9$). Напомним, что полиномиальных хеш вычисляется по формуле: \(h(s_1s_2\ldots s_n) = (s_1 + 26 \cdot s_2 + 26^2 \cdot s_3 + \ldots + 26^{n-1}\cdot s_n) \% M\)
Решение
Соберем случайные $10^6$ строк небольшой длины. Все их хеши - случайные числа. Конечно, вероятность попасть так в нужный хеш размера $10^9$ стремится к нулю, поэтому трюк:
Искомая строка будет иметь вид $t_i + t_j$, где $t_i, t_j$ из нашего множества строк. И вот теперь то мы уже с вероятностью 1 соберем нужных хеш. Почему? Потому что пар $t_i+t_j$ порядка $10^12$, то есть количество строк увеличилось в квадрат раз. А вероятность того, что мы не попали этими $10^12$ строками в нужный $X$ стремится к нулю.
Такое решение работает за $O(10^6 \log 10^6)$, потому что если зафиксировать $t_i$, то мы знаем, чем должен быть равен хеш $t_j$, то есть явно перебирать все пары нам не надо - надо лишь проверить наличие строки с заданным хешом во множестве.
Подпоследовательсность с максимальной суммой по модулю
Дана последовательность $a_1, a_2, \ldots, a_n$ ($n \leq 40$) целых неотрицательных чисел. Необходим выбрать такую подпоследовательность $a_{i_1}, a_{i_2}, \ldots, a_{i_k}$, что сумма $(a_{i_1} + a_{i_2} + \ldots + a_{i_k}) \% M$ ($M \leq 10^9$)максимальна.
Решение
Разобьём последовательность на две части по $\frac{n}{2}$ элементов. В каждой из них вычислим суммы всех возможных подпоследовательностей за $O(2^n)$. Для каждой возможной суммы $x$ из первой половины найдем оптимальное подмножество с суммой $y$ из второй половины так, чтобы $(x+y)\%m$ было как можно больше. Есть два случая:
- $x + y < m$, тогда нас интересует максимальный $y: y < m - x$. Такой $y$ можно найти в во множестве за $\log$ размера множества, то есть за $O(n)$.
- $x + y \geq m$, тогда нас просто интересует максимальный $y$ без каких-либо ограничений
То есть итоговое решение работает за $O(2^{\frac{n}{2}}n)$ и в нём для каждого $x$ мы ищем лучшее $y$.
Код
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <vector>
#include <cassert>
#include <set>
using namespace std;
int m;
int a[40];
void rec(int s, int i, int n, vector<int>& ans) {
if (i == n) {
ans.push_back(s);
return;
}
rec(s, i + 1, n, ans);
rec((s + a[i]) % m, i + 1, n, ans);
}
int main() {
int n;
cin >> n >> m;
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
vector<int> left, right;
rec(0, 0, n / 2, left);
rec(0, n / 2, n, right);
set<int> lefts(left.begin(), left.end());
int best_ans = 0;
for (int x : right) {
best_ans = max(best_ans, (*left.rbegin() + x) % m);
// y: y < m - x, y -> max
auto it = lefts.lower_bound(m - x);
if (it != lefts.begin()) {
best_ans = max(best_ans, x + *prev(it));
}
}
cout << best_ans;
return 0;
}
Сделай их похожими
Назовем два числа похожими, если в их двоичных представлениях одинаковое количество единиц. Вам задан массив из $n$ ($n \leq 100$) целых чисел $a_1, a_2, \ldots, a_n$ ($a_i \leq 10^9$). Вы должны выбрать неотрицательное целое число $x$, после чего вы получите новый массив из $n$ чисел $b_1, b_2, \ldots, b_n$, где $b_i = a_i \oplus x$. Можно ли получить такой массив $b$, в котором все числа похожи друг на друга?
Решение
Если бы $a_i \leq 2^{15}$, то можно было бы перебрать все $x$ и в лоб проверить. Но $a_i$ большие, исправим это: разобьем $a_i$ на две части: $a_i = p_i + 2^{15} \cdot q_i$, где $p_i, q_i \leq 2^15$. Пусть так же $x = x_p + 2^{15} x_q$
Если я зафиксирую $x_p$, то какие ограничения можно наложить на $x_q$? Для всех $i$ должно выполняться:
\[cnt_1(p_1 \oplus x_p) + cnt_1(q_1 \oplus x_q) = cnt_1(p_i \oplus x_p) + cnt_1(q_i \oplus x_q)\]Перенесём всё что с $x_q$ в одну часть, всё что с $x_p$ - в другую:
\[cnt_1(p_i \oplus x_p) - cnt_1(p_1 \oplus x_p) = cnt_1(q_i \oplus x_q) - cnt_1(q_1 \oplus x_q)\]Таким образом мы можем вычислить вектор из значений $cnt_1(p_i \oplus x_p) - cnt_1(p_1 \oplus x_p)$ и искать подходящий вектор из такого же вида из $q$ (во множестве). Таким образом асимптотика $O(A_{\max} \cdot n \cdot \log (A_{\max}))$