zoukankan      html  css  js  c++  java
  • ExKMP(Z Algorithm) 讲解

    问题引入

    众所周知,(mathrm{KMP}) 算法是最为经典的单模板字符串匹配问题的线性解法。那么 (mathrm{ExKMP}) 字面意义是 (mathrm{KMP}) 的扩展,那么它是解决什么问题呢?

    CaiOJ 1461 【EXKMP】最长共同前缀长度

    存在母串 (S) 和子串 (T) ,设 (|S| = n, |T| = m) ,求 (T)(S) 的每一个后缀的最长公共前缀 ((mathrm{LCP}))

    (extend) 数组, (extend[i]) 表示 (T)(S_{i sim n})(mathrm{LCP}) ,对于 (i in [1, n])(extend[i])

    (1 le m le n le 10^6)

    以下的字符串下标均从 (1) 开始标号。

    算法讲解

    本文参考了这位 大佬的讲解

    其实可以直接用 (SA / SAM) 解决,但是太大材小用了。。。(但似乎不太好做到 (O(n)) 有一种是做到 (O(n) - O(1) mathrm{RMQ}) )

    对于一般的 (mathrm{KMP}) 只需要求所有 (extend[i] = m) 的位置,那么 (mathrm{ExKMP}) 就是需要求出这个 (extend[i]) 数组。

    举个例子更好理解。

    (S = underline{aaaabaa}, T = underline{aaaaa})

    S: a a a a b a a
       | | | | X
    T: a a a a a
    

    我们知道 (extend[1] = 4) ,然后计算 (extend[2]) ,我们发现重新匹配是很浪费时间的。

    由于 (S_{1 sim 4} = T_{1sim 4}) ,那么 (S_{2 sim 4} = T_{2 sim 4})

    此时我们需要一个辅助的匹配数组 (next[i]) 表示 (T_{i sim m})(T)(mathrm{LCP})

    我们知道 (next[2] = 4) ,那么 (T_{2 sim 5} = T_{1 sim 4} Rightarrow T_{2 sim 4} = T_{1 sim 3})

    所以可以直接从 (T_4) 开始和 (S_5) 匹配,此时发现会失配,那么 (extend[2] = 3)

    这其实就是 (mathrm{ExKMP}) 的主要思想,下面简述其匹配的过程。

    匹配过程

    此处假设我们已经得到了 (next[i])

    当前我们从前往后依次递推 (extend[i]) ,假设当前递推完前 (k) 位,要求 (k + 1) 位。

    此时 (extend[1 sim k]) 已经算完,假设之前 (T) 能匹配 (S) 的后缀最远的位置为 (p = max_{i < k} (i + extend[i] - 1)) ,对应取到最大值的位置 (i)(pos)

    S: 1 ... pos ... k k+1 ... p ...
    

    那么根据 (extend) 数组定义有 (S_{pos sim p} = T_{1 sim p - pos + 1} Rightarrow S_{k + 1 sim p} = T_{k - pos + 2 sim p -pos + 1})

    (len = next[k - pos + 2]) ,分以下两种情况讨论。

    1. (k + len < p)

      S: 1 ... pos ... k k+1 ... k+len k+len+1 ... p ...
      			           |   |    |      X
      T:			       1  ...  len   len+1  ...
      

      此时我们发现 (S_{k + 1 sim k + len} = T_{1 sim len})

      由于 (next[k - pos + 2] = len) 所以 (T_{k + len + pos + 2} ot = T_{len + 1})

      又由于 (S_{k + len + 1} = T_{k + len - pos + 2}) 所以 (S_{k + len + 1} ot = T_{len + 1})

      这意味着 (extend[k + 1] = len)

    2. (k + len ge p)

      S: 1 ... pos ... k k+1 ...  p  p+1   ... ...
      			           |   |   |   ?
      T:		           1  ... ... p-k+2 ... len ...
      

      那么 (S_{p + 1}) 之后的串我们都从未尝试匹配过,不知道其信息,我们直接暴力向后依次匹配即可,直到失配停下来。

      如果 (extend[k + 1] + k > p) 要更新 (p)(pos)

    next 的求解

    前面我们假设已经求出 (next) ,但如何求呢?

    其实和 (mathrm{KMP}) 是很类似的,我们相当于 (T) 自己匹配自己每个后缀的答案,此处需要的 (next) 全都在前面会计算过。

    和前面匹配的过程是一模一样的。

    复杂度证明

    下面来分析一下算法的时间复杂度。

    1. 对于第一种情况,无需做任何匹配即可计算出 (extend[i])

    2. 对于第二种情况,都是从未被匹配的位置开始匹配,匹配过的位置不再匹配,也就是说对于母串的每一个位置,都只匹配了一次,所以算法总体时间复杂度是 (O(n)) 的。

    代码解决

    注意 (k + 1 = i) ,不要弄错下标了。

    #include <bits/stdc++.h>
    
    #define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i)
    #define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i)
    #define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i)
    #define Set(a, v) memset(a, v, sizeof(a))
    #define Cpy(a, b) memcpy(a, b, sizeof(a))
    #define debug(x) cout << #x << ": " << (x) << endl
    #define next Next
    
    using namespace std;
    
    template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
    template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }
    
    inline int read() {
        int x(0), sgn(1); char ch(getchar());
        for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
        for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
        return x * sgn;
    }
    
    void File() {
    #ifdef zjp_shadow
    	freopen ("1461.in", "r", stdin);
    	freopen ("1461.out", "w", stdout);
    #endif
    }
    
    const int N = 1e6 + 1e3;
    
    void Get_Next(char *S, int *next) {
        int lenS = strlen(S + 1), p = 1, pos;
        next[1] = lenS; // 对于 next[1] 要特殊考虑
        while (p + 1 <= lenS && S[p] == S[p + 1]) ++ p;
        next[pos = 2] = p - 1; // next[2] 是为了初始化
     
        For (i, 3, lenS) { // 注意此时 k + 1 = i
            int len = next[i - pos + 1];
            if (len + i < p + 1) next[i] = len; // 对应上面第一种情况
            else {
                int j = max(p - i + 1, 0); // 找到前面对于 子串 最靠后已经匹配的位置
                while (i + j <= lenS && S[j + 1] == S[i + j]) ++ j; // 第二种需要暴力匹配
                p = i + (next[pos = i] = j) - 1; // 记得更新 p, pos
            }
        }
    }
     
    void ExKMP(char *S, char *T, int *next, int *extend) {
        int lenS = strlen(S + 1), lenT = strlen(T + 1), p = 1, pos;
     
        while (p <= lenT && S[p] == T[p]) ++ p;
        p = extend[pos = 1] = p - 1; // 初始化 extend[1]
     
        For (i, 2, lenS) {
            int len = next[i - pos + 1];
            if (len + i < p + 1) extend[i] = len;
            else {
                int j = max(p - i + 1, 0);
                while (i + j <= lenS && j <= lenT && T[j + 1] == S[i + j]) ++ j;
                p = i + (extend[pos = i] = j) - 1;
            }
        } // 和上面基本一模一样啦
    }
    
    char S[N], T[N]; int next[N], extend[N];
    
    int main () {
    
    	File();
    
    	scanf ("%s", S + 1);
    	scanf ("%s", T + 1);
    
    	Get_Next(T, next);
    	ExKMP(S, T, next, extend);
    
    	For (i, 1, strlen(S + 1))
    		printf ("%d%c", extend[i], i == iend ? '
    ' : ' ');
    
        return 0;
    
    }
    

    一些例题

    UOJ #5. 【NOI2014】动物园

    题意

    给你一个字符串 (S) ,定义 (num) 数组 --- 对于字符串 (S) 的前 (i) 个字符构成的子串,既是它的后缀同时又是它的前缀,并且 该后缀与该前缀不重叠 ,将这种字符串的数量记作 (num[i])

    (prod_{i = 1}^{|S|} (num[i] + 1) pmod {10^{9}+7})

    题解

    如果会 (mathrm{ExKMP}) 就是裸题了。

    然后考虑对于每个 (S) 的后缀 (i) 会被算多少遍,其实就是对于以 ([i, min(2 imes (i - 1), i + next[i] - 1)]) 为结尾的所有前缀有贡献,那么直接差分即可。

    复杂度是 (O(sum |S|)) 的。

    代码

    前面的板子就不再放了。

    const int N = 1e6 + 1e3, Mod = 1e9 + 7;
    
    char str[N]; int num[N], next[N];
     
    int main () {
    
    	File();
    	
    	for (int cases = read(); cases; -- cases) {
    
    		scanf ("%s", str + 1); Set(num, 0);
    		Get_Next(str, next);
    
    		int n = strlen(str + 1);
    		For (i, 2, n)
    			if (next[i])
    				++ num[i], -- num[min(i * 2 - 1, i + next[i])];
    
    		int ans = 1;
    		For (i, 1, n)
    			ans = 1ll * ans * ((num[i] += num[i - 1]) + 1) % Mod;
    		printf ("%d
    ", ans);
    
    	}
    
        return 0;
    
    }
    

    CF1051E Vasya and Big Integers

    题意

    给你一个由数字构成的字符串 (a) ,问你有多少种划分方式,使得每段不含前导 (0) ,并且每段的数字大小在 ([l, r]) 之间。答案对于 (998244353) 取模。

    (1 le a le 10^{1000000}, 0 le l le r le 10^{1000000})

    题解

    考虑暴力 (dp) ,令 (dp_i) 为以 (i) 为一段结束的方案数。对于填表法是没有那么好转移的,(因为前导 (0) 的限制是挂在前面那个点上)我们考虑刷表法。

    那么转移为

    [dp_j = dp_j + dp_i~~{j~|~a_i ot = 0 & l le a_{i sim j} le r} ]

    我们发现 (dp_i) 能转移到的 (j) 一定是一段连续的区间。

    我们就需要快速得到这段区间,首先不难发现 (j) 对应的位数区间是可以很快确定的,就是 ([l + |L| - 1, i + |R| - 1])

    但是如果位数一样的话需要多花费 (O(n)) 的时间去逐位比较大小。

    有什么快速的方法吗?不难想到比较两个数字大小的时候是和字符串一样的,就是 (mathrm{LCP}) 的后面一位。

    那么我们用 (mathrm{ExKMP}) 快速预处理 (extend(mathrm{LCP})) 就可以了。

    代码

    const int N = 1e6 + 1e3, Mod = 998244353;
    
    inline void Add(int &a, int b) {
    	if ((a += b) >= Mod) a -= Mod;
    }
    
    char S[N], L[N], R[N];
    
    template<typename T>
    inline int dcmp(T lhs, T rhs) {
    	return (lhs > rhs) - (lhs < rhs);
    }
    
    inline int Cmp(int l, int r, char *cmp, int *Lcp, int len) {
    	if (r - l + 1 != len) return dcmp(r - l + 1, len);
    	return l + Lcp[l] > r ? 0 : dcmp(S[l + Lcp[l]], cmp[Lcp[l] + 1]);
    }
    
    int lenL, lenR, tmp[N], EL[N], ER[N];
    
    inline bool Check(int x, int y) {
    	return Cmp(x, y, L, EL, lenL) >= 0 && Cmp(x, y, R, ER, lenR) <= 0;
    }
    
    int tag[N], dp = 1;
    
    int main () {
    
    	File();
    
    	scanf ("%s", S + 1);
    	int n = strlen(S + 1);
    
    	scanf ("%s", L + 1); lenL = strlen(L + 1); Get_Next(L, tmp); ExKMP(S, L, tmp, EL);
    	scanf ("%s", R + 1); lenR = strlen(R + 1); Get_Next(R, tmp); ExKMP(S, R, tmp, ER);
    
    	tag[1] = Mod - 1;
    	For (i, 1, n) {
    		int l, r;
    		if (S[i] == '0') {
    			if (L[1] == '0') l = r = i;
    			else { Add(dp, tag[i]); continue; }
    		} else {
    			l = i + lenL - 1; if (!Check(i, l)) ++ l;
    			r = i + lenR - 1; if (!Check(i, r)) -- r;
    		}
    		if (l <= r) Add(tag[l], dp), Add(tag[r + 1], Mod - dp); 
    		Add(dp, tag[i]);
    	}
    
    	printf ("%d
    ", (dp + Mod) % Mod);
    
    	return 0;
    
    }
    
  • 相关阅读:
    安装lamp lnmp 一键安装包网址
    mysql float 这个大坑
    今天 运营同事发现的bug记录 上传商品时商品名称带双引号 导致输出页面时 双引号被转义
    excel 导出长数据 变成科学计数 解决办法
    mysql 基本知识 以及优化
    刷算法题记录
    windows 安装svn 要点(非安装步骤)
    《UCD火花集1-2》读后感
    我所经历的的一次问卷调查
    怎样进行批判性的思考
  • 原文地址:https://www.cnblogs.com/zjp-shadow/p/10139818.html
Copyright © 2011-2022 走看看