Search for a command to run...
Progressive hints first, then the full explanation and implementation when you're ready to cash out.
Review status
AI-generated and still unreviewed. Double-check the details before internalizing them.
Hints
Open only as much as you need to keep the solve alive.
Think of the final string as a set of pairs (i, j) with i < j. Each pair is colored round or square, then becomes either () or [].
A pair costs only if the original characters already are () or []. For each bracket type, greedily delete the maximum number of those free pairs with a stack.
After deleting all free pairs, no remaining pair can cost . A remaining pair costs only when its left endpoint is a closing bracket and its right endpoint is an opening bracket. Everything else can keep one endpoint, so it costs .
So the leftover part costs at least leftover_length / 2. The only question is whether you are forced to use one bad closing-opening pair.
Let the leftover counts be open and close. If one count is even, same-orientation pairs avoid the bad case. If both are odd, an opening before a later closing also fixes it; otherwise the leftover shape is closings* openings*, and exactly one extra operation is unavoidable.
Research basis: I checked the official Codeforces editorial/solution, the accepted submissions status page, and independent explanations by Daniel Z and kmjp. They all point to the same core idea: delete already-good same-type pairs, then solve a tiny parity problem on the leftovers.
A useful rephrase: a beautiful string is equivalent to partitioning all positions into pairs (i, j) with i < j, and coloring each pair round or square. The left endpoint becomes the opening bracket of that color, and the right endpoint becomes the closing bracket. For each color, every close has its own earlier open, so the subsequence is correct. Conversely, any correct bracket subsequence can be matched this way.
Now look at the cost of one pair. If the original two characters are already () or [], the pair costs . If not, then the pair costs unless the left character is a closing bracket and the right character is an opening bracket. That cursed orientation costs , because both endpoints must flip direction. So relative to the normal cost , free same-type pairs save one operation, and closing-opening pairs add one operation.
First, take all free pairs we can. For each type independently, scan the string with a stack: push opens, and when a matching close appears, pop one open; otherwise the close stays unmatched. This is exactly the standard greedy maximum matching for a bracket subsequence, so it finds the maximum number of original () pairs and original [] pairs that can remain unchanged.
Why is this safe? A skipped free pair can save at most one later bad-pair penalty, but skipping it already loses one zero-cost saving, so it never improves the answer. Among maximum choices, the stack version is also the extremal useful one: unmatched opens are as early as possible, and unmatched closes are as late as possible. Therefore, if this leftover string has no opening before a later closing, no other maximum deletion can magically create one. No DP, no flow, no 2500-rated ritual sacrifice.
Let be the leftover string after those stack deletions. Let , open be the number of ( or [ in , and close be the number of ) or ] in . Since all zero-cost pairs are gone, every remaining pair costs at least , so the base lower bound is .
When can we achieve exactly ?
If open and close are both even, pair openings with openings and closings with closings. An opening-opening pair keeps the left endpoint; a closing-closing pair keeps the right endpoint. Every pair costs .
If both counts are odd, same-orientation pairing leaves one opening and one closing. If contains an opening before a later closing, then there is an adjacent opening-closing transition somewhere in 1`, and now both remaining counts are even. Again the total is .
The only bad case is: both counts are odd, and no opening appears before a closing. Then must look like all closings first, then all openings: closings* openings*. Since both counts are odd, at least one mixed-orientation pair is forced. But every mixed pair is then closing-opening, so it costs instead of . This adds exactly one extra operation, and one such bad pair is enough.
So the answer is:
m / 2 + 1 only if open and close are odd and there is no opening-closing transition in the leftover string. Otherwise it is just m / 2.
Implementation details:
Run the stack deletion twice, once for (, ) and once for [, ]. Mark every unmatched position. Then scan only marked positions. Count openings and closings, and detect whether a marked closing immediately follows a marked opening. In a binary orientation string, having any opening before any later closing is equivalent to having some adjacent opening-closing transition, so this check is enough.
The complexity is per test case and memory. Over all tests, this is , which fits easily.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void setIO() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
}
int main() {
setIO();
int T;
cin >> T;
while (T--) {
int n;
string s;
cin >> n >> s;
vector<char> rem(n, 0);
auto mark_unmatched = [&](char open, char close) {
vector<int> st;
for (int i = 0; i < n; i++) {
if (s[i] == open) {
st.push_back(i);
} else if (s[i] == close) {
if (!st.empty() && s[st.back()] == open) {
st.pop_back();
} else {
st.push_back(i);
}
}
}
for (int idx : st) rem[idx] = 1;
};
mark_unmatched('(', ')');
mark_unmatched('[', ']');
int opens = 0, closes = 0;
bool prevOpen = false;
bool hasOpenClose = false;
for (int i = 0; i < n; i++) {
if (!rem[i]) continue;
bool isOpen = (s[i] == '(' || s[i] == '[');
if (isOpen) {
opens++;
prevOpen = true;
} else {
closes++;
if (prevOpen) hasOpenClose = true;
prevOpen = false;
}
}
int ans = (opens + closes) / 2;
if (opens % 2 == 1 && closes % 2 == 1 && !hasOpenClose) ans++;
cout << ans << '\n';
}
return 0;
}#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void setIO() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
}
int main() {
setIO();
int T;
cin >> T;
while (T--) {
int n;
string s;
cin >> n >> s;
vector<char> rem(n, 0);
auto mark_unmatched = [&](char open, char close) {
vector<int> st;
for (int i = 0; i < n; i++) {
if (s[i] == open) {
st.push_back(i);
} else if (s[i] == close) {
if (!st.empty() && s[st.back()] == open) {
st.pop_back();
} else {
st.push_back(i);
}
}
}
for (int idx : st) rem[idx] = 1;
};
mark_unmatched('(', ')');
mark_unmatched('[', ']');
int opens = 0, closes = 0;
bool prevOpen = false;
bool hasOpenClose = false;
for (int i = 0; i < n; i++) {
if (!rem[i]) continue;
bool isOpen = (s[i] == '(' || s[i] == '[');
if (isOpen) {
opens++;
prevOpen = true;
} else {
closes++;
if (prevOpen) hasOpenClose = true;
prevOpen = false;
}
}
int ans = (opens + closes) / 2;
if (opens % 2 == 1 && closes % 2 == 1 && !hasOpenClose) ans++;
cout << ans << '\n';
}
return 0;
}