P3809 后缀排序

$$\texttt{SA / rank}$$

这里讲最简单也最慢的方法 (前提是以及把倍增排序看了一下,对各个数组的定义有了一定的了解),我们只求 \(rank\) 来推导 \(SA\)
首先理解 \(O(N^2 \log N)\)\(O(N^2)\) 的方法,显然的快排和基数排序。如果不懂基数排序可以暂时留下。
然后是倍增。

每一个位置的第一关键字为 \(rank_{i,1}\) ,第二关键字为 \(rank_{i+2^{k-1},1}\) 这是比较显然的。
然后我们每一次要进行基数排序,排 两个关键字。那么这样子的时间复杂度就是 \(O(N \log N)\)
我主要讲一下如何排序。

首先基数排序就是说,一个数组 \(pus_i\),它的 关键字 称为 \(key_i\) (\(key_i=pus_i \mod 10\)),它在 原数组中的位置 称为 \(place_i\)。那么我们 \(10\) 个栈 (\(q_{0\_9}\))。假如我们有一个数字 \(223\) , 那么它将会被丢到 \(q_2\) (丢到 \(q_{key_i}\)),然后把 \(place_i\)\(23\) (\(pus_i \div 10\)) 丢进去。持续这样子排序,就可以得到一个有序的 \(place_i\),然后按照原数组来确定顺序。

上面的东西没听懂没有关系,可以自己看博客。
假如你已经会了上述的一个关键字的基数排序,你如何在 \(O(N)\) 的时间内把两个关键字的数组排序好?

\[\texttt{桶+链表(链式前向星)+基数排序} \]

我们把 \(rank_{i,1}\) 丢到一个桶里,桶记录一下位置 (也就是 \(bucket_{rank_{i,1}}=i\))。如果有重复的 \(rank_{j,1}\) (\(rank_{i,1}=rank_{j,1}\)),那么就用链表连起来 (也就是 \(add(bucket_{rank_{j,1}},j)\))。这样子我们就排序好了第一关键字,时间复杂度 \(O(N)\)
然后我们排序第二关键字,用基数排序 (为什么不用桶排? 因为第一关键字以及把第二关键字分成了很多个集合,如果用桶时间复杂度就炸了)。时间复杂度 \(O(N)\),记得去重!!!! (这个可以用桶在 \(O(N)\) 的复杂度内实现)
最后? 求 \(SA\)!

// luogu-judger-enable-o2
Uses math;

Const total=1000200;

var
    sa,num,pus,copy,area,place,bucket,ranking:array[-1..total] of longint;
    reach,from,next,cnt:array[-1..total] of longint;
    queue:array[-1..10,-1..total,-1..2] of longint;
    rank:array[-1..total,1..2] of longint;
    log:array[-1..30] of longint;
    i,n,tot,tail,border:longint;
    s:ansistring;
    j:char;

procedure add(l,r:longint); // 链式前向星
begin inc(tot); from[tot]:=l; reach[tot]:=r; next[tot]:=cnt[l]; cnt[l]:=tot; end;

procedure Radix;
var
    i,j,l,k,rank,maxn:longint;
    s:string;
begin
    maxn:=-maxlongint; rank:=0; pus[0]:=-666;
    for i:=1 to tail do
    begin // copy 是辅助数组,ranking 是拿来去重的
        maxn:=max(pus[i],maxn); copy[i]:=pus[i]; place[i]:=i; ranking[copy[i]]:=maxlongint;
    end; str(maxn,s);
    for i:=1 to length(s) do // 基数排序如上述
    begin 
        for j:=1 to tail do
        begin
            k:=pus[j] mod 10; pus[j]:=pus[j] div 10; inc(queue[k,0,0]);
            queue[k,queue[k,0,0],1]:=pus[j];
            queue[k,queue[k,0,0],2]:=place[j];
        end; k:=0;
        for j:=0 to 9 do
            for l:=1 to queue[j,0,0] do
            begin
                inc(k); pus[k]:=queue[j,l,1]; place[k]:=queue[j,l,2];
                queue[j,l,1]:=0; queue[j,l,2]:=0; queue[j,0,0]:=0;
            end;
    end;
    for i:=1 to tail do pus[i]:=copy[place[i]]; // 得到排好序列数组,然后给予排名
    for i:=1 to tail do begin if pus[i]<>pus[i-1] then inc(rank); ranking[pus[i]]:=rank; end; // 给予排名 (等于去重)
    for i:=1 to tail do place[i]:=ranking[copy[i]]; // 给定第 i 个位置 (同一关键字的第 i 个第二关键字) 的排名
