UOJ Logo return20071007的博客

博客

一种野蛮处理静态序列多次在线区间查询的数据结构

2021-01-18 16:50:43 By return20071007

$O(n\log^*n)$(<-记 $\log^*n$ 表示 $n$ 最多能连续取几次 $\log$) 预处理然后 $O(1)$ 查询,但在 OI 里没啥意义,本文不作讨论。

空间复杂度同预处理复杂度。

不知道现在 OI 界是否有同样的算法,如果没有的话,可以称之为三叶虫树(UTT),有的话麻烦评论区告知,会删掉这个命名。

UPD:由于评论区已被神仙告知有,命名删除。不过博主还是不知道其结构是否相同(感觉不太是?),所以还是挂在这里吧。求轻喷。

接着是算法。

观察 zkw 线段树区间查询的查询结构,以 RMQ 为例:

  for (l += M - 1, r += M + 1; l ^ r ^ 1; l >>= 1, r >>= 1) {
    if (~l & 1) ret = max(ret, zkw[l ^ 1]);
    if (r & 1) ret = max(ret, zkw[r ^ 1]);
  }

发现就是两边链查询,查询的信息类似于一条链上所有左/右儿子的兄弟们的最大值。

然后我们就只需要解决从叶子开始的链查询问题了,用一个 pair 维护这两个信息,然后甚至连原线段树都能省了。

我们观察到,如果预处理出每一种这样的链的答案,那查询就能做常数时间,但是预处理复杂度是 $O(n\log n)$ 的。

之所以造成这样的复杂度是因为出现了一堆所谓“冗余”信息,因为很容易发现不同节点的前缀和路径中会出现巨量重叠.

那我们如果把重叠的部分选择性记录下来不就可以省时间了?

于是我们给线段树分层以记录重叠信息。

然后,观察三叶虫的结构(?):

很容易发现无论是横向还是纵向都被分成了三部分,于是我们采用仿生技术(?),把线段树也分成三部分。

画个图:

在每一部分内做从叶子开始的“前缀和”(<-其实是前缀操作,这里就以和为例,姑且称作前缀和了)。

比如说这个线段树的这个分法,所有会被计算的前缀和(就以和为例到底了吧):

最底下一层:

16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31

中间那层:

8,9,10,11,12,13,14,15,8+4,9+4,10+5,11+5,12+6,13+6.14+7,15+7

最顶上一层:

2,3,2+1,3+1

对就是这样。

然后查询显然还是常数时间。

至于怎么分这棵树,有两种处理方法:直接数学推决策,或者干脆先花 $O(\log^2 n)$ 的时间枚举两条线然后看何时最优。

然后 OI 用的场景,数据范围绝大部分情况下不会超过 $2^{28}\approx2.68\times10^8$ 对吧,这个场景下预处理复杂度可以视作线性(枚举一下就会发现常数差不多 $3$ 左右)。空间复杂度同预处理复杂度。

如果真的有什么毒瘤到数据范围出得比这还大的出题人,那也没事反正常数还是很小(误),我们就要升级一下我们的三叶虫树了,把它变成四叶虫树(真)

真就把分三层改成分四层就好了。这玩意的查询显然还是常数时间,预处理的常数当序列长度不到 $2^{2059}$ 时常数严控于 $3$。空间复杂度依然同预处理复杂度。

不可能真的有出题人出再大的数据范围了,不可能了。

真的要再大的话,复杂度开头给了。此时无非就是多分几层,然后再加一个层间前缀和(不用管这是什么)把查询稳在常数时间。空间复杂度还是同预处理复杂度。

例题:第一行输入序列长度和查询数,接着一行输入序列,接着若干行每行一个查询区间,所有输入为 $10^5$ 以内的正整数。

