lower_bound и upper_bound - встроенные функции бинарного поиска.

lower_bound - функция, которая за логарифмическое время в отсортированном массиве находит наименьший элемент, который больше заданного значения k или равен ему.
В качестве аргументов принимает границы массива и значение k.
Возвращает итератор на найденный элемент или на конец (невключенный) массива, если такого элемента не существует.
Подробнее можете прочитать в документации.

upper_bound - функция, которая за логарифмическое время в отсортированном массиве находит наименьший элемент, который строго больше заданного значения k.
В качестве аргументов принимает границы массива и значение k.
Возвращает итератор на найденный элемент или на конец (невключенный) массива, если такого элемента не существует.
Подробнее можете прочитать в документации.

Стоит уточнить, что применение данных функций на set или multiset работает не за логарифмическое время по причине отсутствия у вышеупомянутых коллекций random access итераторов.
Однако у этих коллекций есть соответствующие встроенные методы (то есть нужно использовать их "через точку").

Примеры:
 
	vector a = { 0, 1, 3, 5, 7 };
	vector::iterator it;
	
	it = lower_bound(a.begin(), a.end(), 4);
	// *it == 5

	it = lower_bound(a.begin(), a.end(), 5);
	// *it == 5

	it = lower_bound(a.begin(), a.end(), 8);
	// it == a.end()
	
	it = upper_bound(a.begin(), a.end(), 4);
	// *it == 5

	it = upper_bound(a.begin(), a.end(), 5);
	// *it == 7

	it = upper_bound(a.begin(), a.end(), -1);
	// *it == 0


	// путем вычета итераторов можно получить индекс найденного элемента
	int ind = lower_bound(a.begin(), a.end(), 4) - a.begin();
	// ind == 3


	// нужно использовать методы вместо функций для set и аналогичных коллекций

	set s{ 1, 3, 5 };
	set::iterator sit;

	sit = s.lower_bound(3);
	// *sit == 3

	sit = s.upper_bound(3);
	// *sit == 5
 

unique - функция, которая за линейное время сжимает все последовательности одинаковых подряд идущих элементов в один.
В качестве аргумента ей передаются границы массива, в пределах которых необходимо применить сжатие.
Возвращается же итератор на новый конец (не включительно) массива. Стоит быть внимательным с элементами, расположенными после нового конца, но до старого, так как они будут иметь неопределенное значение.
Подробнее можете прочитать в документации.

Если вы применяете эту функцию на vector, то удобно делать resize, используя возвращаемый результат (подробнее ниже).

Примеры:
 
	vector  a = { 3, 3, 3, 2, 3, 3, 1, 1, 4, 5, 5 };
	unique(a.begin(), a.end());
	// a = [3, 2, 3, 1, 4, 5, ?, ?, ?, ?, ?]


	// с помощью функции unique удобно делать
	// вспомогательный массив для сжатия координат

	a = { 235, 10, 41, 10, 41, 41, 235, 500, 500 };
	sort(a.begin(), a.end());
	// a = [10, 10, 41, 41, 41, 235, 235, 500, 500]

	a.resize(unique(a.begin(), a.end()) - a.begin());
	// a = [10, 41, 235, 500]
 

merge - функция, которая объединяет два отсортированных массива, а именно, за линейное время получает отсортированный массив, который состоит из элементов первого и второго массива.
Она принимает 5 аргументов: по две границы для каждого массива и левая граница места назначения (где будут расположены элементы результирующего массива).
Подробнее можете ознакомиться в документации.

Примеры:
	// исходные массивы должны быть отсортированы
	vector a = { 1, 3, 5, 7 };
	vector b = { 2, 4, 6 };

	// необходимо, чтобы место назначения имело достаточный размер
	vector c(7); 
	
	merge(a.begin(), a.end(), b.begin(), b.end(), c.begin());
	// c = [1, 2, 3, 4, 5, 6, 7]


	// элементы могут повторяться
	a = { 1, 2, 4, 4};
	b = { 2, 3, 3, 3, 4, 4 };
	c.resize(10);

	merge(a.begin(), a.end(), b.begin(), b.end(), c.begin());
	// c = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
 Данную функцию очень удобно использовать в контексте сортировки слиянием.

