- •История изменений
- •Благодарности
- •Основы
- •Привет, Мир!
- •Ввод-вывод
- •Целые числа
- •Символы и строки
- •String
- •Перевод строки в целое число
- •Перевод целого числа в строку
- •Случайные числа
- •Профилирование
- •Массивы и матрицы
- •Объявление, размещение и инициализация массивов
- •Ввод массива
- •Вывод массива
- •Valarray
- •Vector
- •Матрицы
- •Элементарные алгоритмы
- •Абсолютное значение целого числа
- •Минимум и максимум среди двух чисел
- •Минимум и максимум среди трёх чисел
- •Сортировка массива из трёх чисел
- •Циклический сдвиг массива из трёх элементов
- •Разложение целого числа на его цифры
- •Линейный поиск
- •Рекурсия
- •Более сложные алгоритмы
- •Бинарный поиск
- •Циклический сдвиг массива
- •Подводные камни
- •Диграфы и триграфы
Симоненко Евгений А. Олимпиадная подготовка по программированию |
25 |
for (int i = 2; i <= n; i++) { res *= i;
}
return res;
}
Если расписать формулу факториала: n !=1 2 ... (n−1) n ,
ипосмотреть на неё с другой стороны: n !=n (n−1) ... 2 1 ,
то можем заметить, что: n !=n (n−1)! .
Полученная формула называется рекуррентной, и к ней применим метод рекурсии, который основан на вызове функции самой себя:
int f(int n) {
if (n == 0) { return 1;
}
return n * f(n - 1);
}
Как видно, код стал намного короче и проще для понимания. Следует обратить пристальное внимание на обязательное условие окончания рекурсии, без которого она никогда не закончится (условно говоря, так как она не может не закончиться, потому что рекурсия активно использует стек, который не безконечен).
Рекурсия часто применяется при работе с графами, в частности, методом рекурсии легко может быть реализован поиск в глубину (DFS).
БОЛЕЕ СЛОЖНЫЕ АЛГОРИТМЫ
Здесь рассматриваются часто встречающиеся несложные алгоритмы в основном для массивов.
БИНАРНЫЙ ПОИСК
Бинарный поиск применяется для упорядоченных массивов. Идея алгоритма заключается в сужении диапазона поиска в два раза в каждой итерации, что на упорядоченных массивах даёт существенно лучшую скорость поиска чем при использовании линейного поиска. Другие названия метода: двоичный поиск, дихотомия. Метод также может применяться для решения некоторых задач для монотонных функций.
Рассмотрим метод на простейшем примере поиска элемента с заданным значением. Пусть n – количество элементов массива, x – сам массив, а y – заданное значение для поиска, тогда код бинарного поиска может выглядеть так:
#include <cstdio>
int main(int argc, char* arv[]) { int n = 0;
scanf("%d", &n);
int *x = new int[n + 1];
for (int i = 0; i < n; i++) { scanf("%d", &x[i]);
}
int y = 0; scanf("%d", &y);
int l = 0, r = n - 1; while (l < r) {
int m = (l + r) /2;
26 Симоненко Евгений А. Олимпиадная подготовка по программированию
if (x[m] < y) {
l = m + 1; } else {
r = m;
}
}
puts(x[r] == y ? "YES" : "NO");
return 0;
}
В этом коде l содержит индекс левой границы диапазона индексов, r – правой, а m – середины диапазона.
Похожая идея используется в методе тернарного (троичного) поиска. Бинарный поиск полезен при решении следующих задач: «Тындекс.Бром» (http://codeforces.ru/contest/62/problem/B), «Тренировки у поселенцев» (http://codeforces.ru/contest/63/problem/B).
ЦИКЛИЧЕСКИЙ СДВИГ МАССИВА
Циклический сдвиг массива на один элемент с перезаписью значений элементов делается элементарно. Пусть x – массив из n элементов:
int t = x[0];
for (int i = 0; i < n - 1; i++) { x[i] = x[i + 1];
}
x[n - 1] = t;
Циклический сдвиг массива на k элементов (0 < k < n) с перезаписью значений элементов можно свести к k-кратному сдвигу массива на 1 элемент:
for (int j = 0; j < k; j++) { int t = x[0];
for (int i = 0; i < n - 1; i++) { x[i] = x[i + 1];
}
x[n - 1] = t;
}
Несмотря на то, что этот код будет работать для любого k ≥ 0, в то же время очевидно, что при k = 0 и k кратном n результат сдвига будет совпадать с исходным массивом, более того, при k > n нет никакой необходимости осуществлять сдвиг k раз, достаточно осуществить сдвиг на k mod n раз:
for (int j = 0; j < k % n; j++) { int t = x[0];
for (int i = 0; i < n - 1; i++) { x[i] = x[i + 1];
}
x[n - 1] = t;
}
При больших n и k представленный выше код может стать очень не эффективным. Если имеется запас памяти, то можно вместо k-кратного копирования одних и тех же элементов произвести три копирования двух участков памяти с помощью функции memcpy() (подключается с помощью заголовка cstring):
int m = k % n; if (m > 0) {
int *buf = new int[m]; memcpy(buf, x, m * sizeof(int));
memcpy(x, &x[m], (n - m) * sizeof(int)); memcpy(&x[n - m], buf, m * sizeof(int));
}
Симоненко Евгений А. Олимпиадная подготовка по программированию |
27 |
Несложно заметить, что результат k mod n сдвигов влево совпадает с результатом (n – k) mod n сдвигов вправо. Более того, если k mod n > n/2 (количество сдвигов влево больше чем дающее эквивалентный результат количество сдвигов вправо), то можно сдвиг влево заменить на n – k mod n сдвигов вправо.
int m = k % n; if (m > 0) {
if (m <= n / 2) { // сдвиг влево int *buf = new int[m];
memcpy(buf, x, m * sizeof(int)); memcpy(x, &x[m], (n - m) * sizeof(int)); memcpy(&x[n - m], buf, m * sizeof(int));
} else { // сдвиг вправо m = n - m;
int *buf = new int[m];
memcpy(buf, &x[n - m], m * sizeof(int)); memcpy(&x[m], x, (n - m) * sizeof(int)); memcpy(x, buf, m * sizeof(int));
}
}
Ещё более эффективным может оказаться отказ от копирования элементов массива в пользу введения смещения индексов. Естественно, что для этого потребуется модифицировать основной алгоритм. Рассмотрим этот подход на следующей часто встречающейся задаче: даны две строки, представляющие замкнутые цепочки символов, нужно проверить совпадают ли они. Решается задача довольно просто: нужно сравнить сначала две заданных строки и, если они не совпали, сдвинуть вторую на один элемент, после чего вновь их сравнить, если опять не совпали, то повторить сдвиг:
#include <iostream> #include <string>
using namespace std;
int main(int argc, char* argv[]) { string s1, s2;
cin >> s1 >> s2;
int k = s1.length(); if (k != s2.length()) {
cout << "NO" << endl; return 0;
}
bool found = false;
for (int j = 0; j < k; j++) { if (j != 0) {
int t = s2[0];
for (int i = 0; i < k - 1; i++) { s2[i] = s2[i + 1];
}
s2[k - 1] = t;
}
bool next = false;
for (int i = 0; i < k; i++) { if (s1[i] != s2[i]) {
next = true; break;
}
}
if (next) { continue;
} else {
found = true; break;
}
}
if (found) {
cout << "YES" << endl; } else {
cout << "NO" << endl;
}
28 |
Симоненко Евгений А. Олимпиадная подготовка по программированию |
return 0;
}
Это был код с реальными сдвигами. Теперь код с воображаемыми:
#include <iostream> #include <string>
using namespace std;
int main(int argc, char* argv[]) { string s1, s2;
cin >> s1 >> s2;
int k = s1.length();
if (k != s2.length()) {
cout << "NO" << endl; return 0;
}
bool found = false;
for (int j = 0; j < k; j++) { bool next = false;
for (int i = 0; i < k - j; i++) { if (s1[i] != s2[i + j]) {
next = true; break;
}
}
if (next) { continue;
}
for (int i = k - j; i < k; i++) {
if (s1[i] != s2[i - k + j]) { next = true;
break;
}
}
if (next) { continue;
} else {
found = true; break;
}
}
if (found) {
cout << "YES" << endl; } else {
cout << "NO" << endl;
}
return 0;
}
Код стал сложнее, но было бы интересно сравнить его производительность на очень длинных строках.
