Стоимость скобочной последовательности ограничение...

Créé le : 9 juin 2026

Répondu en utilisant GPT-5.5 par Chat01

Question

Стоимость скобочной последовательности
ограничение по времени на тест
2 секунды
ограничение по памяти на тест
512 мегабайт

Пусть стоимость произвольной скобочной строки — это длина её максимальной подпоследовательности∗, которая является правильной скобочной последовательностью†.
Дана скобочная строка s и целое число k. Ваша задача удалить из строки s не более k символов, таким образом, чтобы стоимость получившейся строки была минимальной.
∗Последовательность a является подпоследовательностью b, если a может быть получена из b удалением нескольких (возможно, ни одного или всех) элементов на произвольных позициях.
†Скобочная последовательность называется правильной, если в неё можно вставить символы + и 1 так, чтобы получить корректное арифметическое выражение. Например, последовательности «(())()», «()» и «(()(()))» являются правильными, а «)(», «(()» и «(()))(» — нет.
Входные данные
Каждый тест состоит из нескольких наборов входных данных. В первой строке находится одно целое число t (1≤t≤103) — количество наборов входных данных. Далее следует описание наборов входных данных.
В первой строке каждого набора входных данных содержатся два целых числа n и k (1≤n≤5000; 0≤k≤n) — длина строки s и максимальное количество удалений.
Во второй строке каждого набора входных данных содержится s длины n и состоящая из символов «(» и «)».
Дополнительные ограничения на входные данные:
сумма n по всем наборам входных данных не превосходит 5000.
Выходные данные
Для каждого набора входных данных выведите бинарную строку длины n. i-й символ должен быть равен «1», если соответствующий символ строки s удаляется, и «0» иначе.
Количество единиц в строке не должно превышать k. Стоимость строки, полученной после удаления отмеченных символов, должна быть минимально возможной.
Если существует несколько ответов, выведите любой.

#include <iostream>
#include <vector>
using namespace std;

int main() {
ios_base::sync_with_stdio(0);
cin.tie(0);

text
int t; cin >> t; while (t--) { int n, k; cin >> n >> k; string s; cin >> s; int ko = 0, kz = 0; for (int i = 0; i < n; ++i) { if (s[i] == '(') ko++; else kz++; } vector<int> ans(n); if (ko <= kz && k != 0) { int b = 0; for (int i = 0; i < n; ++i) { if (s[i] == '(') { b++; if (b > 0) { ans[i] = 1; k--; } } else b--; if (k == 0) break; } } else if (k != 0) { int b = 0; for (int i = 0; i < n; ++i) { if (s[i] == ')') { b--; if (b >= 0) { ans[i] = 1; k--; } } else b++; if (k == 0) break; } } for (auto u : ans) cout << u; cout << '\n'; } return 0;

}

намекни как мне минимально изменяя код сделать его верным, и потом исправь его МИНИМАЛЬНО его изменяя и все подробно объясняя

Réflexion

Minimizing cost by selecting an optimal split

I need a way to minimize the cost by selecting a position "p" such that the prefix consists of opening parentheses and the suffix consists of closing ones. The optimal solution relies on selecting a vertex cover to minimize matching. For the string "())", I test different splits and find the minimal cover by considering positions of opened and closed parentheses. It works when p=3.

Optimizing positions to minimize deletions

For the string ")((()", I’m calculating optimal positions (p). For example, for "())(()", calculating the prefix and suffix of '(' and ')' helps minimize the cost. The goal is to delete up to "k" characters in the most efficient way to achieve a minimal cost. This involves applying vertex cover concepts to manage parentheses. I'm comparing the current approach to see where it fails, particularly with strings like "())".

Thought for 1m 23s

Réponse

Намёк: не надо решать, удалять ( или ) по их общему количеству. Важно не сколько всего скобок каждого типа, а какие из них образуют покрытие всех возможных пар ( перед ).

Текущий код ломается, например, на:

text
1 6 1 ))((()

Оптимально удалить последнюю ), тогда стоимость станет 0.

А твой код при ko <= kz удалит первую (, получится:

text
))(()

и там всё ещё есть подпоследовательность "()", то есть стоимость 2.


Идея минимальной правки

Стоимость строки — это:

text
2 * количество пар, которые можно сопоставить

Пару можно сделать, если есть ( слева и ) справа.

Рассмотрим некоторую границу split.

Возьмём множество:

text
все '(' слева от границы + все ')' справа от границы

Такое множество пересекает любую возможную пару ( ... ).
Если удалить все эти символы, пар точно не останется.

Нам надо найти границу, где это множество минимально.

Для границы после позиции i размер множества равен:

text
количество '(' в префиксе + количество ')' в суффиксе

Это можно переписать как:

text
total_close + balance

где:

cpp
balance = открывающие_в_префиксе - закрывающие_в_префиксе

Значит надо найти позицию, где balance минимален.


Минимально исправленный код

Я оставил структуру твоего кода почти такой же: чтение, ans, вывод. Заменена только неправильная логика выбора удаляемых символов.

cpp
#include <iostream> #include <vector> using namespace std; int main() { ios_base::sync_with_stdio(0); cin.tie(0); int t; cin >> t; while (t--) { int n, k; cin >> n >> k; string s; cin >> s; int ko = 0, kz = 0; for (int i = 0; i < n; ++i) { if (s[i] == '(') ko++; else kz++; } vector<int> ans(n); int b = 0; int mn = 0; int split = -1; for (int i = 0; i < n; ++i) { if (s[i] == '(') b++; else b--; if (b < mn) { mn = b; split = i; } } for (int i = 0; i < n && k > 0; ++i) { if (i <= split && s[i] == '(') { ans[i] = 1; k--; } else if (i > split && s[i] == ')') { ans[i] = 1; k--; } } for (auto u : ans) cout << u; cout << '\n'; } return 0; }

Что именно изменилось

Вместо этого блока:

cpp
if (ko <= kz && k != 0) { ... } else if (k != 0) { ... }

мы теперь делаем так:

cpp
int b = 0; int mn = 0; int split = -1; for (int i = 0; i < n; ++i) { if (s[i] == '(') b++; else b--; if (b < mn) { mn = b; split = i; } }

Здесь split — позиция, после которой выгодно провести границу.

Потом удаляем символы из множества:

cpp
i <= split && s[i] == '('

или

cpp
i > split && s[i] == ')'

То есть:

text
'(' слева от границы ')' справа от границы

Почему этого достаточно

Любая правильная пара имеет вид:

text
'(' раньше, ')' позже

Если ( стоит слева от split, мы можем покрыть пару удалением этой (.

Если ( стоит справа от split, то соответствующая ) тоже обязательно справа от split, значит пару можно покрыть удалением этой ).

То есть выбранное множество символов пересекает каждую возможную пару.

Если удалить один символ из такого множества, максимальное число пар уменьшится ровно на 1, пока пары ещё есть. Поэтому можно просто удалить первые k подходящих символов.

Если k больше, чем нужно, удалятся не все k, а только полезные символы. Это разрешено, потому что по условию надо удалить не более k символов.

Question

Ограничение времени: 2 с
Ограничение реального времени: 5 с
Ограничение памяти: 64M
Задача J: Разреженные таблицы

Условие задачи (pdf)

Дан массив из n чисел. Требуется написать программу, которая будет отвечать на запросы следующего вида: найти минимум на отрезке между u и v включительно.

Формат входных данных

В первой строке входного файла даны три натуральных числа n, m (1⩽n⩽105, 1⩽m⩽107) и a1 (0⩽a1<16714589) — количество элементов в массиве, количество запросов и первый элемент массива соответственно. Вторая строка содержит два натуральных числа u1 и v1 (1⩽u1,v1⩽n) — первый запрос.

Элементы a2,a3,…,an задаются следующей формулой:ai+1=(23⋅ai+21563)mod16714589.

Например, при n=10, a1=12345 получается следующий массив: a = (12345, 305498, 7048017, 11694653, 1565158, 2591019, 9471233, 570265, 13137658, 1325095).

Запросы генерируются следующим образом:

ui+1=((17⋅ui+751+ansi+2i)modn)+1,
vi+1=((13⋅vi+593+ansi+5i)modn)+1,

где ansi — ответ на запрос номер i.

Обратите внимание, что ui может быть больше, чем vi.

Формат выходных данных

В выходной файл выведите um, vm и ansm (последний запрос и ответ на него).

#include <iostream>
#include <vector>
using namespace std;

const int mod = 16714589;
vector<long long> a, logs;
vector<vector<long long>> table;

long long get(int l, int r) {
int len = r - l + 1;
int k = logs[len];

return min(table[k][l], table[k][r - (1 << k) + 1]);

}

int main() {
ios_base::sync_with_stdio(0);
cin.tie(0);

text
int n, m, a1; cin >> n >> m >> a1; a.resize(n); a[0] = a1; for (int i = 1; i < n; ++i) { a[i] = (a[i - 1] * 23 + 21563) % mod; } logs.resize(100002); logs[1] = 0; for (int i = 2; i <= 100000; ++i) { logs[i] = logs[i / 2] + 1; } int lg = logs[n]; table.resize(lg + 1, vector<long long> (n)); for (int i = 0; i < n; ++i) table[0][i] = a[i]; for (int k = 1; k <= lg; ++k) { for (int j = 0; j <= n - (1 << k); ++j) { table[k][j] = min(table[k - 1][j], table[k - 1][j + (1 << (k - 1))]); } } int u1, v1; cin >> u1 >> v1; vector<int> u(m + 1), v(m + 1); u[0] = u1; v[0] = v1; for (int i = 0; i < m; ++i) { int l = min(u[i], v[i]) - 1; int r = max(u[i], v[i]) - 1; long long ans = get(l, r); u[i + 1] = ((((17LL * u[i] + 751 + ans + 2LL * (i + 1)) % n) + n) % n) + 1; v[i + 1] = ((((13LL * v[i] + 593 + ans + 5LL * (i + 1)) % n) + n) % n) + 1; if (i == m - 1) { cout << u[i] << ' ' << v[i] << ' ' << ans; } } return 0;

}
что не так? исправь мой код, МИНИМАЛЬНО его изменяя и ОЧЕНБ ПОДРОБНО все объясняя

Réflexion

Разберу именно твой код, а не буду переписывать с нуля: здесь алгоритм почти правильный, основная проблема — память и хранение всех запросов.

Thought for 7s

Réponse

Главная ошибка: ты хранишь все u и v:

cpp
vector<int> u(m + 1), v(m + 1);

А m до 10^7.

Это примерно:

text
u: 40 МБ v: 40 МБ

Уже 80 МБ, а лимит — 64 МБ. Поэтому будет превышение памяти.

Нужно хранить только текущие u, v, потому что каждый следующий запрос зависит только от предыдущего.

Ещё лучше заменить long long в таблице на int, потому что элементы массива меньше 16714589.


Минимально исправленный код:

cpp
#include <iostream> #include <vector> using namespace std; const int mod = 16714589; vector<int> a, logs; vector<vector<int>> table; int get(int l, int r) { int len = r - l + 1; int k = logs[len]; return min(table[k][l], table[k][r - (1 << k) + 1]); } int main() { ios_base::sync_with_stdio(0); cin.tie(0); int n, m, a1; cin >> n >> m >> a1; a.resize(n); a[0] = a1; for (int i = 1; i < n; ++i) { a[i] = (1LL * a[i - 1] * 23 + 21563) % mod; } logs.resize(n + 1); logs[1] = 0; for (int i = 2; i <= n; ++i) { logs[i] = logs[i / 2] + 1; } int lg = logs[n]; table.resize(lg + 1, vector<int>(n)); for (int i = 0; i < n; ++i) table[0][i] = a[i]; for (int k = 1; k <= lg; ++k) { for (int j = 0; j <= n - (1 << k); ++j) { table[k][j] = min(table[k - 1][j], table[k - 1][j + (1 << (k - 1))]); } } int u, v; cin >> u >> v; int ans = 0; for (int i = 1; i <= m; ++i) { int l = min(u, v) - 1; int r = max(u, v) - 1; ans = get(l, r); if (i == m) { cout << u << ' ' << v << ' ' << ans; break; } int nu = ((17LL * u + 751 + ans + 2LL * i) % n) + 1; int nv = ((13LL * v + 593 + ans + 5LL * i) % n) + 1; u = nu; v = nv; } return 0; }

Что изменилось минимально:

cpp
vector<long long> a, logs; vector<vector<long long>> table;

заменили на:

cpp
vector<int> a, logs; vector<vector<int>> table;

Потому что значения маленькие, long long не нужен.

Было:

cpp
vector<int> u(m + 1), v(m + 1);

Стало:

cpp
int u, v;

Потому что нам не нужны все запросы, нужен только текущий.

Цикл стал таким:

cpp
for (int i = 1; i <= m; ++i)

Потому что формулы используют номер запроса i, начиная с 1.

И важно: сначала считаем ответ на текущий запрос, потом, если это не последний запрос, генерируем следующий.

Question

D. Совершенно новое татарское телешоу
ограничение по времени на тест
2 секунды
ограничение по памяти на тест
256 мегабайт
Дабиру и Егору было недостаточно славы с предыдущего выпуска, что они решили снова устроить телешоу: ребята играют в свою любимую игру на массиве a и с их любимым числом k.

Дабир ходит первым. Первым ходом выбирается любое число из массива и удаляется. Если на предыдущем ходу был выбран элемент равный x, то на текущем ходу игрок должен выбрать из оставшихся такой элемент y что 0≤y−x≤k и он будет удалён. Игрок, который не может сделать ход проигрывает.
Но так как это была не простая игра, а настоящий шоу-матч, то на игру вновь пригласили Арсения (aka MAKAN) — главную знаменитость Омска. На правах приглашённой знаменитости Арсению дали возможность сделать первый ход в этом матче, то есть сделать самый первый ход в игре вместо Дабира. Но оказалось, что Арсений поклонник Егора, а потому хочет сделать первый ход такой, чтобы у Егора была выигрышная стратегия при любой игре Дабира.
Определите, может ли Арсений сделать первый ход за Дабира так, чтобы при любых ходах Дабира победил Егор.
Входные данные
Каждый тест состоит из нескольких наборов входных данных. В первой строке вводится одно целое число t (1≤t≤104) — количество наборов входных данных.
Первая строка каждого набора входных данных содержит одно целое число n и k (1≤n,k≤2⋅105) — длину массива a и любимое число Дабира и Егора.
Вторая строка содержит n целых чисел a1,a2,…,an (1≤ai≤n).
Гарантируется, что сумма значений n по всем наборам входных данных не превышает 2⋅105.
Выходные данные
Для каждого набора входных данных, если существует такой первый ход, что при дальнейшей оптимальной игре обоих игроков Егор победит, то выведите «YES», иначе выведите «NO».
Вы можете выводить «YES» и «NO» в любом регистре (например, строки «yES»,«yes» и «Yes» будут распознаны как правильный ответ).

#include <iostream>
#include <vector>
#include <set>
#include <map>
using namespace std;

int main() {
ios_base::sync_with_stdio(0);
cin.tie(0);

text
int t; cin >> t; while (t--) { int n, k; cin >> n >> k; set<int> d; vector<int> a(n); for (int i = 0; i < n; ++i) { cin >> a[i]; d.insert(a[i]); } map<int, int> m; for (int i = 0; i < n; ++i) { m[a[i]]++; } vector<int> s; for (auto u : d) s.push_back(u); reverse(s.begin(), s.end()); vector<pair<int, int>> v(n); int id = 0; int pr = -1; for (auto u : s) { if (pr == -1 || pr - u > k) { v[id].first = u; id++; } else if (v[id - 1].second == 0) v[id - 1].second = u; } bool fl = false; for (int i = 0; i < id; ++i) { int m1 = v[i].first, m2 = v[i].second; if (m[m1] % 2 == 0) { cout << "YES\n"; fl = true; break; } else if (m2 != 0 && m1 - m2 <= k) { cout << "YES\n"; fl = true; break; } } if (!fl) cout << "NO\n"; } return 0;

}

C. Омские программисты
ограничение по времени на тест
1 секунда
ограничение по памяти на тест
256 мегабайт
На главной площади Омска проходит ежегодное мероприятие — ярмарка программистов. Вы, как главный программист Омска, решили поучаствовать в данном прекрасном мероприятии и выдвинулись туда. На входе охранник решил проверить ваши навыки и задал вам задачу:

У вас есть три целых числа a, b, x. Вы хотите сделать a и b равными. Для этого вы можете применять следующие операции:
Выбрать одно из чисел a или b и прибавить к нему 1.
Выбрать одно из чисел a или b и разделить его с округлением вниз на x.
Необходимо найти минимальное количество операций, после которых a станет равным b. Сможете ли вы доказать свои навыки, или вам придется поехать домой?
Входные данные
В первой строке записано одно целое число t (1≤t≤104) — количество наборов входных данных.
Далее следуют t наборов входных данных.
В единственной строке каждого набора данных дано три целых числа a, b, x (1≤a,b≤109, 2≤x≤109).
Выходные данные
Для каждого набора входных данных выведите целое число, обозначающее минимальное количество операций, необходимых, чтобы сделать a и b равными.
#include <iostream> using namespace std; int main() { ios_base::sync_with_stdio(0); cin.tie(0); int t; cin >> t; while (t--) { int a, b, x; cin >> a >> b >> x; int ans = 0; while (a != b) { if (a > b) swap(a, b); int k = b - a; int r1 = b / x - a / x + 2; int r2; if (b / x < a) r2 = a - b / x + 1; else r2 = b / x - a + 1; if (k <= r1 && k <= r2) { ans += k; break; } if (r1 < r2) { ans += 2; b /= x; a /= x; } else { ans++; b /= x; } } cout << ans << '\n'; } return 0; }

что не так? исправь мои коды МИНИМАЛЬНО их изменяя и ОЧЕНЬ ПОДРОБНО ОБЪЯСНЯЯ все исправления и мои ошибки

Réflexion

По второй задаче видно, что жадный подход с r1/r2 ненадёжен: операции “+1” и “/x” лучше рассматривать как короткий путь по состояниям после делений. По первой задаче ошибка глубже: группировка по “кускам” и чётности максимума не описывает игру.

Thought for 42s

Réponse

Разберу обе задачи.


1. D. Совершенно новое татарское телешоу

Главная ошибка

Твой код пытается делить значения на группы и смотреть только:

cpp
m[m1] % 2

и наличие второго элемента в группе.

Но игра устроена не так. Важно не просто количество максимального элемента, а есть ли дальше проигрышная позиция, в которую можно сходить.

Ещё в твоём коде есть техническая ошибка:

cpp
int pr = -1; for (auto u : s) { if (pr == -1 || pr - u > k) { ... } }

Ты нигде не делаешь:

cpp
pr = u;

То есть pr всегда остаётся -1, и группировка вообще не работает.


Правильная идея

Пусть есть некоторое значение x.

После того как Арсений выбрал x, следующий ход делает Егор.

Егор может брать:

text
y >= x и y - x <= k

То есть игра всегда идёт вправо по значениям, назад вернуться нельзя.

Для каждого различного значения x посчитаем:

cpp
lose[x]

Это значит: если предыдущим ходом уже взяли x, и сейчас ход текущего игрока, то текущий игрок проигрывает.

Если количество x нечётное, то после первого взятия x остаётся чётное число таких же x.

Если из x нельзя перейти в уже проигрышную позицию, то позиция проигрышная.

Формула такая:

cpp
lose[i] = cnt[i] % 2 == 1 && нет проигрышного значения справа на расстоянии <= k

Почему cnt[i] % 2 == 1?

Потому что Арсений уже взял один элемент x.

Осталось:

cpp
cnt[x] - 1

Если cnt[x] нечётное, то cnt[x] - 1 чётное. Тогда игра на одинаковых x сама по себе проигрышная, если нет хорошего перехода вправо.


Минимально исправленный код

Я оставил твои set, map, vector, но полностью заменил неправильную логику группировки.

cpp
#include <iostream> #include <vector> #include <set> #include <map> #include <algorithm> using namespace std; int main() { ios_base::sync_with_stdio(0); cin.tie(0); int t; cin >> t; while (t--) { int n, k; cin >> n >> k; set<int> d; vector<int> a(n); for (int i = 0; i < n; ++i) { cin >> a[i]; d.insert(a[i]); } map<int, int> m; for (int i = 0; i < n; ++i) { m[a[i]]++; } vector<int> s; for (auto u : d) s.push_back(u); set<int> lose_values; bool fl = false; for (int i = (int)s.size() - 1; i >= 0; --i) { int x = s[i]; auto it = lose_values.upper_bound(x); bool has_lose = false; if (it != lose_values.end() && *it - x <= k) { has_lose = true; } bool lose = (m[x] % 2 == 1 && !has_lose); if (!lose) { fl = true; } else { lose_values.insert(x); } } if (fl) cout << "YES\n"; else cout << "NO\n"; } return 0; }

Что изменилось

Было:

cpp
vector<pair<int, int>> v(n); int id = 0; int pr = -1; ...

Это удалено, потому что такая группировка не описывает игру.

Добавили:

cpp
set<int> lose_values;

Тут храним значения, из которых следующий игрок проигрывает.

Проверяем, есть ли проигрышное значение справа:

cpp
auto it = lose_values.upper_bound(x); bool has_lose = false; if (it != lose_values.end() && *it - x <= k) { has_lose = true; }

Если есть проигрышное значение справа на расстоянии <= k, значит текущая позиция выигрышная: можно сходить туда.

Считаем:

cpp
bool lose = (m[x] % 2 == 1 && !has_lose);

Если позиция не проигрышная, значит Арсений может выбрать это значение, и Егор будет выигрывать:

cpp
if (!lose) { fl = true; }

2. C. Омские программисты

Главная ошибка

Твой код жадный:

cpp
if (r1 < r2) { ans += 2; b /= x; a /= x; } else { ans++; b /= x; }

Но локально лучший ход не всегда ведёт к глобальному минимуму.

Тут надо перебрать, сколько раз мы делим a, и сколько раз делим b.

После этого меньшее число можно догнать операциями +1.

То есть вариант такой:

text
поделили a i раз поделили b j раз потом прибавили разницу

Цена:

cpp
i + j + abs(a_after - b_after)

Так как a, b <= 1e9, делений будет мало: максимум около 31 при x = 2.


Минимально исправленный код

cpp
#include <iostream> #include <vector> #include <cmath> using namespace std; int main() { ios_base::sync_with_stdio(0); cin.tie(0); int t; cin >> t; while (t--) { long long a, b, x; cin >> a >> b >> x; vector<long long> va, vb; long long cur = a; while (true) { va.push_back(cur); if (cur == 0) break; cur /= x; } cur = b; while (true) { vb.push_back(cur); if (cur == 0) break; cur /= x; } long long ans = abs(a - b); for (int i = 0; i < (int)va.size(); ++i) { for (int j = 0; j < (int)vb.size(); ++j) { ans = min(ans, 1LL * i + j + abs(va[i] - vb[j])); } } cout << ans << '\n'; } return 0; }

Что здесь происходит

Например, для числа a строим все варианты после делений:

cpp
vector<long long> va; long long cur = a; while (true) { va.push_back(cur); if (cur == 0) break; cur /= x; }

То есть если:

text
a = 20, x = 3

то получим:

text
20, 6, 2, 0

Индексы в этом массиве — это количество операций деления.

Например:

cpp
va[2] = 2

значит:

text
20 / 3 / 3 = 2

и это стоит 2 операции.

Потом перебираем:

cpp
for (int i = 0; i < va.size(); ++i) { for (int j = 0; j < vb.size(); ++j) { ans = min(ans, i + j + abs(va[i] - vb[j])); } }

Смысл:

text
i делений сделали с a j делений сделали с b потом меньшее число увеличили до большего

Количество +1 операций равно:

cpp
abs(va[i] - vb[j])

Итого:

cpp
i + j + abs(va[i] - vb[j])

Почему это быстро

Даже если a = 1e9, а x = 2, цепочка делений короткая:

text
1e9, 5e8, 2.5e8, ..., 0

Это примерно 31 значение.

Значит на один тест:

text
31 * 31

операций, что очень мало.

Partager cette Q&R