Задача 27
А вот эта задача очень простая. Бывает трёх видов (сводящихся к одной и той же схеме):
- выбрать из кучи пар чисел по одному числу так, чтобы что-нибудь максимизировать или минимизировать
- найти самый частовстречающийся результат какого-нибудь отображения, применённого к каждому элементу массива
- редуцировать (в какой-нибудь полугруппе) все пары элементов массива, индексы которых в этом массиве различаются не менее чем на заданную величину
И то, и другое нужно либо сделать хоть как-то (тогда за задачу дадут 2 балла), либо за один проход по входным данным, не сохраняя их в массив (тогда за задачу дадут 4 балла).
Редукция
Самая частая разновидность.
Типичное задание: найти количество пар разных элементов входного массива, сумма которых делится на 6. Обратите внимание на то, что в конкретно этой задаче неявно присутствует минимальное ограничение на разность индексов элементов пары, равное 1.
#include <iostream>
#include <deque>
#include <map>
using namespace std;
int main() {
unsigned N; cin >> N; // входной размер массива
deque<unsigned> queue; // задержка между получением элементов и обработкой
map<unsigned,unsigned> mod_count; // таблица количеств по остаткам
unsigned answer = 0;
for (unsigned i = 0; i < N; i++) {
unsigned x; cin >> x;
queue.push_back(x);
if (i < 1) { continue; }
mod_count[queue.front()%6] += 1;
queue.pop_front();
unsigned mod = queue.back()%6;
answer += mod_count[(6 - mod) % 6];
}
cout << answer << endl;
}
Может показаться, что очередь длины 1 — весьма странное решение, но в некоторых
ситуациях любые альтернативы (кроме банальных замен deque
на две переменных)
оказываются более сложными. Всегда есть соблазн (если в условии ничего
явно не сказано при минимальную разность индексов) просто завести
несколько переменных, в каждую из которых редуцировать неким образом
все элементы массива, а затем написать одну большую формулу, выражающую
ответ в терминах этих переменных. Такой соблазн лучше игнорировать.
Рассмотрим ещё одно похожее задание: найти максимальное произведение пары элементов входного массива (все элементы — целые положительные числа), индексы которых различаются хотя бы на 6, делящееся на 15.
#include <iostream>
#include <deque>
#include <map>
using namespace std;
int main() {
unsigned N; cin >> N;
deque<unsigned> queue;
map<unsigned,unsigned> mod_max; // таблица максимумов по остаткам
unsigned answer = 0;
for (unsigned i = 0; i < N; i++) {
unsigned x; cin >> x;
queue.push_back(x);
if (i < 6) { continue; } // здесь поменялась длина очереди
unsigned mod = queue.front() % 15;
mod_max[mod] = max(mod_max[mod], queue.front()); // сумма сменилась на максимум
queue.pop_front();
mod = queue.back()%15;
if (mod == 0) {
for (unsigned m = 0; m < 15; m+=1) { answer = max(answer, queue.back()*mod_max[m]); }
} else if (mod % 3 == 0) {
for (unsigned m = 0; m < 15; m+=5) { answer = max(answer, queue.back()*mod_max[m]); }
} else if (mod % 5 == 0) {
for (unsigned m = 0; m < 15; m+=3) { answer = max(answer, queue.back()*mod_max[m]); }
}
}
cout << answer << endl;
}
Таблица частот
Требуется, например, найти и напечатать самую частую первую цифру десятичной записи чисел. Задание весьма тривиально, если завести таблицу частот цифр.
#include <iostream>
#include <map>
using namespace std;
unsigned first_digit(unsigned x) {
while (x > 9) { x /= 10; }
return x;
}
int main() {
unsigned N; cin >> N;
map<unsigned,unsigned> digit_freq;
for (unsigned i = 0; i < N; i++) {
unsigned x; cin >> x;
digit_freq[first_digit(x)] += 1;
}
// дальнейшее написано в предположении, что
// если несколько частот совпадают, то нужно вывести
// наибольшую из цифр
unsigned best_digit = 0;
unsigned best_freq = digit_freq[0];
for (unsigned digit = 1; digit <= 9; digit++) {
if (digit_freq[digit] >= best_freq) {
// если бы требовалось вывести наименьшую из цифр,
// достаточно было бы поменять знак на >
best_freq = digit_freq[digit];
best_digit = digit;
}
}
cout << best_digit << endl;
}
Выбор одного из двух
Типичное задание таково. На вход подаются пары (целых положительных) чисел. Нужно выбрать из каждой пары одно число так, чтобы:
- сумма выбранных чисел не делится на 42
- она является максимальной среди возможных
Если такой выбор сделать нельзя, требуется напечатать 0. В противном случае нужно напечатать сумму.
Ключевой момент заключается в том, что:
- либо подходит сумма наибольших (в каждой паре) чисел
- либо в одной из пар можно взять вместо наибольшего числа наименьшее
- либо сумма наибольших чисел и все разности в парах делятся на 42 и, соответственно, любой выбор чисел имеет сумму, делящуюся на 42
Это приводит к следующему решению (очень напоминающему решение типичной 25 задачи):
#include <iostream>
using namespace std;
int main() {
unsigned N; cin >> N;
unsigned large = 10001; // ограничение сверху плюс один
unsigned total = 0; // сумма наибольших
unsigned min_diff = large; // минимальная из разностей, не делящихся на 42
for (unsigned i = 0; i < N; i++) {
unsigned x,y; cin >> x >> y;
total += max(x,y);
unsigned diff = max(x,y) - min(x,y);
if (diff % 42 != 0) {
min_diff = min(min_diff, diff);
}
}
unsigned answer = total;
if (answer % 42 == 0 and min_diff != large) {
answer -= min_diff;
}
if (answer % 42 == 0) {
answer = 0;
}
cout << answer << endl;
}
Общая схема
Нетрудно заметить, что вторая и третья разновидности задачи являются частными случаями первой с нулевой длиной очереди.
Собственно, общая схема решения предполагает следующие входные данные:
- начальное состояние и начальный ответ
- величину задержки (длину очереди)
- функцию обновления состояния по элементу, удалённому из очереди
- функцию обновления ответа по состоянию и элементу, пришедшему в очередь
Эту схему можно записать (на хаскеллоподобном языке) так:
solve :: s -> a -> Int -> (x -> s -> s) -> (x -> s -> a -> a) -> [x] -> a
solve s0 a0 n updateS updateA xs = go s0 a0 $ zip xs (drop n xs)
where
go _ a [] = a
go s a ((old,new):rest) =
let s' = (updateS old s)
in go s' (updateA new s' a) rest
По-научному такая схема (а точнее говоря, некоторое её обобщение) называется зигохистоморфизмом.
Казалось бы, причём здесь зигохистоморфные препроморфизмы?..