zoukankan      html  css  js  c++  java
  • SA & SAM

    后缀数组SA

    (sa[i])(rk[i])

    • (sa[i]) 表示排名为 (i) 的后缀是哪一个(在原串中开头位置)。
    • (rk[i])(或(rank[i]))表示开头位置是 (i) 的后缀的排名。

    两者是互相映射关系,即 (sa[rk[i]] = i)

    后缀排序(倍增)

    假设我们求出了只考虑长度为(w)的每一个后缀的前缀的 (sa)(rk),怎么求考虑长度为 (2w) 的每一个后缀的前缀的(sa)(rk) .

    对于两个后缀 (i)(j), 由于我们求出了在 (w) 下的 $sa $ 和 (rk),实际上可以通过比较两个二元组 ((rk_i,rk_{i+w}),(rk_j, rk_{j+w}))来确定大小关系,这里定义一个后缀(i)的两维度:第一维字符串([i...i+w-1]),第二维字符串([i+w,i+2w-1])

    为了方便实现以及减小常数,我们开一个辅助数组(tmp[i])表示上一轮排序(长度(w)),排名为 (i) 的后缀的长度为(w)的前缀对应的是现在的哪一个后缀的第二维,(tmp)可以由(w)下的(sa)求得。

    cnt = 0;
    for (int i = n - w + 1; i <= n; ++ i) tmp[++cnt] = i;
    for (int i = 1; i <= n; ++ i) if (sa[i] > w) tmp[++cnt] = sa[i] - w;
    

    那么基数排序时先把所有 (rk) 放进桶里,然后用(tmp)数组从大到小在它对应的后缀的排名的桶里给(sa)求出新排完序的位置(有点拗口,可能讲的也不是很清楚,看代码)。

    inline void Rsort() 
    {
        fill(buc, buc + 1 + M, 0);
        for (int i = 1; i <= n; ++ i) buc[rk[i]]++;
        for (int i = 1; i <= M; ++ i) buc[i] += buc[i - 1];
        for (int i = n; i >= 1; -- i) sa[buc[rk[tmp[i]]]--] = tmp[i];//好好体会下这句话(其实是我说不出来)
    }
    

    贴完整代码:(注意那个 (swap) 操作)

    M = 76;//字符集
    for (int i = 1; i <= n; ++ i)
     	rk[i] = str[i - 1] - '0', tmp[i] = i;
    Rsort();
    for (int w = 1, cnt = 0; cnt < n; w <<= 1, M = cnt) 
    {
         cnt = 0;
         for (int i = n - w + 1; i <= n; ++ i) tmp[++cnt] = i;
         for (int i = 1; i <= n; ++ i) if (sa[i] > w) tmp[++cnt] = sa[i] - w;
         Rsort(), swap(rk, tmp); rk[sa[1]] = cnt = 1;
         for (int i = 2; i <= n; ++ i)
            rk[sa[i]] = (tmp[sa[i]] == tmp[sa[i - 1]] && tmp[sa[i] + w] == tmp[sa[i - 1] + w]) ? cnt : ++cnt;//swap后tmp就是原来的rk数组
    }
    

    Height数组

    (height[i]) 表示 (lcp(sa[i], sa[i - 1])) ,即排名为 (i) 的后缀和排名 (i-1) 的后缀的最长公共前缀。

    (H[i]) 表示 (height[rk[i]]) ,即后缀(i)和排在它前面一位的最长公共前缀。

    性质:(H[i]geq H[i - 1] - 1)

    证明:设 (i-1) 号后缀和 (k) 号后缀在排序中是相邻的((rk[i - 1] = rk[k] + 1)),那么 (H[i - 1] = height[rk[i-1]]),那么 (i) 号后缀会和 (k-1) 号后缀有长度为 (H[i - 1] - 1) 的公共前缀,所以此时必然有 (H[i]geq H[i - 1] - 1)

    (Height) 数组:

    void getHt() 
    {
        int len = 0;
        For (i, 1, n) 
        {
            if (len) len --;
            int j = sa[rk[i] - 1];
            while (str[j + len - 1] = str[i + len - 1]) len ++;
            ht[rk[i]] = len;
        }
    }
    

    经典应用

    1. 求 (lcp(x, y))
    (lcp(x, y) = min(height[rk[x] + 1],cdots ,height[rk[y]]))默认 (x) 排名小于 (y) 排名,用 (rmq) 维护。
    2.本质不同的子串数量

    排名为 (i) 的后缀对答案的贡献为 (len(i) - height[i])

    后缀自动机(SAM)

    本文只是为了方便复习,而不适合用来学习SAM。

    学习的话推荐吴作同的课件及oi-wiki上的教程

    2019-09-07 10-54-29 的屏幕截图.png

    2019-09-07 11-01-44 的屏幕截图.png

    SAM中的每一个状态(点)对应的是一个endpos集合,且互不相同,否则两个状态可以合并

    后文可能会说到点的endpos,代表的意义就是这个点接受子串的endpos,之后不再区分。

    SAM中每一个状态接受的子串长度是连续的,比如:ba,bba,cbba,结合 endpos 的定义理解下

    maxlen 与 minlen

    一个状态的maxlen为它接受的最长的子串,如一个点接受的串为 ba, bba, cbba, 那么它的maxlen就是4。

    minlen同理。

    两个状态 (fa)(u) ,若有

    • (endpos(u)subseteq endpos(fa))

    • (endpos(fa)) 的大小是满足上面条件下最小的那一个

    那么 (u) 的后缀链接为 (fa)

    对于一个点显然只有一个 (link) ,所以这个结构是一颗以 (t_0) 为根的树,称它为 (parent) 树。

    (endpos)(link) 的定义,(minlen(u) = maxlen(link(u)) + 1)

    所以想象一下,每个状态接受的串末尾对齐是一个梯形的形状(他们的endpos是一样的),如

      bacas
     bbacas
    cbbacas
    

    而它把它和它所以祖先接受的串按从上到下的顺序拼接,会是一个完美的等腰直角三角形,如

    (link(u)) 是这样:

     cas
    acas
    

    (u) 是这样:

      bacas
     bbacas
    cbbacas
    

    把u和它所有祖先的串拼起来:

    	  s
    	 as
    	---分界
    	cas
       acas
      -----分界
      bacas
     bbacas
    cbbacas
    

    体会到一些东西了吧,parent树还有一个性质:它是 (s) 的反串的后缀树!

    一个点到根的parent树上的状态的转移边是有单调性的,即一个存在一个节点它所有祖先都有 (c) 这条边,而它下面的点都没有这条边,由于一个点的endpos集合是被其父亲包含的,所以不难证明这一点。

    构造 (&) 实现:

    看吴作同的课件,自己手动模拟下。

    变量声明:

    int Ncnt, last;//Ncnt 节点数(不包括根节点0),last 插入上一个字符接受所有后缀的节点。
    struct Status
    {
        int len, link;
        //link  后缀链接
        //len 该状态接受的最长串长度,即 maxlen,这里不记minlen是因为可以由link的maxlen推得。
        int ch[26];//这里默认小写字母,26条转移边
    } st[maxN + 2];
    

    算法流程:

    我们从前往后一个个加入字符:设当前加入SAM中的 (s)(len)

    新建节点 (cur), (st[cur].lenleftarrow st[last].len + 1),把最长的后缀接受过来。

    然后考虑在尾部加入字符 (c)可能 会影响哪些状态,那么就是 (endpos) 包含了最后一位的的状态,即last的所有祖先,由于到根路径上的点的转移边是有单调性的,所以只需要考虑最下面的一段。

    所以我们跳 (last) 的后缀链接,直到该点拥有 (c) 这条出边或者到根,否则向 (cur) 连一条 (c) 的转移边,每向 (cur) 连一条边,它接受的字符串的梯形就会变高(想象一下),(minlen) 就会更新为当前跳到的点的 (minlen+1)

    假设我们跳到 (p) ,跳上来的那个儿子是 (son) ,它通过 (c) 转移到 (q) ,有可能会出现这样的两种情况:

    • (maxlen(q) = minlen(son)+1) 那么把 (cur)(link) 指向 (q)(cur) 的所有祖先和它的串就正好形成了一个等腰三角形,注意到 (maxlen(q) = minlen(son)+1)(maxlen(q) = maxlen(p) + 1) 是等价的,因为 (maxlen(p) = minlen(cur) - 1=minlen(son) - 2)

    • (maxlen(q) ot= minlen(son)+1) 这时可以理解成它不能和 (cur) 接受的梯形完美拼合上,所以我们把它拆成两个节点,即 (q ightarrow q' & clone) ,令 (clone)(maxlen = maxlen(p) + 1)(cur)(q')(link) 指向 (clone) ,$ clone$ 的后缀链接依然是 (q) 的后缀链接 ,并把 (q) 的所以出边复制到 (clone) 上,再把 (p) 到根路径上出边 (c) 指向 (q) 的点指向 (clone)

    这样就处理完增加一个字符对SAM结构的影响。

    代码:

    namespace SAM
    {
    	int Ncnt, last, size[2 * maxN + 2];
    	struct Status
    	{
    		int link, len;
    		int ch[26];
    	} st[2 * maxN + 2];
    
    	void init() { last = 0, st[0].link = -1, st[0].len = 0; }//插入第一个字符前记得初始化
    
    	void insert(char ch)
    	{
    		int c = ch - 'a';
    		int cur = ++Ncnt;
    		int p = last;
    		st[cur].len = st[last].len + 1;
    		while (p != -1 and !st[p].ch[c])//把所有last不含c边的祖先向cur连边。
    		{
    			st[p].ch[c] = cur;
    			p = st[p].link;
    		}
    		if (p == -1)
    			st[cur].link = 0;
    		else 
    		{
    			int q = st[p].ch[c];
    			if (st[q].len == st[p].len + 1)
    				st[cur].link = q;
    			else 
    			{
    				int clone = ++Ncnt;
    				st[clone] = st[q];
    				st[clone].len = st[p].len + 1; //把 q 拆出来个 clone
    				while (p != -1 and st[p].ch[c] == q) //把所有p的祖先转移到q的c边连向clone
    				{
    					st[p].ch[c] = clone;
    					p = st[p].link;
    				}
    				st[cur].link = st[q].link = clone;//更新link
    			}
    		}
    		last = cur;
    	}
    }
    
  • 相关阅读:
    Linux计划任务Crontab实例详解教程
    配置Linux任务计划
    全局变量报错:UnboundLocalError: local variable 'l' referenced before assignment
    Multipath多路径冗余全解
    Ubuntu 查看文件以及磁盘空间大小管理
    转载
    grep的用法
    Python标准库06 子进程 (subprocess包)
    Python与shell的3种交互方式介绍
    python和shell变量互相传递的几种方法
  • 原文地址:https://www.cnblogs.com/cnyali-Tea/p/11481202.html
Copyright © 2011-2022 走看看