Муниципальный этап, 9–11 классы всероссийской олимпиады по информатике: 5 задач с официальным разбором, к каждой наше решение на C++.
К задаче 1Муниципальный этап ВсОШ по информатике 2023/24, 9–11 классы.
Задачи тура
- 1Ферзьматематика, разбор случаев
- 2Рамка для рисункаматематика, жадный алгоритм
- 3Телефонный справочникстроки, жадный алгоритм
- 4Задачи на печать!жадный алгоритм, разбор случаев
- 5Вечер кёрлингажадный алгоритм, два указателя
Отмечайте решённые: 0 из 5
1. Ферзь
Пётр любит шахматы и математику. Он знает, что самая мощная фигура в шахматах — это ферзь, потому что он ходит и как ладья, на все клетки на одной с ним вертикали или горизонтали, и как слон, на все клетки по диагоналям. Ферзя можно поставить на доску так, чтобы он контролировал (то есть мог переместиться в эти клетки за один ход) целых 27 клеток доски!
Петра заинтересовало, какое максимальное количество клеток может контролировать ферзь на прямоугольных досках самых разных размеров. Помогите ему в решении этой задачи.
Формат входных данных
Первая строка входных данных содержит целое число () — размер доски по вертикали.
Вторая строка входных данных содержит целое число () — размер доски по горизонтали.
Формат выходных данных
Программа должна вывести одно целое число — максимальное количество клеток, которое может контролировать ферзь на доске .
Обратите внимание на то, что ответ в этой задаче может превышать возможное значение 32-битной целочисленной переменной, поэтому необходимо использовать 64-битные целочисленные типы данных (тип long long в языке C++, тип int64 в Pascal, тип long в Java и C#).
Система оценки
Решения, правильно работающие, когда и не превоcходят , будут оцениваться в 40 баллов.
Решения, правильно работающие, когда и не превоcходят , будут оцениваться в 80 баллов.
Замечание
Второй пример из условия приведён на рисунке. Крестиками обозначены клетки, которые контролирует ферзь.

| стандартный ввод | стандартный вывод |
|---|---|
8 8 | 27 |
| стандартный ввод | стандартный вывод |
|---|---|
3 4 | 9 |
Официальный разборСначала попробуйте решить сами
Наше решение на C++Полный код на C++17
Официальный разбор
Чтобы набрать 40 баллов достаточно написать решение, перебирающее все клетки, на которые можно поставить ферзя. Затем нужно посчитать количество клеток, которые бьёт этот ферзь. Для этого также переберём все оставшиеся клетки доски, и проверим, находятся ли они в одной горизонтали, вертикали или диагонали с выбранной клеткой. Запомним наибольшее количество клеток, которое может бить ферзь. Сложность такого решения будет . В примере такого решения строки и столбцы доски нумеруются для удобства с нуля.
n = int(input())
m = int(input())
ans = 0
for x in range(n):
for y in range(m):
count = 0
for xx in range(n):
for yy in range(m):
if x==xx or y==yy or x-xx==y-yy or x-xx==yy-y:
count += 1
ans = max(ans, count - 1)
print(ans)Чтобы набрать 80 баллов, нужно уменьшить сложность решения до . Для этого можно находить количество клеток, которые бьёт ферзь, без цикла, то есть за . Или, наоборот, заметить, что ферзя нужно поставить в центр доски, и найти циклами количество клеток, которые он бьёт. Пример второго решения, где ферзь ставится в клетку с координатами .
n = int(input())
m = int(input())
x = n // 2
y = m // 2
count = 0
for xx in range(n):
for yy in range(m):
if x==xx or y==yy or x-xx==y-yy or x-xx==yy-y:
count += 1
print(count - 1)Полное решение имеет сложность . Здесь нужно заметить, что ферзь контролирует наибольшее количество клеток, находясь в центре доски, и посчитать их количество, без использования циклов.
Пусть , то есть доска “вытянута” по горизонтали, иначе поменяем значения и . Тогда в одной горизонтали с ферзём находятся клетка, а в одной вертикали клетка. Диагонали, проходящие через ферзя, также могут содержать не более клеток (поэтому в каждой из них не более клетки, за вычетом клетки, в которой стоит ферзь), поэтому ответ будет равен . Но есть одно исключение: на квадратной доске, сторона которой имеет чётную длину, одна из диагоналей будет короче на одну клетку. Это, например, случай доски (первый пример из условия), для которой ответ равен 27, а не 28. Почему так происходит, можно видеть на рисунке.

В этом случае просто вычтем 1 из ответа. Пример такого решения.
n = int(input())
m = int(input())
if n > m:
n, m = m, n
ans = 3 * n + m - 4
if n % 2 == 0 and m == n:
ans -= 1
print(ans)Наше решение на C++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long n, m;
cin >> n >> m;
if (n > m) swap(n, m);
long long answer = 3 * n + m - 4;
if (n == m && n % 2 == 0) answer--;
cout << answer << "\n";
return 0;
}2. Рамка для рисунка
У Алексея есть набор, который состоит из палочек длины 1 и палочек длины 2. Палочки можно соединять между собой, либо выстраивая их в линию, либо под прямым углом.
Алексей хочет собрать из имеющихся палочек рамку прямоугольной формы, чтобы потом вставить в эту рамку лист бумаги и нарисовать красивый пейзаж для мамы на Новый год.
При этом Алексей считает, что чем больше будет площадь прямоугольника, тем значимей будет его подарок. Поэтому ему важно определить максимальную площадь прямоугольника, границу которого можно собрать из имеющихся палочек.
Формат входных данных
Первая строка входных данных содержит целое число — количество палочек длины 1, .
Вторая строка входных данных содержит целое число — количество палочек длины 2, .
Формат выходных данных
В единственной строке выведите единственное целое число — максимальную площадь прямоугольника, который можно сложить из имеющихся палочек. Если из имеющихся палочек невозможно сложить никакой прямоугольник, то выведите число 0.
Обратите внимание на то, что ответ в этой задаче может превышать возможное значение 32-битной целочисленной переменной, поэтому необходимо использовать 64-битные целочисленные типы данных (тип long long в языке C++, тип int64 в Pascal, тип long в Java и C#).
Система оценки
Решения, правильно работающие, когда и не превосходят , будут оцениваться в 20 баллов.
Решения, правильно работающие, когда и не превосходят , будут оцениваться в 40 баллов.
Решения, правильно работающие, когда и не превосходят , будут оцениваться в 60 баллов.
Замечание
В первом примере есть 5 палочек длины 1. Из них можно сложить квадрат со стороной 1, его площадь равна 1, при этом одна палочка останется.
Во втором примере есть 4 палочки длины 1 и 3 палочки длины 2. Из них можно сложить прямоугольник размера .
В третьем примере есть 3 палочки длины 1, из них невозможно сложить прямоугольник.
| стандартный ввод | стандартный вывод |
|---|---|
5 0 | 1 |
| стандартный ввод | стандартный вывод |
|---|---|
4 3 | 6 |
| стандартный ввод | стандартный вывод |
|---|---|
3 0 | 0 |
Официальный разборСначала попробуйте решить сами
Наше решение на C++Полный код на C++17
Официальный разбор
Начнём с переборных решений, набирающих частичные баллы. Пусть и — количество палочек длины 1 и 2 соответственно.
40 баллов можно набрать, если перебирать две стороны прямоугольника и . Проверим, можно ли сложить из имеющихся палочек прямоугольник . Должны выполняться два условия: общая длина всех палочек должна быть не меньше периметра прямоугольника, равного , и каждая сторона нечётной длины должна содержать хотя бы одну палочку длины 1, поэтому значение должно быть не меньше количества нечётных чисел среди сторон . Пример такого решения.
n1 = int(input())
n2 = int(input())
max_side = (n1 + 2 * n2) // 2
ans = 0
for a in range(1, max_side + 1):
for b in range(1, max_side + 1):
if n1 + 2 * n2 >= 2 * a + 2 * b and n1 >= 2 * (a % 2 + b % 2):
ans = max(ans, a * b)
print(ans)Чтобы набрать 60 баллов нужно перебирать только одну сторону, а не две. Пусть это сторона . Максимальное значение одной стороны, как и в предыдущем решении, равно (целочисленное частное от деления суммы длин всех палочек на 2). Тогда у нас будет две стороны длины . Проверим условие, что не меньше, чем , то есть если — нечётное, то найдётся хотя бы две палочки длины 1.
Теперь определим наибольшее подходящее значение для данного значения . Для этого посчитаем длину оставшихся палочек и поделим её на 2. При этом могло оказаться, что , то есть значение оказалось меньше, чем число нечётных чисел среди значений , то нам не хватит палочек длины 1 для того, чтобы собрать нечётные отрезки длины . Но ранее мы проверили, что нам хватает палочек длины 1 для того, чтобы собрать только отрезки и , поэтому это возможно только в случае нечётного . В этом случае уменьшим значение на 1. Так мы определяем наибольшее значение второй стороны для ранее выбранной стороны . Запомним наибольшее из значений площадей прямоугольников .
n1 = int(input())
n2 = int(input())
max_side = (n1 + 2 * n2) // 2
ans = 0
for a in range(1, max_side + 1):
if n1 < 2 * (a % 2):
continue
b = (n1 + 2 * n2 - 2 * a) // 2
if n1 < 2 * (a % 2 + b % 2):
b -= 1
ans = max(ans, a * b)
print(ans)Чтобы написать полное решение, нужно избавится и от перебора всех возможных значений одной стороны. Нужно заметить, что среди всех прямоугольников с одинаковым периметром максимальная площадь будет у квадрата или у прямоугольника, стороны которого различаются на 1. Действительно, пусть прямоугольник имеет стороны , при этом . Рассмотрим прямоугольник с таким же периметром. Его площадь будет равна , то есть больше площади .
Поэтому для максимизации периметра в качестве значения одной из сторон нужно выбрать от максимально возможного периметра. Для этого в качестве значения минимальной стороны возьмём значение , а значение подберём наибольшее подходящее значение, как в предыдущем решении. Но если значение оказалось нечётным, то для формирования сторон длины понадобится минимум 2 палочки длины 1, и, возможно, нам не хватит палочек длины 1 для достижения максимально возможного значения . Поэтому необходимо рассмотреть как чётное, так и нечётное значение , то есть нужно взять не только , но и значение на 1 меньше — , так как одно из них будет чётным. Для каждого из этих значений выберем подходящее и найдём наибольшее значение .
Пример такого решения.
n1 = int(input())
n2 = int(input())
ans = 0
side = (n1 + 2 * n2) // 4
for a in range(side - 1, side + 1):
if n1 < 2 * (a % 2):
continue
b = (n1 + 2 * n2 - 2 * a) // 2
if n1 < 2 * (a % 2 + b % 2):
b -= 1
ans = max(ans, a * b)
print(ans)Есть и другие способы решения задачи. Например, можно попробовать конструктивно построить решение так, чтобы две стороны прямоугольника оказались максимально большими и при этом близки друг к другу. Для этого сначала разложим палочки длины 2 поровну на все стороны. У нас останется 0, 1, 2 или 3 палочки длины 2. Если осталось хотя бы две палочки длины 2, то увеличим на 2 длины двух сторон . Если после этого осталась хотя бы одна палочка длины 2 и ещё одна палочка длины 2 или две палочки длины 1, то также можно сторону увеличить на 2. Иначе попробуем используя палочки длины 1 выровнять длины сторон. После выравнивания длин сторон все оставшиеся палочки длины 1 разложим поровну по всем сторонам. После этого останется не более трёх палочек длины 1, если их две или три то можно длины двух сторон увеличить на 1.
Сложность реализации такого решения в том, что нужно аккуратно рассмотреть все случаи, не пропустив ни одного.
n1 = int(input())
n2 = int(input())
a = 2 * (n2 // 4)
b = 2 * (n2 // 4)
n2 %= 4
if n2 >= 2:
n2 -= 2
a += 2
if n2 == 1 and n1 >= 2:
b += 2
n2 = 0
n1 -= 2
while a < b and n1 >= 2:
a += 1
n1 -= 2
while a > b and n1 >= 2:
b += 1
n1 -= 2
a += n1 // 4
b += n1 // 4
n1 %= 4
if n1 >= 2:
a += 1
print(a * b)Наше решение на C++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long ones, twos;
cin >> ones >> twos;
long long total = ones + 2 * twos;
long long side = total / 4;
long long best = 0;
for (long long a = side - 1; a <= side; a++) {
if (a < 1) continue;
if (ones < 2 * (a % 2)) continue;
long long b = (total - 2 * a) / 2;
if (ones < 2 * (a % 2 + b % 2)) b--;
if (b < 1) continue;
best = max(best, a * b);
}
cout << best << "\n";
return 0;
}3. Телефонный справочник
Саша недавно начала регистровать компанию по разработке чат-ботов и уже подала необходимые документы. Но добрые люди рассказали Саше, что в телефонном справочнике компании располагаются в лексикографически возрастающем порядке их названий. Что такое телефонный справочник, Саша не знает, но решила учесть рекомендации и поменять название своей компании, чтобы оно было как можно раньше в телефонном справочнике. Поскольку Саша уже подала документы, она не может полностью поменять название компании, но может сказать, что допустила опечатку, и поменять любые две буквы в названии местами. Помогите Саше выбрать новое название компании или оставить текущее.
Формат входных данных
В единственной строке содержится одно слово, состоящее из строчных латинских букв (от a до z) длиной ().
Формат выходных данных
Выведите одно слово — новое название компании. Если название не изменилось, выведите изначальное название.
Система оценки
Решения, верно работающие при , будут оцениваться в 20 баллов.
Решения, верно работающие при , будут оцениваться в 40 баллов.
Решения, верно работающие при , будут оцениваться в 60 баллов.
Решения, верно работающие при , будут оцениваться в 80 баллов.
| стандартный ввод | стандартный вывод |
|---|---|
aefbz | abfez |
| стандартный ввод | стандартный вывод |
|---|---|
abc | abc |
Официальный разборСначала попробуйте решить сами
Наше решение на C++Полный код на C++17
Официальный разбор
Обозначим через исходную строку, — её длину, а — мощность алфавита, то есть количество возможных символов. Для английского алфавита .
У этой задачи есть много разных идей решения. Начнём с самой простой — будем перебирать пары символов двумя вложенными циклами и менять символы строки и местами. Можно дополнительно проверять, что , то есть при перестановке символов строка станет меньше в лексикографическом порядке. Из всех полученных таким образом строк выберем минимальную. Такое решение имеет сложность , так как пар символов , а сравнение строк выполняется за .
s = input()
ans = s
for i in range(len(s)):
for j in range(i + 1, len(s)):
if s[i] > s[j]:
s1 = s[:i] + s[j] + s[i + 1:j] + s[i] + s[j + 1:]
if s1 < ans:
ans = s1
print(ans)Можно улучшить это решение, если не сравнивать строки явно и даже не строить строку с ответом. Для этого заметим, что пусть у нас есть несколько способов выполнить перестановку двух символов. Например, в строке “ccddaabb” мы можем поменять одну из букв “c” или “d” с одной из букв “a” или “b”. Пусть есть два способа это сделать, например, поменять символ и или поменять и .
Первый способ даст меньшую строку, если меньше , то есть мы уменьшим тот символ, который стоит раньше. Если , то первый способ будет меньше, если , то есть на место первого заменяемого символа ставится меньший символ. В случае равенства символов первый способ будет меньше при , т.к. второй участвующий в обмене символ увеличивается, и мы должны выбрать его как можно позже. Например, для строки “ccddaabb” наилучшим будет ответ “acddacbb”.
Поэтому для построения решения сложности можно перебирать пары переставляемых символов и и запоминать такую пару, которая даст лучший ответ (c минимальным значением , при равенстве c минимальным значением , при равенстве с максимальным значением ).
Пример такого решения. В этом решении в переменной ans хранится тройка чисел . Перед значением поставили знак минус, потому что кортежи сравниваются в лексикографическом порядке, а нам необходимо, чтобы при минимальных и был выбран тот ответ, у которого значение будет больше, поэтому мы будем хранить . Запомним наименьший из всех подходящих кортежей, потом выведем ответ, переставив два символа, если был найден подходящий ответ.
s = input()
ans = (len(s), ' ', 0)
for i in range(len(s)):
for j in range(i + 1, len(s)):
if s[i] > s[j]:
ans = min(ans, (i, s[j], -j))
if ans[0] != len(s):
i = ans[0]
j = -ans[2]
s = s[:i] + s[j] + s[i + 1:j] + s[i] + s[j + 1:]
print(s)Для дальнейшего улучшения этого решения заметим, что если рассматриваем символ , который мы хотим поменять с каким-то предыдущим символом, то можно рассматривать не все символы , а только самые первые вхождения какой-либо буквы, например, самое первое вхождение буквы “z”, самое первое вхождение буквы “y” и т.д. Запомним для каждой буквы алфавита от “a” до ”z” её первое вхождение.
Затем переберём символы , которые мы рассматриваем, как правый символ из двух переставляемых. Но в качестве левого символа будем рассматривать не все символы, а только первые вхождения тех символов, значение которых больше, чем . В примере решение ниже это цикл по переменной c. Идея выбора наилучшего ответа аналогично предыдущему решению. Такое решение имеет сложность .
s = input()
n = len(s)
first = [n] * 26
for i in range(len(s)):
c = ord(s[i]) - ord('a')
if first[c] == n:
first[c] = i
ans = (n, ' ', 0)
for j in range(n):
for c in range(ord(s[j]) - ord('a') + 1, 26):
i = first[c]
if i < j:
ans = min(ans, (i, s[j], -j))
if ans[0] != n:
i = ans[0]
j = -ans[2]
s = s[:i] + s[j] + s[i + 1:j] + s[i] + s[j + 1:]
print(s)Следующим шагом будет запоминание не только первого вхождения каждого символа, но и последнего вхождения, потому что для каждой пары символов и , где нужно переставлять первое вхождение с последним вхождением . После нахождения первой и последней позиции (первый цикл) переберём все пары символов и рассмотрим ответ, который получается перестановкой первого вхождения (переменная i) и последнего вхождения (переменная j). Такое решение будет иметь сложность , и оно уже набирает 100 баллов.
s = input()
n = len(s)
first = [n] * 26
last = [-1] * 26
for i in range(len(s)):
c = ord(s[i]) - ord('a')
if first[c] == n:
first[c] = i
last[c] = i
ans = (n, ' ', 0)
for c in range(26):
i = first[c]
for d in range(c):
j = last[d]
if i < j:
ans = min(ans, (i, d, -j))
if ans[0] != n:
i = ans[0]
j = -ans[2]
s = s[:i] + s[j] + s[i + 1:j] + s[i] + s[j + 1:]
print(s)Есть и решение сложности , то есть не зависящее от мощности алфавита. Для этого поймём, как будет устроена строка, которую нельзя уменьшить перестановкой двух символов. В такой строке символы будут идти в порядке неубывания, например, “aaacdddfffkkkm”. Пропустим префикс строки, состоящий из неубывающих символов. Пусть после этого мы встретили символ, который меньше предыдущего, например, после символа “m” пусть идёт символ “h”. Этот символ можно переставить вперёд, при этом мы найдём самый первый символ, с которым его можно переставить. Это будет первый символ на префиксе, который больше его, то есть в этом примере это будет первое вхождение “k”. Найти этот символ мы можем обратным циклом, в котором индекс символа будет уменьшаться. Запомним эти символы в ответе.
Если дальше мы встретим такой же символ, то, поскольку он будет позже, нужно обновить позицию второго символа в ответе. А если мы встретим меньший символ, то мы сможем переставить его с символом, который находится раньше первого запомненного символа. В этом случае опять запустим цикл, идущий к началу строки, в поисках первого символа, который больше данного. Пример такого решения.
s = input()
i = 0
while i < len(s) - 1 and s[i] <= s[i + 1]:
i += 1
ans_i = i
ans_j = i
ans_c = s[i]
for j in range(i + 1, len(s)):
if s[j] == ans_c:
ans_j = j
elif s[j] < ans_c:
ans_c = s[j]
ans_j = j
while ans_i > 0 and s[ans_i - 1] > ans_c:
ans_i -= 1
if ans_i != ans_j:
s = s[:ans_i] + s[ans_j] + s[ans_i + 1:ans_j] + s[ans_i] + s[ans_j + 1:]
print(s)Наше решение на C++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin >> s;
int n = s.size();
vector<int> first(26, n), last(26, -1);
for (int i = 0; i < n; i++) {
int c = s[i] - 'a';
if (first[c] == n) first[c] = i;
last[c] = i;
}
int bestI = n, bestD = 26, bestJ = -1;
for (int c = 0; c < 26; c++) {
int i = first[c];
for (int d = 0; d < c; d++) {
int j = last[d];
if (i < j) {
if (i < bestI || (i == bestI && d < bestD) || (i == bestI && d == bestD && j > bestJ)) {
bestI = i;
bestD = d;
bestJ = j;
}
}
}
}
if (bestI != n) swap(s[bestI], s[bestJ]);
cout << s << "\n";
return 0;
}4. Задачи на печать!
Олимпиады бывают не только личные, но и командные. В командной олимпиаде по программированию обычно принимают участие команды из трёх человек, которым предоставляется один компьютер и один комплект условий задач, причём задач обычно существенно больше, чем в личных олимпиадах.
Условие каждой из задач помещается на одной, двух или трёх страницах. При этом условие может быть напечатано на двух сторонах одного листа, но для удобства команд на одном листе может располагаться условие только одной из задач. Для экономии бумаги, если условие задачи занимает две страницы, оно должно быть напечатано на двух сторонах одного листа, а если из трёх страниц — на двух сторонах одного листа и на одной стороне другого листа, вторая сторона которого останется чистой. При этом можно напечатать первую страницу такой задачи отдельно на чистом листе, а оставшиеся две страницы на одном листе или, наоборот, первые две страницы распечатать на одном листе, а третью на чистом листе. Задачи и все их страницы печатаются последовательно.
Условия всех задач распечатываются на принтере в виде нескольких последовательных заданий. Для каждого задания необходимо задать диапазон печати: номера первой и последней страниц, которые будут напечатаны в этом задании (будут напечатаны все страницы в этом диапазоне), а также тип печати — односторонняя или двусторонняя.
Вам необходимо минимизировать количество заданий для печати условий.
Формат входных данных
Первая строка входных данных содержит целое число () — количество задач в олимпиаде.
Следующие строк содержат по одному целому числу () — количество страниц в -й задаче.
Формат выходных данных
Выведите единственное число — минимальное количество последовательных диапазонов, каждый из которых можно напечатать одной командой односторонней или двусторонней печати так, что условия всех задач будут напечатаны в удобном для командной олимпиады виде.
Система оценки
Решения, верно работающие, когда все значения отличны от 1, будут оцениваться не менее чем в 30 баллов.
Решения, верно работающие, когда все значения отличны от 2, будут оцениваться не менее чем в 30 баллов.
Решения, верно работающие, когда все значения отличны от 3, будут оцениваться не менее чем в 30 баллов.
Решения, верно работающие, когда , будут оцениваться не менее чем в 50 баллов.
Решения, верно работающие, когда , будут оцениваться не менее чем в 80 баллов.
Замечание
В первом примере на олимпиаду предложены 4 задачи, условия которых состоят из 1, 3, 2 и 1 страниц соответственно. Всего необходимо распечатать 7 страниц. Их можно распечатать за два задания: cтраницы 1–2 односторонней печатью (это единственная страница первой задачи и первая из трёх страниц второй задачи), оставшиеся страницы 3–7 — двусторонней.
Во втором примере на олимпиаду предложены 2 задачи, условия которых состоят из 3 страниц каждая. Всего необходимо распечатать 6 страниц. Их можно распечатать за два задания: cтраницы 1–1 односторонней печатью (это первая из трёх страниц первой задачи), оставшиеся страницы 2–6 — двусторонней. При этом в первой задаче первая страница будет напечатана на одной стороне, потому что она напечатана односторонней печатью, а во второй задаче последняя страница будет напечатана на одной стороне, потому что в этом задании нечётное число страниц печатается на двух сторонах, поэтому вторая сторона этого листа будет пустой.
| стандартный ввод | стандартный вывод |
|---|---|
4 1 3 2 1 | 2 |
| стандартный ввод | стандартный вывод |
|---|---|
2 3 3 | 2 |
Официальный разборСначала попробуйте решить сами
Наше решение на C++Полный код на C++17
Официальный разбор
Будем рассматривать задачи последовательно, определяя, в каком режиме должна быть напечатана очередная задача. Например, если в задаче 2 страницы, то её условие должно быть напечатана только в двустороннем режиме. А если в задаче 1 страница, то всё зависит от того, в каком режиме была напечатана предыдущая страница. Если это был односторонний режим, то мы расширим предыдущий диапазон печати на новую задачу, а если двусторонний — то эту одну страницу можно напечатать в двустороннем режиме, однако, следующая страница обязательно должна стать началом нового диапазона печати, который может быть как односторонним, так и двусторонним.
Заведём переменную mode, в которой будет храниться текущий режим печати — 1 для односторонней печати и 2 для двусторонней. Значение 0 означает, что очередная страница должна стать началом нового диапазона печати, который может быть любым. В переменной ans хранится общее число диапазонов печати. В переменной p хранится количество страниц в текущей задаче.
Далее нужно аккуратно разобрать все случаи. Если то мы обязательно переходим в режим 2, при этом если ранее режим был другим, то к ответу прибавляем 1.
Если , то в режиме 1 не нужно делать ничего, в режиме 2 эта страница печатается в двустороннем режиме, но нужно перейти в режим 0 для начала нового диапазона со следующей страницы (потому что на обороте этой страницы ничего нельзя печатать), а в режиме 0 нужно перейти в режим 1, начав новый диапазон, то есть добавив к ответу 1.
Наконец, разберём случай . Если до этого был режим 1, то мы печатаем одну страницу односторонней печатью, а ещё две страницы — двусторонней, поэтому нужно перейти в режим 2. Если мы были в режиме 2, то все три страницы можно напечатать в двустороннем режиме, потом нужно перейти в режим 0, потому что придётся начать новый диапазон. Аналогично поступим, когда режим был равен 0 — начнём новый двусторонний режим, напечатаем три страницы, а затем придётся начать новый диапазон, то есть в этом случае нужно просто увеличить значение ans на 1, сохранив значение mode равным 0.
Пример такого решения.
ans = 0
mode = 0
n = int(input())
for i in range(n):
p = int(input())
if p == 1:
if mode == 2:
mode = 0
elif mode == 0:
mode = 1
ans += 1
if p == 2:
if mode != 2:
ans += 1
mode = 2
if p == 3:
if mode == 1:
mode = 2
ans += 1
elif mode == 2:
mode = 0
else:
ans += 1
print(ans)Частичные решения можно получить используя полный перебор вариантов.
Задачу можно решить и динамическим программированием, в котором целевой функцией будет количество диапазонов, необходимое для печати первых задач. При этом придется ввести и второй параметр, аналогичный по смыслу переменной mode.
Наше решение на C++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
int mode = 0;
long long answer = 0;
for (int k = 0; k < n; k++) {
int pages;
cin >> pages;
if (pages == 1) {
if (mode == 2) {
mode = 0;
} else if (mode == 0) {
mode = 1;
answer++;
}
} else if (pages == 2) {
if (mode != 2) {
answer++;
mode = 2;
}
} else {
if (mode == 1) {
mode = 2;
answer++;
} else if (mode == 2) {
mode = 0;
} else {
answer++;
}
}
}
cout << answer << "\n";
return 0;
}5. Вечер кёрлинга
Сегодня вечером по телевизору на разных каналах будут показывать матчей по кёрлингу, причём -й матч начинается в момент времени и заканчивается в момент времени .
Василиса хочет посмотреть как можно больше матчей от начала до конца. При этом если какой-то матч заканчивается в момент времени , то она может после него посмотреть любой матч , который начинается не раньше момента времени , то есть (Василиса может моментально переключить каналы в момент окончания матча и начать смотреть новый матч). Также она хочет сделать перерыв длины хотя бы между какими-то двумя играми, чтобы поужинать, то есть должны найтись два последовательных матча и , которые просмотрит Василиса, удовлетворяющие условию . Перерыв не может быть до или после всех просмотренных игр.
Помогите Василисе составить набор, содержащий максимальное количество матчей, которые она сможет просмотреть полностью и при этом сделать перерыв продолжительностью не менее между какими-то матчами, или определите, что такого набора не существует.
Формат входных данных
Первая строка входных данных содержит число () — количество показываемых матчей.
Вторая строка входных данных содержит число () — минимальная длина перерыва, который должна сделать Василиса.
В следующих строках содержится по два числа и () — начало и конец -го матча.
Формат выходных данных
Программа должна вывести число — максимально возможное количество матчей, которые просмотрит Василиса. Во второй строке выведите чисел через пробел — номера матчей, которые должна посмотреть Василиса, в порядке просмотра.
Если Василиса не может составить расписание хотя бы из двух матчей так, чтобы между какими-то двумя матчами был перерыв хотя бы , то выведите число .
Система оценки
Решения, верно работающие при , будут оцениваться в 20 баллов.
Решения, верно работающие при , будут оцениваться в 40 баллов.
Решения, верно работающие при , будут оцениваться в 30 баллов.
Замечание
В первом примере ответом будет последовательность матчей 6, 3, 5. Василиса сначала посмотрит матч 6, который заканчивается в момент времени 4, потом переключится на матч 3, который продолжается с 4 до 6. Затем она сделает перерыв с 6 по 10, после чего просмотрит матч номер 5 с 10 до 12. Получилось расписание из 3 матчей с перерывом, продолжительность которого равна 4. Заметим, что в данном примере правильным ответом также будет последовательность матчей 6, 4, 5, в этом случае продолжительность перерыва между матчами 4 и 5 будет равна 3.
Во втором примере всего два матча, первый заканчивается в 5, а второй начинается в 9, то есть составить расписание, в котором был бы перерыв продолжительностью не менее , нельзя.
| стандартный ввод | стандартный вывод |
|---|---|
6 3 8 13 1 5 4 6 4 7 10 12 2 4 | 3 6 3 5 |
| стандартный ввод | стандартный вывод |
|---|---|
2 5 1 5 9 13 | -1 |
Официальный разборСначала попробуйте решить сами
Наше решение на C++Полный код на C++17
Официальный разбор
Будем говорить о матчах, как об отрезках на прямой, у которых левый конец — момент начала матча, правый конец — момент окончания матча. Также будем говорить о том, что два отрезка не пересекаются, если левый конец одного отрезка не меньше правого конца другого отрезка. Концы могут и совпадать, но это допускается в условии этой задачи.
Без условия о наличии перерыва эта задача решается при помощи жадного алгоритма. Отсортируем все отрезки по правому концу, и будем перебирать их в порядке неубывания правого конца. Если левый конец очередного рассматриваемого отрезка не меньше, чем правый конец последнего выбранного отрезка, то добавляем отрезок в множество выбранных. То есть мы выбираем очередной отрезок, как отрезок с минимальным правым концом, который не пересекается с последним ранее выбранным отрезком.
Заметим, что этот алгоритм также находит способ выбрать отрезков так, чтобы они попарно не пересекались, а правый конец последнего выбранного отрезка был как можно меньше.
Если этот алгоритм применить ещё раз, но только от конца к началу, то можно решить задачу “с конца”: для каждого мы найдём способ выбрать отрезков так, чтобы они не пересекались и начало первого из них был как можно больше.
Теперь научимся обрабатывать перерыв. Для этого будем перебирать количество матчей, которые Василиса просмотрит до перерыва. Мы уже нашли минимальное время, за которое Василиса может просмотреть эти матчи. Добавим к этому времени продолжительность перерыва. Затем, используя ранее найденные значения, найдём какое максимальное количество матчей можно просмотреть за оставшееся время. Для этого будем использовать метод двух указателей: если какому-то количеству матчей, которые Василиса просмотрит до перерыва, соответствует матчей, которые можно просмотреть после перерыва, то при увеличении значение будет уменьшаться. Поэтому сложность такого алгоритма после сортировки будет , а вместе с сортировкой — .
n = int(input())
t = int(input())
games = []
for i in range(1, n + 1):
l, r = map(int, input().split())
games.append([i, l, r])
games_sorted_end = sorted(games, key = lambda elem: elem[2])
min_end_time = [0]
min_end_game = [0]
prev_game = [0] * (n + 1)
for i, l, r in games_sorted_end:
if l >= min_end_time[-1]:
prev_game[i] = min_end_game[-1]
min_end_time.append(r)
min_end_game.append(i)
INF = 2 * 10**9
games_sorted_beg = sorted(games, key = lambda elem: elem[1], reverse=True)
max_begin_time = [INF]
max_begin_game = [0]
next_game = [0] * (n + 1)
for i, l, r in games_sorted_beg:
if r <= max_begin_time[-1]:
next_game[i] = max_begin_game[-1]
max_begin_time.append(l)
max_begin_game.append(i)
ans_m = -1
game_before_break = 0
game_after_break = 0
j = len(max_begin_time) - 1
for i in range(1, len(min_end_time)):
while max_begin_time[j] - min_end_time[i] < t:
j -= 1
if j > 0 and i + j > ans_m:
ans_m = i + j
game_before_break = min_end_game[i]
game_after_break = max_begin_game[j]
print(ans_m)
if ans_m > 0:
ans = []
curr = game_before_break
while curr:
ans.append(curr)
curr = prev_game[curr]
ans = ans[::-1]
curr = game_after_break
while curr:
ans.append(curr)
curr = next_game[curr]
print(" ".join(map(str, ans)))Возможные частичные неэффективные решения могут использовать перебор или динамическое программирование.
В решении с перебором при нужно отсортировать все матчи, зачем перебрать подмножеств выбранных матчей и проверить, что они удовлетворяют условию непересечения матчей и наличия перерыва, затем выбрать подходящее подмножество, содержащее наибольшее число элементов.
В решении динамическим программированием можно рассмотреть целевую функцию — минимальное время, за которое можно просмотреть матчей. Значение будет равно 0 или 1 и означает отсутствие или наличие перерыва в выбранной последовательности из матчей. Такое решение будет иметь сложность .
Наше решение на C++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
long long breakLength;
cin >> n >> breakLength;
vector<long long> l(n + 1), r(n + 1);
for (int i = 1; i <= n; i++) cin >> l[i] >> r[i];
vector<int> byEnd(n), byBegin(n);
iota(byEnd.begin(), byEnd.end(), 1);
iota(byBegin.begin(), byBegin.end(), 1);
stable_sort(byEnd.begin(), byEnd.end(), [&](int a, int b) { return r[a] < r[b]; });
stable_sort(byBegin.begin(), byBegin.end(), [&](int a, int b) { return l[a] > l[b]; });
vector<long long> minEndTime = {0};
vector<int> minEndGame = {0};
vector<int> prevGame(n + 1, 0);
for (int i : byEnd) {
if (l[i] >= minEndTime.back()) {
prevGame[i] = minEndGame.back();
minEndTime.push_back(r[i]);
minEndGame.push_back(i);
}
}
const long long INF = 4e18;
vector<long long> maxBeginTime = {INF};
vector<int> maxBeginGame = {0};
vector<int> nextGame(n + 1, 0);
for (int i : byBegin) {
if (r[i] <= maxBeginTime.back()) {
nextGame[i] = maxBeginGame.back();
maxBeginTime.push_back(l[i]);
maxBeginGame.push_back(i);
}
}
int best = -1, gameBefore = 0, gameAfter = 0;
int j = (int)maxBeginTime.size() - 1;
for (int i = 1; i < (int)minEndTime.size(); i++) {
while (maxBeginTime[j] - minEndTime[i] < breakLength) j--;
if (j > 0 && i + j > best) {
best = i + j;
gameBefore = minEndGame[i];
gameAfter = maxBeginGame[j];
}
}
cout << best << "\n";
if (best > 0) {
vector<int> order;
for (int cur = gameBefore; cur; cur = prevGame[cur]) order.push_back(cur);
reverse(order.begin(), order.end());
for (int cur = gameAfter; cur; cur = nextGame[cur]) order.push_back(cur);
for (size_t k = 0; k < order.size(); k++) {
if (k) cout << ' ';
cout << order[k];
}
cout << "\n";
}
return 0;
}Сначала решите сами
Возьмите столько времени, сколько было на олимпиаде, и только потом открывайте разбор.
Другие туры
2026/27
- Школьный этап, 9–11 классы5 задач
- Школьный этап, 7–8 классы6 задач
- Школьный этап, 5–6 классы5 задач
2025/26
- Школьный этап, 9–11 классы5 задач
- Школьный этап, 7–8 классы6 задач
- Школьный этап, 5–6 классы5 задач
- Муниципальный этап, 9–11 классы5 задач
- Муниципальный этап, 7–8 классы6 задач
2024/25
- Школьный этап, 9–11 классы5 задач
- Школьный этап, 7–8 классы7 задач
- Школьный этап, 5–6 классы5 задач
- Муниципальный этап, 9–11 классы5 задач
- Муниципальный этап, 7–8 классы7 задач
2023/24
- Школьный этап, 9–11 классы5 задач
- Школьный этап, 7–8 классы7 задач
- Школьный этап, 5–6 классы5 задач
- Муниципальный этап, 9–11 классы5 задач
- Муниципальный этап, 7–8 классы7 задач