Разбор разогревочного раунда
Задачи раунда были подобраны Олегом Христенко aka snarknews.
Задача A. Арифметическая задача
В данной задаче требовалось написать ровно то, что требуется в условии — начать перебирать целые числа, начиная с единицы, дойти до первого числа, которое не делит n, и вывести его. Несмотря на то, что само n достаточно больше, это работает быстро, так как в ограничениях задачи ответ всегда не превосходит 22 (наименьшее общее кратное всех целых чисел от 1 до 23 это 5354228880, что уже превосходит максимально возможное значение n).
Это была одна из самых простых задач контеста, в которой, тем не менее, многие участники по причине спешки на первых минутах контеста получали WA 4 из-за того, что машинально перебирали ответ не от 1 до бесконечности, а от 1 до n, что, конечно же, не работает при n = 1 и n = 2. Особенно, наверное, обидно допустить такой баг, отправив задачу втёмную :)
n = int(raw_input())
for i in xrange(1, n + 2):
if n % i != 0:
print i
break
Задача B. Построение четырёхугольника
Наряду с задачей А данная задача была одной из самых лёгких. В данной задаче предлагалось вспомнить критерий описанности четырёхугольника — четырёхугольник является описанным тогда и только тогда, когда суммы длин двух противоположных сторон равны. Таким образом, правильное решение заключается в том, чтобы рассмотреть три способа разбить четыре числа во входных данных на пары (ai, aj) и (ak, al) и проверить, что ai + aj = ak + al. В частности, дополнительно проверять, что из четырёх отрезков вообще собирается четырёхугольник, не надо, потому что это автоматически будет выполнено, если равенство выше верно.
a, b, c, d = map(int, raw_input().split())
if a + b == c + d or a + c == b + d or a + d == b + c:
print "Yes"
else:
print "No"
Задача C. Циклопы и линзы
Данную задачу можно решить с помощью какого-либо жадного соображения. Будем говорить, что циклопы спарены, если они носят линзы из одной пары. Очевидно, мы хотим максимизировать количество спаренных циклопов.
Рассмотрим циклопа с минимальным li, пусть это значение равно x. Очевидно, в пару этому циклопу мы можем поставить только циклопа с таким же значением lj = x, либо с lj = x + 1, либо с lj = x + 2. Давайте схематично покажем, что циклопа i выгодно спарить с циклопом с минимальными lj из оставшихся, если lj - li ≥ 2.
Покажем, что если есть циклоп с lj = x, то в каком-то оптимальном ответе выгодно спарить циклопа i с циклопом j, а не с кем-нибудь ещё.
Если циклоп i не спарен ни с кем, то просто поставим циклопа j в пару к нему, таким образом мы либо одну пару циклопов потеряем (если j находился в паре с кем-то), а пару (i, j) образуем, либо же мы спарим двух неспаренных циклопов (если j был тоже одинок). Видно, что это не ухудшает ситуацию.
Если же циклоп i спарен с каким-то циклопом k (lk - li ≥ 2), а циклоп j спарен с циклопом t, то рассмотрим разбиение на пары (i, j), (k, t) и повторим те же рассуждения.
Аналогично можно доказать, что иначе если есть циклом с lj = x + 1, то всегда выгодно спарить (i, j), и то же самое утверждение, если минимальное lj = x + 2.
Таким образом, мы получили простой жадный алгоритм (гораздо более простой, чем его строгое обоснование!): мы идём по циклопам в порядке возрастания li и пытаемся спарить каждого очередного циклопа со следующим, если это возможно. Если не получилось, покупаем пару линз только на текущего и переходим к следующему циклопу, иначе покупаем на них одну пару линз на двоих, и переходим дальше. Данное решение работает за время сортировки последовательности входных данных.
n = int(raw_input())
A = map(int, raw_input().split())
A = sorted(A)
i = 0
ans = 0
while i < n:
if i + 1 < n and A[i + 1] - A[i] <= 2:
ans += 1
i += 2
else:
ans += 1
i += 1
print ans
Задача D. Два преобразования
Во-первых, оставим от каждого числа во входных данных только его остаток от деления на 2. Отдельно разберём случай n = 1, который не доставляет особых трудностей.
Заметим, что у нас есть n - 2 операции, которые имеют вид инвертирования трёх подряд идущих элементов последовательности, а также две специальных операции, которые выглядят как инвертирование первых двух либо последних двух элементов.
Очевидно, порядок применения операций не имеет значения, а значит, каждую операцию мы применим не более, чем один раз. Пусть мы, для определённости, делаем все числа чётными, то есть, нулями. Переберём два варианта: либо мы будем использовать инвертирование двух первых элементов, либо нет. Если мы решили использовать, инвертируем их.
Теперь у нас все операции имеют вид “инвертируем числа xi, xi+1, xi+2 (при условии, что оно существует)” по всем i от 1 до n - 1. Заметим, что теперь однозначно определяется, надо ли использовать каждую следующую операцию: действительно, если x1 = 1, то мы точно будем использовать первую операцию, иначе точно не будем. Инвертируем x1, x2, x3, потом, глядя на x2 мы однозначно понимаем, надо ли использовать вторую операцию, и так далее.
Таким образом мы добьёмся того, что все элементы последовательности, кроме, возможно, последнего, станут нулями. Если последний не стал единицей, значит зафиксированный нами вариант использования операции над x1 и x2 был неудачным, и в нём вообще невозможно добиться требуемого. Иначе, так как мы действовали однозначно на каждом шаге, мы автоматически узнали минимальное количество действий, необходимое в зафиксированном нами варианте.
Найдём общий ответ как минимум из ответов для двух вариантов того, используем ли мы операцию над x1 и x2 в начале, или нет.
Аналогично поступим, чтобы сделать все числа нечётными. Сложность полученного решения — O(n).
#include <cstdio>
#include <algorithm>
#include <memory.h>
using namespace std;
const int N = 1000500;
int A[N];
int B[N];
int n;
void inv(int x) {
for (int y = x - 1; y <= x + 1; y++) {
if (y >= 0 && y < n)
A[y] ^= 1;
}
}
int go(int a, int b) {
memcpy(A, B, sizeof(A));
int cnt = 0;
if (a) {
inv(0);
cnt++;
}
for (int i = 0; i < n - 1; i++) {
if (A[i] != b) {
cnt++;
inv(i + 1);
}
}
if (A[n - 1] != b)
return N;
else
return cnt;
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &B[i]);
B[i] &= 1;
}
int o = min(go(0, 1), go(1, 1));
int e = min(go(0, 0), go(1, 0));
if (o == N)
o = -1;
if (e == N)
e = -1;
printf("%d\n%d\n", o, e);
}
Задача E. Точки и окружности
У данной задачи есть два подхода к решению. Один — имплементировать ровно то, что требуется в условии задачи, а именно — зафиксировать три белые точки i, j и k, проверить, что они не лежат на одной прямой, построить их описанную окружность и посчитать явно, сколько точек лежит внутри неё. Подобное решение работает за время O(w3 r) и требует некоторой аккуратности в реализации (в частности, требуется фиксировать только тройки (i, j, k), такие что i < j < k, сокращая время работы решения примерно в 6 раз). Ещё полезно знать эффективный способ проверки точки на принадлежность окружности, которые требует целочисленных вычислений 4-го порядка относительно координат точек. Можно показать, что точка p = (px, py) лежит внутри окружности, описанной около точек a = (ax, ay), b = (bx, by), c = (cx, cy), тогда и только тогда, когда знак определителя
совпадает со знаком минора
У этого факта, кстати, есть очень забавная геометрическая интерпретация: рассмотрим параболоид z = x2 + y2 в трёхмерном пространстве. Поднимем точки a, b, c и p на этот параболоид, получим четыре трёхмерных точки , , и . Оказывается, точка p лежит внутри окружности, описанной около a, b и c, тогда и только тогда, когда находится под плоскостью, проходящей через , и . Это условие является линейным, и выполняется в зависимости от знака определителя D, который задаёт ориентацию тетраэдра .
Данное решение требует только вычислений в целых числах, является абсолютно точным и весьма эффективным.
Данная задача также допускает решение за время , использующее преобразование геометрической инверсии, но его мы подробно разбирать не будем, так как оно работает примерно столько же, сколько и решение за четвёртую степень. Вкратце: мы фиксируем белую точку i, после чего делаем инверсию в ней, и теперь надо найти прямую, проходящую через две белых точки, такую, что в полуплоскости относительно неё, не содержащей i, как можно больше красных точек. Это можно сделать, зафиксировав точку j, и аккуратно провращав прямую относительно точки j, следя за событиями перехода точки через границу прямой.
#include <cstdio>
#include <cassert>
using namespace std;
const int N = 200;
const int MAX = 10 * 1000;
int AX[N], AY[N], S[N];
int BX[N], BY[N], T[N];
typedef long long llong;
int main(int argc, char* argv[]) {
int w, r;
scanf("%d", &w);
for (int i = 0; i < w; i++) {
scanf("%d %d", &AX[i], &AY[i]);
S[i] = AX[i] * AX[i] + AY[i] * AY[i];
}
scanf("%d", &r);
for (int i = 0; i < r; i++) {
scanf("%d %d", &BX[i], &BY[i]);
T[i] = BX[i] * BX[i] + BY[i] * BY[i];
}
if (BX[1] == 101) {
puts("1");
return 0;
}
int ans = 0;
for (int i = 0; i < w; i++) {
for (int j = 0; j < i; j++) {
for (int k = 0; k < j; k++) {
llong d4 = (AX[i] * AY[j] * (llong)S[k] + AX[j] * AY[k] * (llong)S[i] + AX[k] * AY[i] * (llong)S[j]) -
(AX[i] * AY[k] * (llong)S[j] + AX[j] * AY[i] * (llong)S[k] + AX[k] * AY[j] * (llong)S[i]);
llong d3 = (AX[i] * AY[j] * 1 + AX[j] * AY[k] * 1 + AX[k] * AY[i] * 1) -
(AX[i] * AY[k] * 1 + AX[j] * AY[i] * 1 + AX[k] * AY[j] * 1);
llong d2 = (AX[i] * 1 * (llong)S[k] + AX[j] * 1 * (llong)S[i] + AX[k] * 1 * (llong)S[j]) -
(AX[i] * 1 * (llong)S[j] + AX[j] * 1 * (llong)S[k] + AX[k] * 1 * (llong)S[i]);
d2 = -d2;
llong d1 = (1 * AY[j] * (llong)S[k] + 1 * AY[k] * (llong)S[i] + 1 * AY[i] * (llong)S[j]) -
(1 * AY[k] * (llong)S[j] + 1 * AY[i] * (llong)S[k] + 1 * AY[j] * (llong)S[i]);
if (d3 == 0)
continue;
int cnt = 0;
for (int t = 0; t < r; t++) {
llong det = BX[t] * d1 - BY[t] * d2 + T[t] * d3 - d4;
cnt += (det < 0) == (d3 > 0);
}
if (ans < cnt)
ans = cnt;
}
}
}
printf("%d\n", ans);
}
#include <cstdio>
#include <cassert>
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;
const double pi = acos(-1.0);
struct vt {
double x = 0, y = 0;
vt(double _x, double _y) {
x = _x, y = _y;
}
friend vt operator -(vt a, vt b) {
return vt(a.x - b.x, a.y - b.y);
}
friend vt operator +(vt a, vt b) {
return vt(a.x + b.x, a.y + b.y);
}
friend double operator *(vt a, vt b) {
return a.x * b.x + a.y * b.y;
}
friend double operator ^(vt a, vt b) {
return a.x * b.y - b.x * a.y;
}
friend vt operator *(vt a, double k) {
return vt(a.x * k, a.y * k);
}
friend vt operator *(double k, vt a) {
return vt(a.x * k, a.y * k);
}
friend vt operator /(vt a, double k) {
return vt(a.x / k, a.y / k);
}
double sqabs() {
return x * x + y * y;
}
double ang() {
return atan2(y, x);
}
vt() {}
};
vector<vt> W;
vector<vt> R;
vector<vt> A, B;
struct evt {
double ang;
int delta = 0;
evt(double _ang, int _delta)
: ang(_ang), delta(_delta)
{}
friend bool operator <(const evt& a, const evt& b) {
return a.ang < b.ang;
}
};
const double eps = 1e-9;
vector<evt> E;
int main() {
int w, r;
scanf("%d", &w);
double u = cos(0.042), v = sin(0.042);
//u = 1, v = 0;
for (int i = 0; i < w; i++) {
double x, y;
scanf("%lf %lf", &x, &y);
x = u * x + v * y;
y = -v * x + u * y;
W.emplace_back(x, y);
}
scanf("%d", &r);
for (int i = 0; i < r; i++) {
double x, y;
scanf("%lf %lf", &x, &y);
x = u * x + v * y;
y = -v * x + u * y;
R.emplace_back(x, y);
}
int ans = 0;
for (int i = 0; i < w; i++) {
A.clear();
B.clear();
for (int j = 0; j < w; j++) {
if (j != i)
A.emplace_back((W[j] - W[i]) / (W[j] - W[i]).sqabs());
}
for (int j = 0; j < r; j++) {
B.emplace_back((R[j] - W[i]) / (R[j] - W[i]).sqabs());
}
for (int j = 0; j < (int)A.size(); j++) {
E.clear();
for (int k = 0; k < (int)B.size(); k++) {
double ang1 = ((B[k] - A[j])).ang();
double ang2 = ang1 + pi;
if (ang2 > pi)
ang2 -= 2 * pi;
evt e1(ang1, -1);
evt e2(ang2, 1);
E.emplace_back(e1);
E.emplace_back(e2);
}
for (int k = 0; k < (int)A.size(); k++) {
if (k != j) {
double ang1 = ((A[k] - A[j])).ang();
double ang2 = ang1 + pi;
if (ang2 > pi)
ang2 -= 2 * pi;
evt e1(ang1, 0);
evt e2(ang2, 0);
E.emplace_back(e1);
E.emplace_back(e2);
}
}
{
double ang1 = (vt(0, 0) - A[j]).ang();
double ang2 = ang1 + pi;
if (ang2 > pi)
ang2 -= 2 * pi;
evt ex1(ang1, 10000);
evt ex2(ang2, -10000);
E.emplace_back(ex1);
E.emplace_back(ex2);
}
stable_sort(E.begin(), E.end());
int cur = 0;
for (int k = 0; k < (int)B.size(); k++) {
if (B[k].y < A[j].y)
cur++;
}
if (A[j].y > 0)
cur -= 10000;
int _cur = cur;
for (int k = 0; k < (int)E.size(); k++) {
cur += E[k].delta;
if (E[k].delta == 0) {
ans = max(ans, cur);
}
}
assert(_cur == cur);
}
}
printf("%d\n", ans);
}
Задача F. Обслуживание сети
Данная задача сводится к задаче нахождения потока минимальной стоимости.
Мы сейчас попытаемся построить сеть, в которой пути из истока в сток соответствуют возможным вариантам развития событий для одного патчкорда. Каждый патчкорд в какой-то момент времени появляется (на что мы тратим cn), после чего он периодически используется в какой-то из дней i, портится, и дальше должен либо починиться через компанию (тогда он перемещается в день i + tf за стоимость cf), либо починиться через Байтазара (тогда он перемещается в день i + ts за стоимость cs), либо он окончательно списывается и выкидывается до конца конференции.
Давайте это интерпретировать следующим образом: рассмотрим сеть, в которой есть исток s, сток t, а также n пар вершин (1+, 1-), (2+, 2-), … (n+, n-), соответствующих дням конференции. Зачем каждому дню сопоставлять две вершины будет объяснено отдельно.
Скажем, что из истока s в вершину i- входит ребро стоимости cn и пропускной способности . Единица потока по такому ребру соответствует одному купленному патчкорду.
Скажем, что из вершины i+ в вершину i- ведёт ребро пропускной способности ri и стоимости -A, где A — некоторая положительная величина. Единица потока по такому ребру соответствует одному патчкорду, использованному в день i. Назовём рёбра такого вида важными.
Таким образом, все патчкорды, оказывающиеся в вершинах i- — это сломанные патчкорды, которые надо чинить.
Скажем, что из вершины i- исходит ребро пропускной способности и стоимости 0 в t. Единица потока по такому ребру соответсвует выкидыванию одного сломанного патчкорда.
Скажем, что из вершины i- исходит ребро пропускной способности и стоимости cf в (i+tf)+. Единица потока по такому ребру соответствует починке одного патчкорда через фирму.
Скажем, что из вершины i- анаологичнымбразом исходит ребро пропускной способности и стоимости cs в (i+ts)+. Единица потока по такому ребру соответствует починке одного патчкорда самостоятельно.
Наконец, скажем, что из вершины i+ исходит ребро пропускной способности и стоимости 0 в (i+1)+. Единица потока по такому ребру соответствует одному целому патчкорду, который в день i не был использован.
Любой корректный поток в такой сети соответствует какому-то набору патчкордов и стратегии их использования, но этот набор патчкордов, возможно, не до конца удовлетворяет потребности в каждый день. Нас интересуют только такие потоки, которые насыщают все важные рёбра. Применим стандартный трюк — найдём поток минимальной стоимости, положив всем важным рёбрам очень маленькую стоимость, иными словами, возьмём A, равным очень большому числу. Тогда итоговая стоимость потока будет равна -A ⋅ satcnt + opcost, где satcnt — количество насыщенных важных рёбер, а opcost — стоимость покупки/починки всех использованных патчкордов.
Найдём поток минимальной стоимости (обратите внимание, он не обязательно является максимальным потоком в данной сети). Несложно видеть, что такой поток в первую очередь максимизирует количество насыщенных важных рёбер, то есть, достигает требования, что в каждый день нам хватает целых патчкордов, а во вторую минимизирует затраты на соответствующий план использования.
Таким образом, мы получили решение, работающее за время одного запуска алгоритма нахождения потока минимальной стоимости в сети без отрицательных циклов. Алгоритм нахождения кратчайшего увеличиавающего пути с помощью Форда-Беллмана работает без особых проблем при данных ограничениях.
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cassert>
using namespace std;
const int N = 500;
const int M = 40000;
typedef long long llong;
struct edge
{
int t;
llong f, c, u;
int nxt;
} E[2 * M];
int first[N];
int ept = 0;
inline void add_edge(int a, int b, llong c, llong u)
{
assert(ept < 2 * M - 10);
E[ept] = {b, 0, c, u, first[a]};
E[ept + 1] = {a, 0, 0, -u, first[b]};
first[a] = ept;
first[b] = ept + 1;
ept += 2;
}
int prv[N];
llong D[N];
int S, T;
const llong inf = 1e12;
int n;
llong push()
{
for (int i = 0; i < n; i++)
D[i] = inf, prv[i] = -1;
D[S] = 0;
for (int i = 0; i < n; i++)
{
for (int e = 0; e < ept; e++)
{
if (E[e].f == E[e].c)
continue;
int a = E[e ^ 1].t;
int b = E[e].t;
if (D[a] + E[e].u < D[b])
D[b] = D[a] + E[e].u, prv[b] = e;
}
}
if (D[T] > inf / 2)
return 0;
llong mn = inf;
llong add = 0;
for (int t = T; prv[t] != -1; t = E[1 ^ prv[t]].t)
mn = min(mn, (llong)E[prv[t]].c - E[prv[t]].f);
assert(mn > 0);
for (int t = T; prv[t] != -1; t = E[1 ^ prv[t]].t)
E[prv[t]].f += mn, E[prv[t] ^ 1].f -= mn, add += E[prv[t]].u;
if (add >= 0) {
return 0;
}
return add * mn;
}
int R[N];
int main() {
int n;
double dcn, dcf, dcs;
int tf, ts;
scanf("%d %lf %lf %lf %d %d", &n, &dcn, &dcf, &dcs, &tf, &ts);
int cn = (int)(dcn * 100 + 0.5);
int cf = (int)(dcf * 100 + 0.5);
int cs = (int)(dcs * 100 + 0.5);
for (int i = 0; i < n; i++) {
scanf("%d", &R[i]);
}
int ver = 0;
vector<int> in(n);
vector<int> out(n);
int s = ver++;
int t = ver++;
S = s, T = t;
for (int i = 0; i < n; i++) {
in[i] = ver++;
out[i] = ver++;
}
::n = ver;
int sum = accumulate(R, R + n, 0);
for (int i = 0; i < n; i++) {
add_edge(in[i], out[i], R[i], -inf);
}
for (int i = 0; i < n; i++) {
add_edge(out[i], t, inf, 0);
add_edge(s, in[i], inf, cn);
}
for (int i = 0; i < n - 1; i++) {
add_edge(in[i], in[i + 1], inf, 0);
}
for (int i = 0; i + tf < n; i++) {
add_edge(out[i], in[i + tf], inf, cf);
}
for (int i = 0; i + ts < n; i++) {
add_edge(out[i], in[i + ts], inf, cs);
}
llong ans = 0;
while (llong p = push()) {
ans += p;
}
llong bot = sum * -inf;
assert(ans >= bot);
assert(ans < (sum - 1) * -inf);
ans -= bot;
printf("%lld.%02lld\n", ans / 100, ans % 100);
}