qoj18234 Compare Suffixes

有一个未知的字符串,你每次可以给出两个下标 \(x,y\) 并得知以 \(x,y\) 开头的后缀的大小关系。

试在 \(n\log\sigma+\sigma\log n\) 次询问内求出该字符串的后缀数组。

其中 \(\sigma=26\) 是字符集大小。


定义 \(s(i)\) 表示第 \(i\) 位的字符,\(S(i)\) 表示以 \(i\) 开头的后缀。

一个暴力的做法是依次插入每一个后缀,每次在当前的 SA 基础上二分得到插入的位置,操作次数 \(n\log n\)

为了优化它,需要利用 \(\sigma\le26\) 的性质。

我们从平凡的结论开始:

Observation 1:对字符串的每个后缀 \(S(i)\) 排序等价于对二元组 \((s(i),S(i+1))\) 排序。

由这一点出发我们可以进一步得到以下两个结论。

Observation 2SA 中以每个字符 \(c_i\) 开头的后缀构成连续段。连续段最多有 \(\sigma\) 个。

Observation 3:每个连续段中 \(\text{rk}(\text{sa}(i)+1)\) 递增。

换句话说,如果我们考察 \(f(i)=\text{rk}(\text{sa}(i)+1)\) 的取值,其被至多划分为 \(\sigma\) 个递增的段。且在每个段的结束位置 \(p\)\(s(\text{sa}(p))<s(\text{sa}(p+1))\)\(f(p)>f(p+1)\)

因此,由于段内 \(f\) 递增,且当我们插入后缀 \(p\) 时,\(\text{rk}(p+1)\) 已知。那么在每个段内 \(p\) 只有一个可能的插入位置。

\(p\) 可以被合法地插入某个段,那么可以在段上二分出第一个满足条件的段。这一步至多询问 \(n\log\sigma\) 次。

否则,这意味着 \(p\) 将会创建一个新段,我们直接暴力在所有后缀上二分,由于这种情况最多只会发生 \(\sigma\) 次。这一步总共至多询问 \(\sigma\log n\) 次。


有一个小细节是,开头相同的后缀必在同一段内,但同一段内的后缀开头不一定相同,只要满足 \(f\) 单增即可被合并为一段。具体实现的时候要注意这一点。


#include <algorithm>
#include <iostream>
#include <vector>

const int N = 1007;
int sa[N], rk[N], ext[N];

int main() {
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int n, m = 1, t = 2; std::cin >> n;
  auto compare = [&](int x, int y) -> bool {
    std::cout << "query " << x << ' ' << y << "\n" << std::flush;
    std::string res; std::cin >> res;
    return res == "first";
  };
  int cmp = compare(n-1, n);
  if(cmp) {
    sa[1] = n - 1, sa[2] = n;
    rk[n - 1] = 1, rk[n] = 2;
  } else {
    sa[2] = n - 1, sa[1] = n;
    rk[n - 1] = 2, rk[n] = 1;
  }
  auto insert = [&](int p, int q) {
    // insert q at pos p
    for(int i = t+1; i >= p+1; --i) 
      if(sa[i-1]) sa[i] = sa[i-1], rk[sa[i]] = i;
    sa[p] = q, rk[q] = p;
  };
  auto locate = [](int id, int p) {
    int y = ext[id]-1;
    for(int i = ext[id]; i < ext[id+1]; ++i) {
      if(rk[p] > rk[sa[i]+1] && i > y)
        y = i;
    }
    return y;
  };
  auto build = [&]() {
    ext[m=1] = 1;
    for(int i = 1; i <= t-1; ++i) {
      if(rk[sa[i]+1] > rk[sa[i+1]+1]) {
        ext[++m] = i+1;
      }
    }
    ext[m+1] = t+1;
  };
  for(int p = n-2; p >= 1; --p) {
    // assume it doesnt craete a new phaseA
    build();
    int l = 0, r = m+1;
    while(r - l > 1) {
      int mid = (l + r) >> 1;
      int q = locate(mid, p+1);
      if(!q || compare(sa[q], p))
        l = mid;
      else r = mid;
    }
    int q = locate(l, p+1);
    if(q == t || compare(p, sa[q+1])) {
      // success
      insert(q+1, p);
    } else {
      // new, just search
      int l = 0, r = t+1;
      while(r - l > 1) {
        int mid = (l + r) >> 1;
        if(compare(sa[mid], p))
          l = mid;
        else r = mid;
      }
      insert(r, p);
    }
    ++t;
  }
  std::cout << "answer ";
  for(int i = 1; i <= n; ++i) 
    std::cout << sa[i] << " ";
}
posted @ 2026-08-17 01:23  CuteNess  阅读(8)  评论(0)    收藏  举报