nth_element - функция, которая позволяет находить n-ый элемент в массиве в отсортированном порядке за линейное время.
Функция принимает левую границу массива, итератор на позицию, значение которой в отсортированном порядке необходимо найти и правую границу массива.
После применения функции на указанном по итератору месте будет находиться необходимое значение, при этом остальные значения приобретут хаотичный порядок, но левее n-ого будут значения не больше его, а правее не меньше. То есть стоит понимать, что эта функция рушит исходный порядок элементов.
Подробнее можете прочитать в документации (https://www.cplusplus.com/reference/algorithm/nth_element/).

Пример:
	vector a = { 4, 0, 3, 9, 2, 1, 8, 5, 6, 7 };
	
	// ищем элемент по индексу 4
	// обратите внимание на порядок аргументов
	nth_element(a.begin(), a.begin() + 4, a.end());
	// a = [#, #, #, #, 4, $, $, $, $, $]
	// где # <= 4 и 4 <= $
 

Перебор всех перестановок

 
В комбинаторике перестановкой называется упорядоченный набор элементов, составленный из элементов исходного множества без повторений


Перестановка, о которой мы здесь поговорим, заключается в том, как расположить n объектов в n позициях. Причем, в перестановке важен порядок элементов.

Как перечислить все перестановки?
Начнем с наименьшего числа (с 1) и далее будем получать все остальные перестановки из оставшихся чисел. Потом возьмем следующее после минимального и опять будем отавлять перестановки из оставшихся чисел по такому же принципу. 
Например, нам необходимо перебрать все перестановки длиной 3 из первых трех букв английского алфавита.  По описанному выше принципу перечисления перестановок, выглядеть список будет таким образом:
ABC
ACB 
BAC 
BCA 
CAB
CBA

Всего перестановк из трех элементов 6.
Для n объектов количество перестановок равно Pn = n!.

Рассмотрим другой полезный подход к решению задачи перебора. Идея заключается в том, чтобы создать глобальный массив, назовем его used, который будет содержать информацию о том, использовали ли мы каждое число или нет. Когда мы рассматриваем очередной элемент для добавления в перестановку, мы проверяем значение в used для этого элемента. Если оно равно false, то это значит, что число ещё не использовалось, и мы можем включить его в перестановку. Затем мы помечаем этот элемент как использованный, устанавливая соответствующий флаг в used на значение true.

Таким образом, мы гарантируем, что каждое число будет использовано только один раз в перестановке, и избегаем повторений.

Запишем алгоритм на псевдокоде

алгоритм generate(n, used, pref)
    если длина(pref) = n то
        check(pref) 
    иначе
        для каждого next от 1 до n выполнить
            если not used[next] то
                used[next] = True
                generate(n, used, pref + [next])
                used[next] = False
            конец если
        конец цикла
    конец если
конец алгоритма

В программе выше используется алгоритм generate, который принимает три параметра: n (максимальное число в перестановке), used (массив, отслеживающий использованные каждого числа) и pref (текущий префикс перестановки).  Если префикс имеет длину n, значит мы добавили в перестановку все числа. Вызываем процедуру check  для обработки перестановки.
Иначе, для каждого числа next от 1 до (включительно), мы проверяем, было ли число использовано ранее (проверка not used[next]). Если число не использовалось, помечаем его как использованное (used[next] = True), рекурсивно вызываем алгоритм generate с обновленными значениями used и pref, и затем освобождаем число для следующей ветки (следующей перестановки), помечая его как неиспользованное (used[next] = False).

Реализуйте данный алгоритм на знакомом вам языке программирования!