CF1610G AmShZ Wins a Bet
以下用 \(\texttt{LR}\) 代表 \(\texttt{()}\)。
因为我们想最小化字典序,所以每次我们删掉 \(\texttt{L}\) 时都会尽量靠右删,同理 \(\texttt{R}\) 会尽量靠左删。于是可以说明每次删除的 \(\texttt{LR}\) 其实一定相邻。
再做一些观察。如果存在一个未匹配的 \(\texttt{R}\),那么它左右实际上是独立的。因此我们可以把串划分为若干段,每段形如一个匹配的括号串,再加上一个 \(\texttt{R}\)。最后一段可能是不满足条件的,因为可能有未匹配的 \(\texttt{L}\)。但是这种情况下,我们肯定是只保留所有未匹配的 \(\texttt{L}\)。
那么我们现在就要对一个匹配的串求答案。考虑对匹配关系建树。对于一个点 \(p\),考虑如果我们求出了 \(p\) 的所有子节点的答案,该怎么推出 \(p\) 的答案 \(f_p\)。
这个其实是容易的。我们令 \(f_p\) 初值为 \(\texttt{R}\),然后倒序枚举所有子节点 \(to\),\(f_p\leftarrow \min(f_p,f_{to}+f_p)\)。最后再 \(f_p\leftarrow \texttt{L}+f_p\) 即可。这里的 \(+\) 都是拼接。
这些东西都可以用链表实现,暴力实现的复杂度是 \(O(n^2)\)。
一个 $O(n^2)$ 的实现
#include <bits/stdc++.h>
using namespace std;
template <typename T> void Chkmax(T &x, T y) { x = max(x, y); }
const int kN = 1e6 + 5;
int n;
list<char> res[kN];
vector<int> g[kN];
bool Cmp(list<char> &a, list<char> &b) { // ? a < (b + a)
auto it1 = a.begin(), it2 = b.begin();
for(int i = 0; i < a.size(); i++) {
if(*it1 != *it2) return *it1 < *it2;
it1++, it2++;
if(it2 == b.end()) it2 = a.begin();
}
return 1;
}
void DFS(int x) {
res[x] = list<char> {')'};
reverse(g[x].begin(), g[x].end());
for(int to : g[x]) {
DFS(to);
res[to].push_front('(');
if(!Cmp(res[x], res[to])) res[x].splice(res[x].begin(), res[to]);
}
}
string Solve(string s) {
n = s.size();
s = " " + s;
fill(g, g + n + 1, vector<int> ());
stack<int> stk;
stk.push(0);
for(int i = 1; i <= n; i++) {
if(s[i] == '(') stk.push(i);
else {
int p = stk.top();
stk.pop();
g[stk.top()].push_back(p);
}
}
DFS(0);
string ans;
for(char c : res[0]) ans += c;
ans.pop_back();
return ans;
}
int main() {
// freopen("1.in", "r", stdin);
// freopen("1.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
int n;
string s;
cin >> s;
n = s.size();
s = " " + s;
int p = 0;
string ans;
while(p < n) {
int tp = p, v = 0;
while(++p <= n) {
(s[p] == '(') ? v++ : v--;
if(v < 0) break;
}
string str = s.substr(tp + 1, p - tp - 1);
if(v >= 0) ans += string(v, '(');
else ans += Solve(str);
if(p <= n) ans += ')';
}
cout << ans << "\n";
return 0;
}
事实上,如果你交一发上面的代码,就可以发现它其实能过。。。不过这个是可以被 \(\texttt{LRLR...LRR}\) 卡掉的。
可以发现这个的瓶颈就在比较 \(f_p\) 和 \(f_{to}+f_p\) 上。直接暴力是 \(O(|f_p|)\) 的。考虑优化。
经过一些尝试可以发现,我们可以只比较前 \(\min(|f_p|,|f_{to}|)\) 位,然后如果 \(|f_p|\le |f_{to}|\) 就有 \(f_p<f_{to}+f_p\)。这样实现就是 \(O(n\log n)\),可以通过。
$O(n\log n)$ 的实现
#include <bits/stdc++.h>
using namespace std;
template <typename T> void Chkmax(T &x, T y) { x = max(x, y); }
const int kN = 1e6 + 5;
int n;
list<char> res[kN];
vector<int> g[kN];
bool Cmp(list<char> &a, list<char> &b) { // ? a < (b + a)
auto it1 = a.begin(), it2 = b.begin();
for(int i = 0; i < min(a.size(), b.size()); i++) {
if(*it1 != *it2) return *it1 < *it2;
it1++, it2++;
}
return a.size() <= b.size();
}
void DFS(int x) {
res[x] = list<char> {')'};
reverse(g[x].begin(), g[x].end());
for(int to : g[x]) {
DFS(to);
res[to].push_front('(');
if(!Cmp(res[x], res[to])) res[x].splice(res[x].begin(), res[to]);
}
}
string Solve(string s) {
n = s.size();
s = " " + s;
fill(g, g + n + 1, vector<int> ());
stack<int> stk;
stk.push(0);
for(int i = 1; i <= n; i++) {
if(s[i] == '(') stk.push(i);
else {
int p = stk.top();
stk.pop();
g[stk.top()].push_back(p);
}
}
DFS(0);
string ans;
for(char c : res[0]) ans += c;
ans.pop_back();
return ans;
}
int main() {
// freopen("1.in", "r", stdin);
// freopen("1.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
int n;
string s;
cin >> s;
n = s.size();
s = " " + s;
int p = 0;
string ans;
while(p < n) {
int tp = p, v = 0;
while(++p <= n) {
(s[p] == '(') ? v++ : v--;
if(v < 0) break;
}
string str = s.substr(tp + 1, p - tp - 1);
if(v >= 0) ans += string(v, '(');
else ans += Solve(str);
if(p <= n) ans += ')';
}
cout << ans << "\n";
return 0;
}
这个做法看起来其实有点没道理,考虑证明。观察我们算答案的过程,可以发现,对于每个 \(i\),实际上 \(f_i\) 都满足:它的字典序小于任意一个非空后缀。这个可以归纳说明。那么上面的正确性就是容易说明的了。
浙公网安备 33010602011771号