Сортировка
Пары с удвоением
Проверить, можно ли разбить массив на пары x и 2x
Постановка задачи
Описание (LeetCode - 954. Array of Doubled Pairs)
Дан целочисленный массив arr четной длины.
Нужно проверить, можно ли переупорядочить массив так, чтобы для каждого i
выполнялось:
arr[2 * i + 1] = 2 * arr[2 * i]
Другими словами, каждый элемент должен образовать пару со своим удвоенным значением.
Пример 1:
arr = [3, 1, 3, 6]
Результат:
false
Пояснение:
Можно составить пару (3, 6), но для второй 3 пары нет.
Пример 2:
arr = [2, 1, 2, 6]
Результат:
false
Пояснение:
Можно составить пару (1, 2), но оставшиеся 2 и 6 не образуют пару с
удвоением.
Пример 3:
arr = [4, -2, 2, -4]
Результат:
true
Пояснение:
Можно составить пары (-2, -4) и (2, 4).
Ограничения:
2 <= arr.length <= 3 * 10⁴arr.lengthчетная-10⁵ <= arr[i] <= 10⁵
Решение
Можно попробовать перебрать все пары чисел и проверить, что одно из них равно
удвоенному другому. Таких проверок O(n²), но список подходящих пар еще не даст
решения. Каждый элемент можно использовать только один раз, а одно число часто
входит сразу в несколько пар. Пары нельзя выбирать независимо: выбор одной
блокирует другую, и пришлось бы отдельно искать набор пар на весь массив.
Например, для массива [1, 2, 4, 8] подходят пары (1, 2), (2, 4) и (4, 8). Если взять (2, 4), то 1 и 8 останутся без пары. Подходит только набор
(1, 2) и (4, 8). Число таких наборов растет экспоненциально, а по условию
длина массива может доходить до 3 * 10⁴. Поэтому такое решение неэффективно, и
нужно искать другое.
Идея решения:
Попробуем отсортировать массив по возрастанию и для каждого числа x искать
пару 2x. Для быстрой проверки, есть ли в массиве 2x, мы можем хранить
частоты элементов в хеш-таблице.
Однако с отрицательными числами возникает проблема: для -2 удвоенное значение
равно -4, а -4 стоит левее -2. Если обрабатывать числа по возрастанию, то
мы сначала увидим -4 и будем искать -8.
Поэтому будем сортировать по модулю: тогда сначала мы возьмем -2 и будем
искать для него -4. Идем по массиву, берем текущее число x. Если частота x
равна нулю, то это число уже использовано, пропускаем его. Иначе ищем 2x. Если
2x есть, уменьшаем частоты x и 2x. Если нет, возвращаем false: разбить
массив на пары невозможно.
Основные шаги:
- Считаем частоты всех чисел в
arr. - Сортируем
arrпоabs(x)по возрастанию. - Идем по числам в этом порядке.
- Если частота
xравна0, значит, число уже использовано, пропускаем его. - Уменьшаем частоту
x. - Проверяем, есть ли доступное число
2x. - Если нет, возвращаем
false. - Если есть, уменьшаем частоту
2x. - Если все числа удалось разложить по парам, возвращаем
true.
Рассмотрим пример:
arr = [4, -2, 2, -4]
После сортировки по модулю:
[-2, 2, 4, -4]
Частоты:
-4: 1
-2: 1
2: 1
4: 1
Составляем пары:
- Берем
-2, ищем-4. Такое число есть, получаем пару(-2, -4).
- Берем
2, ищем4. Такое число есть, получаем пару(2, 4).
- Остальные элементы уже использованы.
Ответ равен true.
В задачах с отрицательными числами обычная сортировка по значению часто меняет смысл жадного выбора. Здесь нам важен не порядок чисел на числовой прямой, а их абсолютная величина, поэтому мы сортируем по абсолютному значению.
Оценка сложности
Временная сложность
Подсчет частот занимает O(n). Сортировка массива длины n по модулю занимает
O(n log n). После этого мы делаем один проход.
Итоговая временная сложность: O(n log n).
Пространственная сложность
Мы храним хеш-таблицу частот, в которой может быть до n разных чисел.
Пространственная сложность равна O(n).
Код решения
Приведем код решения.
func canReorderDoubled(arr []int) bool {
count := map[int]int{}
for _, num := range arr {
count[num]++
}
sort.Slice(arr, func(i, j int) bool {
return abs(arr[i]) < abs(arr[j])
})
for _, num := range arr {
if count[num] == 0 {
continue
}
count[num]--
doubled := num * 2
if count[doubled] == 0 {
return false
}
count[doubled]--
}
return true
}
func abs(num int) int {
if num < 0 {
return -num
}
return num
}Итоги
Задача Array of Doubled Pairs - это пример того, как сортировка задает порядок выбора, а хеш-таблица помогает проверять доступность нужной пары.
- Суть алгоритма: мы считаем частоты, сортируем числа по абсолютному
значению и для каждого еще не использованного
xпытаемся найти одно число2x. - Эффективность:
- Временная сложность:
O(n log n), потому что основное время занимает сортировка. - Пространственная сложность:
O(n), потому что мы храним частоты чисел.
- Временная сложность:
- Граничные случаи:
- Отрицательные числа: нужна сортировка по модулю, иначе пары такие как
(-2, -4)будут обрабатываться в неправильном порядке. - Нули: каждый
0должен найти второй0, поэтому нечетное количество нулей приведет кfalse. - Повторы: частоты позволяют не использовать один и тот же элемент в двух разных парах.
- Отрицательные числа: нужна сортировка по модулю, иначе пары такие как
- Оптимальное решение: сначала фиксируем правильный порядок обработки, а затем строим пары. Когда мы идем от меньшего модуля к большему, текущее число уже не может быть удвоением какого-то еще не обработанного меньшего числа.