Два указателя
Количество треугольников
Посчитать количество троек, из которых можно составить треугольник
Постановка задачи
Описание (LeetCode)
Дан массив целых чисел nums. Каждый элемент массива обозначает длину отрезка.
Необходимо вернуть количество троек индексов (i, j, k), из которых можно
составить треугольник.
Тройка считается корректной, если отрезки с длинами nums[i], nums[j] и
nums[k] могут быть сторонами треугольника.
Пример 1:
nums = [2, 2, 3, 4]
Результат:
3
Пояснение:
Корректные треугольники:
[2, 3, 4] (используем первую двойку)
[2, 3, 4] (используем вторую двойку)
[2, 2, 3]
Первый и второй треугольники отличаются индексами, потому что в массиве есть два
числа 2.
Пример 2:
nums = [4, 2, 3, 4]
Результат:
4
Ограничения:
1 <= nums.length <= 10000 <= nums[i] <= 1000
Решение
На первый взгляд задача выглядит как простой перебор всех троек. Можно
использовать три вложенных цикла, проверить каждую комбинацию сторон и посчитать
подходящие треугольники. Такое решение будет работать за O(n³).
При n = 1000 это уже слишком много: количество троек будет порядка миллиарда.
На интервью такой подход можно предложить как стартовый, но нужно сразу перейти
к оптимизации.
Главное наблюдение здесь связано с неравенством треугольника. Для того, чтобы из
трех отрезков a, b и c мы могли составить треугольник, должны выполняться
условия:
a + b > c
a + c > b
b + c > a
Например из сторон a = 5, b = 4, c = 8 можно составить треугольник, так
как выполняется неравенство треугольника:
5 + 4 > 8
5 + 8 > 4
4 + 8 > 5
Из сторон a = 5, b = 3, c = 8 треугольник составить нельзя, так как
нарушается одно из условий:
5 + 3 > 8 - ложно
5 + 8 > 3
3 + 8 > 5
Заметим, что если отсортировать стороны так, что a <= b <= c, то достаточно
проверить только одно условие:
a + b > c
Остальные два условия выполняются автоматически, потому что c - самая длинная
сторона.
Идея решения:
Сначала сортируем массив. После этого будем фиксировать самую длинную сторону
треугольника c и искать для нее пары меньших сторон a и b.
Пусть k - индекс самой длинной стороны. Тогда все возможные стороны a и b
лежат левее k, потому что массив отсортирован.
Для каждого k ставим два указателя:
lв начало массиваrпрямо передk
Теперь проверяем сумму nums[l] + nums[r].
Если nums[l] + nums[r] > nums[k], значит, треугольник корректен. Более того,
все элементы между l и r тоже подойдут в качестве левой стороны вместе с
nums[r] и nums[k].
Почему? Потому что массив отсортирован. Если самый маленький кандидат nums[l]
уже дает корректный треугольник с nums[r], то любой элемент правее l будет
не меньше и тоже даст корректный треугольник.
Значит, мы можем добавить к ответу сразу r - l треугольников и сдвинуть r
влево, чтобы попробовать следующую сторону b.
Если же nums[l] + nums[r] <= nums[k], текущая сумма слишком маленькая.
Сдвигать r влево бессмысленно, потому что сумма станет только меньше. Поэтому
нужно увеличить левую сторону и сдвинуть l вправо.
В этом и заключается паттерн двух указателей: после сортировки мы не перебираем все пары заново, а используем монотонность массива, чтобы сдвигать ровно тот указатель, который может улучшить ситуацию.
Основные шаги:
-
Сортируем массив
numsпо неубыванию. -
Фиксируем самую длинную сторону
nums[k].- Идем индексом
kсправа налево, начиная с конца массива. - Для каждого
kрассматриваем только элементы левее него.
- Идем индексом
-
Запускаем два указателя внутри левой части массива.
l = 0r = k - 1
-
В цикле: пока
l < r.- Если
nums[l] + nums[r] > nums[k], добавляемr - lк ответу и сдвигаемrвлево. - Иначе сдвигаем
lвправо.
- Если
-
Завершение цикла:
- Когда указатели встретились, все пары для текущего
kобработаны. - Переходим к следующему
k.
- Когда указатели встретились, все пары для текущего
Разберем пример:
nums = [2, 2, 3, 4]
Массив уже отсортирован.
-
Фиксируем
k = 3, то естьnums[k] = 4.l = 0,r = 2- Проверяем
nums[l] + nums[r] > nums[k]:2 + 3 > 4 - Условие выполняется, значит, все элементы от
lдоr - 1подходят вместе сnums[r]иnums[k]. - Добавляем
r - l = 2треугольника. - Это тройки
[2, 3, 4]и[2, 3, 4]с разными индексами. - Сдвигаем
r: стало1.
-
Продолжаем для
k = 3.l = 0,r = 1- Проверяем
2 + 2 > 4 - Условие не выполняется.
- Сдвигаем
l: стало1. - Указатели встретились, переходим к следующему
k.
-
Фиксируем
k = 2, то естьnums[k] = 3.l = 0,r = 1- Проверяем
2 + 2 > 3 - Условие выполняется.
- Добавляем
r - l = 1треугольник. - Это тройка
[2, 2, 3]. - Сдвигаем
r: стало0.
-
Завершение.
- Всего получили
3корректных треугольника.
- Всего получили
Итог: ответ равен 3.
Оценка сложности
Временная сложность
Сначала мы сортируем массив за O(n log n). Затем для каждого фиксированного
индекса k запускаем два указателя, которые вместе проходят левую часть массива
за линейное время.
В сумме вложенный проход дает O(n²), поэтому итоговая временная сложность
равна O(n²).
Пространственная сложность
Мы не создаем дополнительных структур данных для подсчета ответа. Если не
учитывать память, которую может использовать встроенная сортировка, получаем
O(1).
Код решения
Приведем код решения.
import "sort"
func triangleNumber(nums []int) int {
sort.Ints(nums)
n := len(nums)
count := 0
for k := n - 1; k >= 2; k-- {
l := 0
r := k - 1
for l < r {
if nums[l] + nums[r] > nums[k] {
count += r - l
r--
} else {
l++
}
}
}
return count
}Мы рекомендуем самостоятельно решить похожую задачу: 3Sum. Там идея тоже строится на сортировке и двух указателях, но вместо подсчета количества комбинаций нужно аккуратно собрать сами тройки и не добавить дубликаты.
Итоги
Вот основные итоги по задаче:
-
Сортировка упрощает проверку. После сортировки достаточно проверить только
nums[l] + nums[r] > nums[k], гдеnums[k]- самая длинная сторона. -
Два указателя убирают лишний перебор. Вместо проверки всех пар для каждого
kмы двигаемlиrнавстречу друг другу. -
Главный момент алгоритма. Когда
nums[l] + nums[r] > nums[k], можно добавить сразуr - lтреугольников, потому что все элементы междуlиrтоже достаточно большие. -
Граничные случаи. Нули не требуют отдельной обработки. После сортировки они просто не пройдут проверку
a + b > c, если не могут быть стороной корректного треугольника.