zoukankan      html  css  js  c++  java
  • [小专题]另一种字符串匹配的思路——Shift-And算法

    • 吐槽:前两天打组队赛遇到一个字符串的题考了这个(见:http://acm.hdu.edu.cn/showproblem.php?pid=5972
      当时写了个KMP瞎搞然后TLE了(害),赛后去查了许多资料似乎就看见一个题考了这么个鬼东西…

    问题给出

    • 给一个主串(S=s_1s_2...s_n)和一个模式串(T=(t_{11}|t_{12}|...|t_{1k_1})(t_{21}|t_{22}|...t_{2k_2})...(t_{m1}|t_{m2}|...|t_{mk_m})),对于(S)的一个子串(S'[1,m]=S[i,i+m-1]),只要第(j)个位置满足(S[j]=t_{j1},t_{j2},..t_{jk_j})中的其中一个,就算匹配成功。找到(S)中所有能和(T)匹配的子串。
    • (|S|,|T|)的范围依然很大,(k_i)和字符集比较小

    问题分析

    • 首先想到的是去修改一下KMP里匹配成功的条件…如果在一个位置发现了失配,即(s[i] otsubset t[j+1]),那样我们希望仍然有一个合适的失配数组(nxt[])让我们跳回合适的位置,即找到一个(T[1,j])的最长公共前后缀跳回去,但是这里就出现了问题,如果要保证(S)匹配到的后缀要和新的(T)的前缀匹配,那就得保证我们找的(T)的这个前缀包含了(T)的后缀,可是这样就也要改掉(nxt[])数组的求解。
    • 好那就改吧,构造失配数组的时候把匹配条件t[i]==t[j+1] 改成包含关系,即我们要让(T[1,j+1])这个前缀包含(T[i-j,i])这个后缀如果(k_i)小的话这一步仍然可以认为是(O(1))的时间,整个处理过程仍然是(O(|T|))的。嗯到这里感觉都不错,接下来进行两个字符串的匹配了,(S)(T)的比较也改成比较是否包含。如果失配的话同样是(j=nxt[j])地往回跳,一直到(j==|T|)匹配成功…到这里似乎都没什么问题
    • 但是但是…一旦匹配成功输出结果,(j)这个指针该怎么跳?(j=nxt[j])?但是很快就会发现这里这样做会漏掉一些情况…(因为如果要求【跳到的前缀包含了当前的后缀】这样一个苛刻的条件,那样可能会出现(S)串马上又可以匹配,但是我们条件太苛刻跳过了的情况)
    • 想来想去没法解决这个问题…
    • 好吧既然KMP没法解决…不如我们换个算法(逃

    另一种字符串匹配方法

    和其他算法问题一样,我们可以考虑换一个维护的对象。下标,字符集…

    比如这里,我们考虑从另一个角度切入字符串匹配的问题:对于字符集比较小的匹配,对模式串(T)里每个字符出现的位置进行记录:即用一个数组(B[i][j])表示字符(i)在第(j)个位置是否出现。这样记录能够处理这题里令我们头疼的问题:模式串的一个位置允许多种取值

    朴素暴力

    好了现在有了这么一个想法,先试试看最暴力地要怎么做这个问题

    (约定|S|=m,|T|=n)

    (O(n))地求出(B[][])数组

    for(int i=1;i<=n;i++)
    	int k;scanf("%d",&k);
        for(int j=1;j<=k;j++)
    		int t;scanf("%d",&t);
    		B[t][i]=1
    

    每次暴力匹配(S)(T),时间复杂度还是(O(nm))

    for(int i=1;i<=m;i++)
        int j=1;
    	for(;j<=n&&B[s[i+j-1]][j];j++);
        if(j==n+1)
        	match!
    

    优化算法

    和其他字符串算法的思路一样,我们尝试能不能通过维护一些前后缀的信息来减少信息的冗余:比如这里我们发现,上面的算法每次都在暴力(O(n))地比较(S[i,i+n-1])(T[1,n]),我们可以把这个过程看成(S[1,i+n-1])的后缀和(T[1,n])的前缀进行比较,于是类似KMP的思路,也许我们可以去维护(S)的后缀和(T)的前缀相关的信息!(这就是Shift-And算法的思路!)

    我们考虑再用一个数组(D[])来维护这样一个信息:(D[j]=1)当且仅当(S[i-j+1,i])(T[1,j])匹配,即(S)的一个后缀是(T)的前缀。否则(D[j]=0)。马上我们将会发现用Bool类型储存这样一个信息的优越性。

    如果我们让(i,j)两个指针一起跑(如图),能写出递推式:(D[j+1]=(D[j])&(S[i+1]==T[j+1]))。进一步我们利用前面做好的数组(B[][]),可以把相等的判定修改一下,变成:(D[j+1]=D[j]&B[S[i+1]][j+1])

    到这里都还只是逐位地进行位运算的比较,但是我们注意到这个(D[])似乎可以做成一个(bitset),把它看成是一个长度为(|T|)的二进制数的话,尝试直接用一个(D)表示这个数组,用位运算来实现这个递推。

    考虑上面的过程,从(D[j])(D[j+1])需要先把上一位(D[j])的信息复制过来,再对(j+1)位进行一个取(&)的操作,考虑从(i=1,j=1)往上递推的整个过程…对于每个(i),每次遍历(1,2,3,...,j...,|T|),复制信息…对应位置取(&),这个复制信息的过程不就相当于把一个二进制数全部左移一位么?每次取(&)也很麻烦,我们把(B[i][j])的第二个维度也压掉,直接对两个二进制数按位(&),同时为了保证(&)正确性,每次左移完了之后把最低位赋为1。

    另外,对于超过(|T|)(j)的信息我们可以直接丢掉,所以也不用担心丢失什么信息。

    至此,我们已经可以抛去(j)这个指针,得到从(i)(i+1)递推式:

    (D=(D<<1|1)&B[S[i+1]])实现

    核心代码

    const int N=5000005;
    const int M=1005;
    
    char s[N];
    char t[M];
    bitset<M>B[10],D;
    int n,len;
    
    int main(){
    	scanf("%d",&n);
    	rep(i,1,n){
    		int k;scanf("%d",&k);
    		rep(j,1,k){
    			int t;scanf("%d",&t);
    			B[t].set(i,1);
    		}
    	}
    	
    	scanf("%s",s+1);
    	len=strlen(s+1);
    	rep(i,1,len){
    		D=(D<<1).set(1)&B[s[i]-'0'];
    		if(D[n]){
    			char ch=s[i+1];
    			s[i+1]=0;
    			puts(s+i-n+1);
    			s[i+1]=ch;
    		}
    	}
        return 0;
    }
    

    参考资料

  • 相关阅读:
    有点忙啊
    什么是协程
    HDU 1110 Equipment Box (判断一个大矩形里面能不能放小矩形)
    HDU 1155 Bungee Jumping(物理题,动能公式,弹性势能公式,重力势能公式)
    HDU 1210 Eddy's 洗牌问题(找规律,数学)
    HDU1214 圆桌会议(找规律,数学)
    HDU1215 七夕节(模拟 数学)
    HDU 1216 Assistance Required(暴力打表)
    HDU 1220 Cube(数学,找规律)
    HDU 1221 Rectangle and Circle(判断圆和矩形是不是相交)
  • 原文地址:https://www.cnblogs.com/yoshinow2001/p/13846058.html
Copyright © 2011-2022 走看看