end;

procedure Sort;
var i,j,k,l,maxn,number:longint;
begin
    for i:=1 to n do inc(bucket[num[i]]); // 先把一开始的只有一个关键字的排一下序
    for i:=1 to border do inc(bucket[i],bucket[i-1]); // 求前缀和来记录排名
    for i:=1 to n do rank[i,1]:=bucket[num[i]]; // 作为第一关键字
    i:=0;
    repeat
        inc(i); tot:=0; number:=0;
        for k:=1 to border do begin cnt[k]:=-1; bucket[k]:=0; end; // 清空链式前向星和桶 (其余的能不清空就不清空)
        for k:=1 to n do begin rank[k,2]:=0; if k+log[i]<=n then rank[k,2]:=rank[k+log[i],1]; end; // 造第二关键字,注意超界的都为 0
        for k:=1 to border do if bucket[rank[k,1]]>0 then add(bucket[rank[k,1]],k) else bucket[rank[k,1]]:=k; // 把第一关键字相同的用链表连起来
        for k:=1 to border do
        begin
            if bucket[k]=0 then continue; 
            tail:=1; j:=cnt[bucket[k]]; pus[1]:=rank[bucket[k],2]; area[1]:=bucket[k]; // area 为丢去基数排序的数所在的位置
            while j<>-1 do begin inc(tail); area[tail]:=reach[j]; pus[tail]:=rank[reach[j],2]; j:=next[j]; end; // 把所有第二关键字取出来为 pus
            if tail>1 then Radix else place[1]:=1; maxn:=0; // 那个 if 很重要,它试我的 LOJ AC率掉了一半
            for j:=1 to tail do begin rank[area[j],1]:=place[j]+number; maxn:=max(maxn,rank[area[j],1]); end; // 把第二关键字给予的排名赋给第一关键字
            number:=maxn; // 本次最高排名,给下一次排序用
        end;
    until log[i]>n;
end;

begin
    filldword(rank,sizeof(rank) div 4,0);
    for i:=1 to 25 do log[i]:=1 << (i-1); // 预处理 2^N
    readln(s); n:=length(s); border:=-maxlongint div 843; // border 为最大的排名,虽然到了后面 border=n,但是一开始 border=n+ord('z')
    for i:=1 to n do begin num[i]:=ord(s[i]); border:=max(border,num[i]); end;
    inc(border,n); Sort; // 开始排序
    for i:=1 to n do sa[rank[i,1]]:=i; // 求 SA
    for i:=1 to n do write(sa[i],' ');
end.

排序常数 \(\times 4\),基数排序常数 \(\times 5\),导致我的运算次数很大。

$$\texttt{Prefix}$$

由于 \(Height\) 太难听了就换了个名字。
\(Prefix\) 定义为排名后第 \(i\) 个串与第 \(i-1\) 个串的最长公共前缀 (\(\texttt{LCP}\))。
求这个东西有什么用?

而两个排名不相邻的最长公共前缀定义为排名在它们之间的 \(Prefix\) 的最小值。
怎么求? \(O(N^2)\) 暴力?
要知道一个定理 :

\[Prefix_{rank_{i,1}} \ge Prefix_{rank_{i-1,1}}-1 \]

然后就可以递增求出了。

procedure Height;
var i,h,k:longint;
begin h:=0;
    for i:=1 to n do
    begin
        if rank[i,1]=1 then h:=0 else
        begin
            h:=max(h-1,0);
            while (num[i+h]=num[sa[rank[i,1]-1]+h]) do inc(h);
        end; prefix[rank[i,1]]:=h;
    end;
end;
posted @ 2019-01-25 21:42  Rattry  阅读(202)  评论(0)    收藏  举报