Многие задачи заключаются в нахождении одного или нескольких искомых из полного набора некоторых вариантов. Эти задачи называются задачами выбора на наборе дискретных данных. Самый простой способ решения задачи выбора - перебрать все варианты и выбрать среди них те, которые требуются по условию. Плюсом данного метода является гарантированное получение точного и полного результата. Главным минусом - большая временная сложность алгоритма при большом наборе исходных данных ## Реализация #### 1. Перебор циклами Программа представляет собой несколько вложенных циклов, которые и перебираются все варианты **Пример:** найти количество счастливых автобусных билетов ```cpp #include using namespace std; int main() { int col = 0; for (int a = 0; a <= 9; a++) for (int b = 0; b <= 9; b++) for (int c = 0; c <= 9; c++) for (int d = 0; d <= 9; d++) for (int e = 0; e <= 9; e++) for (int f = 0; f <= 9; f++) if (a + b + c == d + e + f) col++; cout << col << " " << col / 10000.0 << endl; system("pause"); return 0; } ``` Преимущества перебора циклами - простота реализации. Недостатки - часто заранее неизвестно, какое количество циклов нужно вкладывать друг в друга #### 2. P-ичный перебор Имеется $n$ объектов, каждому из которых поставлена в соответствие некоторая стоимость. Разделить объекты на 2 группы так, чтобы разность стоимости между группами была минимальной. Поскольку каждый объект может оказаться в одной из 2 куч, закрепим за каждым из объектов двоичное число - 0 или 1. Если количество объектов $N$ большое, для полного перебора необходимо просмотреть все комбинации $N$-значных двоичных чисел от $N$ нулей до $N$ единиц. Всего вариантов - $2^N$. Для реализации помимо массива объектов можно завести массив нулей и единиц, считая его двоичным числом, на каждом шаге при выборе нового варианта прибавляя к этому числу 1. Данная задача решена методом полного двоичного перебора: ```cpp #include void plus(int* m, int n) { int i = n; while (m[i] == 1) m[i--] = 0; m[i] = 1; } int main() { const int n = 5; int min = 15; int m[n + 1] = { 0,1,2,3,4,5 }, bin[n + 1] = { 0,0,0,0,0,0 }, itog[n + 1] = { 0,0,0,0,0,0 }; while (bin[0] == 0) { int s1 = 0, s2 = 0; for (int i = 1; i <= n; i++) if (bin[i] == 0) s1 += m[i]; else s2 += m[i]; if (abs(s1 - s2) < min) { min = abs(s1 - s2); for (int j = 1; j <= n; j++) if (itog[j] == 0) { std::cout << m[j] << " "; { std::cout << std::endl; std::cout << "2 group: "; } } for (int j = 1; j <= n; j++) { if (itog[j] == 1) std::cout << m[j] << " "; { std::cout << std::endl; std::cout << "2 group: "; } } } plus(bin, n); } std::cout << min << std::endl; } ``` В общем случае перебор может быть P-ичным. Пример: числа 1 2 3 4 5 6 7 8 записаны один за другим. Расставить между ними знаки $+$, $-$, $*$, $/$ или ничего так, чтобы в итоге получилось заданное число $N$. Задачу можно решить полным пятеричным перебором. Количество вариантов - $5^8$. #### 3. Рекурсивный перебор Дан квадратный числовой массив. Найти такой путь из левого верхнего угла массива в правый нижний, чтобы сумма чисел по данному пути была максимальной. Из каждого элемента массива допустимо двигаться только вправо или вниз | 2 | 1 | 1 | 1 | | ---- | ---- | ---- | ---- | | 1 | 2 | 1 | 1 | | 1 | 1 | 2 | 1 | | 1 | 1 | 1 | 2 | ```cpp #include using namespace std; int Max = 0; void poisk(int** m, int n, int i, int j, int s) { s += m[i][j]; if (i == n - 1 && j == n - 1 && s > Max) Max = s; if (i < n - 1) poisk(m, n, i + 1, j, s); if (j < n - 1) poisk(m, n, i, j + 1, s); } int main() { const int n = 10; int m[n][n] = { {1,1,1,1,1}, {1,1,1,1,1}, {1,2,1,1,1}, {1,1,1,2,1}, {1,1,1,1,1} }; int** a = new int* [n]; for (int i = 0; i < n; i++) { a[i] = new int[n]; for (int j = 0; j < n; j++) { a[i][j] = 1; //m[i][j]; } } poisk(a, n, 0, 0, 0); cout << Max << endl; system("pause"); return 0; } ``` Основной плюс функции рекурсивного перебора в ее простоте и компактности. Минусы рекурсивного перебора - возможное переполнение стека и медленная работа.