代码(分三层+枚举决策版):

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
const int N = 1e5 + 9, Mx = 1 << 17;
int n, m, M = 1, h, lg[Mx << 1], x, y;
pii val[Mx << 1];
pii* suf[Mx << 1];
pii pmax(const pii& a, const pii& b) {
  return make_pair(max(a.first, b.first), max(a.second, b.second));
}
pii qlink(int pos, int len) {
  if (h - y > len) return suf[pos][len];
  pii ret = suf[pos][h - y - 1];
  len -= h - y, pos >>= h - y;
  if (y - x > len) return pmax(ret, suf[pos][len]);
  ret = pmax(ret, suf[pos][y - x - 1]);
  len -= y - x, pos >>= y - x;
  return pmax(ret, suf[pos][len]);
}
int main() {
  scanf("%d%d", &n, &m);
  while (M <= n || h <= 1) M <<= 1, ++h;
  for (int i = M + 1, a; i <= M + n; ++i) {
    scanf("%d", &a);
    ((i & 1) ? val[i ^ 1].first : val[i ^ 1].second) = a;
  }
  for (int i = M - 1; i > 1; --i)
    ((i & 1) ? val[i ^ 1].first : val[i ^ 1].second) =
        max(val[i << 1].first, val[i << 1 | 1].second);
  for (int x = 0, mx = INT_MAX; x < h; ++x)
    for (int y = x + 1; y < h; ++y) {
      int tmp = (x << x) + ((y - x) << y) + ((h - y) << h);
      if (tmp < mx) mx = tmp, ::x = x, ::y = y;
    }
  for (int i = (1 << x); i < (1 << (x + 1)); ++i) {
    suf[i] = new pii[x];
    suf[i][0] = val[i];
    for (int j = 1; j < x; ++j) suf[i][j] = pmax(suf[i][j - 1], val[i >> j]);
  }
  for (int i = (1 << y); i < (1 << (y + 1)); ++i) {
    suf[i] = new pii[y - x];
    suf[i][0] = val[i];
    for (int j = 1; j < y - x; ++j)
      suf[i][j] = pmax(suf[i][j - 1], val[i >> j]);
  }
  for (int i = M; i < M + M; ++i) {
    suf[i] = new pii[h - y];
    suf[i][0] = val[i];
    for (int j = 1; j < h - y; ++j)
      suf[i][j] = pmax(suf[i][j - 1], val[i >> j]);
  }
  for (int i = 2; i < M + M; ++i) lg[i] = lg[i >> 1] + 1;
  for (int l, r; m; --m) {
    scanf("%d%d", &l, &r), l += M - 1, r += M + 1;
    int L = qlink(l, lg[l ^ r] - 1).first;
    int R = qlink(r, lg[l ^ r] - 1).second;
    printf("%d\n", max(L, R));
  }
  //接下来应该要有一些手动释放内存的部分(可以 vector 实现然后省掉)。
  //但是测试 OJ 上不允许手动释放,并且程序终止时本身就会自动释放,所以没了。
  return 0;
}

缺点是常数略大,优点是复杂度漂亮。

是博主(至少自认为)原创的,鉴于博主孤陋寡闻,如果已有同样数据结构还请不吝告知。

以上。

评论

happydef
我当场直接膜拜
Trotyl
orzorzorz
142857cs
@return20071007 你这个其实已经是一个和线段树完全不同的结构了 "如果预处理出每一种这样的链的答案"其实就是前缀和,和链没什么关系 预处理O(nlogn)的做法有时被叫做猫树,不过其实叫二区间合并就行了吧 至于你这个给线段树分层。。。其实给序列分块就行了,预处理块内前缀和,块间用二区间合并,然后小块内部的查询继续分块 就叫四区间合并吧,不要取什么奇怪的名字
George1123
哈哈哈哈哈哈 LOL
George1123
笑煞我也
mcyl
感觉有点复杂啊,直接四毛子然后上猫树维护,块内的继续递归下去做到 O(n log*n)-O(log*n) 好像也行 另外据说有对于满足结合律的信息的 O(nα(n)) 做法
yjsnpi
三叶虫树xs
alpha1022
lol
tommy0103
lol

发表评论

可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。