zoukankan      html  css  js  c++  java
  • Suffix Array/SA/后缀数组 学习笔记

    前置约定与概念

    1. 本文字符串下标均从 (1) 开始.
    2. 对于原串 (S) , 记 (n)串长, 记 (Suf(i)) 表示 (S[i - n]) 这一子串.
    3. (SA) : (SA[i]) 表示后缀排序后排名(i) 的后缀的原串编号.
    4. (Rk) : (Rk[i]) 表示 (Suf(i))排名.
    5. (LCP) : 最长公共前缀.
    6. (Height) : (Height[i]) 表示 (LCP (SA[i], SA[i - 1])) 的长度.
    7. "" 中的内容为字符或字符串, 小写字母为单个字符, 大写字母为字符串.

    几个性质与结论

    1. (SA[Rk[i]] = Rk[SA[i]] = i)

      • 证明: 根据定义不难得出.
    2. (LCP(Suf (i), Suf (j)) geq LCP(Suf (i), Suf (k)) (Rk[i] > Rk[j] > Rk[k]))

      • 证明: 把 (LCP) 们放到 (Trie) 树上的叶子结点, 字典序相近的点表现为 (Trie) 树上 (LCA)深度更深, 即 (LCP) 越长.
    3. (Height[Rk[i]] geq Height[Rk[i - 1]] - 1)

      • 证明:

        • (Height[Rk[i - 1]] leq 1) 时, 成立.

        • 考虑 (Height[Rk[i - 1]] > 1) 的情况.

          (j = SA[Rk[i - 1] - 1]).

          (Suf(i - 1)) 长这样: "aBC" (其中 B 长度为 (Height[Rk[i - 1]] - 1)), (Suf(j)) 长这样: "aBD".

          因为 (Rk[j] < Rk[i - 1]) , 所以 "C" 的字典序大于"D".

          又有: (Suf(i)) 长这样: "BC", (Suf(j + 1)) 长这样: "BD". 根据上一句话, 有(Rk[j + 1] < Rk[i]).

          而又 (LCP (Suf (j + 1), Suf (i)) = B), 即 (|LCP (Suf (SA[Rk[i] - 1]), Suf (i))| geq |B| (结论2)).

          上式等价于 (Height[Rk[i]] geq Height[Rk[i - 1]] - 1).

    后缀排序过程详解

    void SuffixSort() {
    	/*
    		SA[i]: 第一关键字排名第 i 的后缀的位置(编号).
    		Rk[i]: 第 i 位的后缀的第一关键字.
    		Rk1[i]: 第二关键字排名第 i 的后缀的第一关键字的位置(编号).
    	*/
    	m = 122;
        // m 为第一关键字值域, 初始为字符集大小 ( ASCII 码值域 )
        // 下面为(单关键字)基数排序
    	for (int i = 1; i <= n; ++i) ++Cnt[Rk[i] = S[i]];
       	// 这里 Cnt 数组存的是第一关键字值域, 相当于一个桶
    	for (int i = 2; i <= m; ++i) Cnt[i] += Cnt[i - 1];
        /*
        	做一遍前缀和. 考虑此时 Cnt[i] 的意义, 实际上就是第一关键字 i 的排名.
        	说人话, 第一关键字为 i 的后缀们的排名区间是 [Cnt[i] - Size(i) + 1, Cnt[i]]
        	其中 Size(i) 指第一关键字为 i 的后缀数量.
        */
    	for (int i = n; i >= 1; --i) SA[Cnt[Rk[i]]--] = i;
    	/*
    		枚举后缀编号.
    		从内层分析, Rk[i] 用来获取 i 的第一关键字.
            Cnt[Rk[i]] 用来定位第一关键字为 Rk[i] 的后缀的排名区间(右端点, 见上句代码的注释).
            SA[Cnt[Rk[i]]] 确定排名为 Cnt[Rk[i]] 的后缀编号(即i).
            Cnt[Rk[i]]-- 是缩小第一关键字为 i 的后缀们的排名区间, 防止同一位置有多个编号.
    	*/
    	for (int K = 1; K <= n; K <<= 1) { // 倍增
    		int Tot = 0; //只是一个单纯的计数器
    		for (int i = n - K + 1; i <= n; ++i) Rk1[++Tot] = i;
            // 第一关键字位置在 [n - K + 1, n] 的后缀没有第二关键字, 把他们的第二关键字排名放到最前面.
    		for (int i = 1; i <= n; ++i) if (SA[i] > K) Rk1[++Tot] = SA[i] - K;
    		// 从小到大枚举排名, SA[i] > K 即其第一关键字能够作为其他后缀的第二关键字, 把其第一关键字扔到 Rk1 里面.
            
            // 下面进行(双关键字)基数排序
    		for (int i = 1; i <= m; ++i) Cnt[i] = 0; // 清空 Cnt
    		for (int i = 1; i <= n; ++i) ++Cnt[Rk[i]];
    		for (int i = 2; i <= m; ++i) Cnt[i] += Cnt[i - 1];
    		/*
            	获取每一个第一关键字的排名区间: [Cnt[i] - Size(i) + 1, Cnt[i]]
            	其中 Size(i) 指第一关键字为 i 的后缀数量.
            	你可以这样去考虑: 因为第一关键字优先, 先确定第一关键字的绝对顺序, 再到每一个第一关键字相同的区间用第二关键字确定相对顺序.
            */
            for (int i = n; i >= 1; --i) SA[Cnt[Rk[Rk1[i]]]--] = Rk1[i];
    		/*
    			从大到小枚举第二关键字排名.
    			Rk1[i] 定位其第一关键字位置, Rk[Rk1[i]] 获取第 Rk1[i] 位的第一关键字.
                Cnt[Rk[Rk1[i]]] 获取 第一关键字为 Rk[Rk1[i]] 的排名区间.
                Cnt[Rk[Rk1[i]]]--表示 Rk1[i] 占有这个坑位.
                由于 Rk1[i] 在有着相同的第一关键字的后缀中第二关键字是最大的, 所以优先占有靠后的位置.
            */
    		std::swap (Rk, Rk1), Rk[SA[1]] = Tot = 1;
    		// 需要生成新的 Rk 数组, 但需要用到原来的, swap 一下
    		for (int i = 2; i <= n; ++i)
    			Rk[SA[i]] = (Rk1[SA[i]] == Rk1[SA[i - 1]] && Rk1[SA[i] + K] == Rk1[SA[i - 1] + K])? Tot : ++Tot; //去重的意思, 新的第一关键字相同当且仅当久的两个关键字都相同.
    		if (Tot == n) break; m = Tot;
            // 如果 n 个后缀互不相同, 说明已排好序, 直接退出. 
            // m = Tot 用来获取新的第一关键字值域.
    	}
    }
    
  • 相关阅读:
    Death Race
    中国队吃中药了?
    (zt)让我们相亲相爱一百年不动摇
    Subversion & Arfa Karim Randhawa
    要是有时间的话……
    (zt)山王
    (zt)东方红
    Oracle 备份 与 恢复 概述
    Solaris 网络 配置
    RAID 磁盘阵列 详解
  • 原文地址:https://www.cnblogs.com/Rothen/p/13879401.html
Copyright © 2011-2022 走看看