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\) 都满足:它的字典序小于任意一个非空后缀。这个可以归纳说明。那么上面的正确性就是容易说明的了。

posted @ 2025-09-12 09:44  CJzdc  阅读(14)  评论(0)    收藏  举报