数据结构——字符串处理

未完成

KMP算法

KMP思想

1.如图所示,在主串s中搜素匹配模式串p的子串的位置时,主串的位置i与模式串的位置j并不匹配。
这里写图片描述
2.在如上图的不匹配的情况下,就需要将模式串p后移。传统的方法是将模式串p整体后移一个单位。从p的第一个字符开始重新进行比较。
3.上面方法是存在大量冗余的,而KMP算法,旨在利用前一次匹配的结果,在不匹配的情况下,可以直接向后移动若干位,而不是一位。这种思想可以等价于主串s的位置i在满足一定条件的情况下,应该与模式串的哪一位对齐,我们这里设置为第k位。如图

这里写图片描述

4.如图所示。如果主串中的位置i应该与模式串中的位置k对齐,那么需要满足的条件是:两个直线中间的区域的值是相等的。如图可以得到:
pjk+1pj1=p1pk1 其中: k<j

KMP思想:如果模式串的第j位与主串的第i位不匹配,且在模式串的前j位中,满足前缀与后缀相同的最大位数为k-1,那么应该继续让子串的第k位与主串的第i位比较。

易知:k为模式串的各位所共有的性质,不随主串的改变而改变。那么如何求得每一位对应的K,也就是KMP的核心next[j]?

5.如何求next[i];
归纳法证明:
如果 i=1 ,也就是第一个元素就没有匹配,所以应该直接后移,next[i]=0;(当next[i]=0时,直接后移)
如果 i>1 ,如图,假设next[i]=j,那么对于next[i+1],就有下面两种情况:
这里写图片描述

-若 pi=pj ,则next[i+1]=next[i]+1;
-若 pipj ,如图:
这里写图片描述

需要将下方的模式串后移,移动的距离如图所示,使得next[j]与i位置相对应。

注:代码的实现是是按照字符串第0个单元的数据有效来编辑的。

c++代码实现

void getNext(char pattern[],int* next){
    /*next[i]表示长度为i的字符串的next值,所以next的范围为next[0]~next[length-1]*/
    int i=0,j=-1;
    next[0]=-1;/*next[0]=-1*/

    while(i<strlen(pattern)){
        /*next[j]=-1说明第一个元素也没有匹配*/
        if(j==-1 || pattern[i]==pattern[j]){
            i++;
            j++;
            next[i]=j;
        }
        else j=next[j];
    }
}

KMP代码实现

//============================================================================
// Name        : KMP.cpp
// Author      : SELOUS
// Version     :
// Copyright   : Your copyright notice
// Description : KMP Algorithm;Handle String
//============================================================================

#include <iostream>
#include <cstring>
using namespace std;

void getNext(char pattern[],int* next){
    /*next[i]表示长度为i的字符串的next值,所以next的范围为next[1]~next[length]*/
    int i=0,j=-1;
    next[0]=-1;/*next[0]=-1*/

    while(i<strlen(pattern)){
        /*next[j]=-1说明第一个元素也没有匹配*/
        if(j==-1 || pattern[i]==pattern[j]){
            i++;
            j++;
            next[i]=j;
        }
        else j=next[j];
    }
}

int indexKMP(char *sstr,int slen,char *pattern,int plen,int pos,const int* next){

    int i = pos;//从pos位置开始匹配
    int j = 0;
    /**/
    while(i<slen && j<plen){

        if(j==-1||sstr[i] == pattern[j]){
            i++;
            j++;
        }else{
            j=next[j];
        }
    }

    if(j>=plen){
        return i-plen;
    }
    else return 0;
}

int main() {
    char sstr[80]="sdddafaffsabaabcacssss\0";
    char str[80]="sabaabcac\0";
    int next[80];
    getNext(str,next);
//  for(int i=1;i<=8;i++){
//      cout<<next[i]<<endl;
//}
        cout<<indexKMP(sstr,strlen(sstr),str,strlen(str),0,next)<<endl;
    return 0;
}

字典树

基本讲解

概念:Trie树,是哈希树的一个变种,将给定的字符串按照字典的形式储存,然后在其上进行相应的操作。例如:给定字符串ab,abd,bs,def,degf,建立的字典树如下图所示。

这里写图片描述

特点: 1.root节点为空节点,不储存相关信息。
2.明显的节省了空间,
3.每个节点回朔到根节点,为该节点对应的字符串。
4.每个节点的直接子节点的数据都不相同。

解决问题:(解决问题引自博客:http://blog.sina.com.cn/s/blog_6002b97001015vyi.html
(1) 字符串检索

事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。

举例:

@ 给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。

@ 给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。

(2)字符串最长公共前缀

Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。

举例:

@ 给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少?

解决方案:首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线(Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。

而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:

  1. 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;

  2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;

(关于并查集,Tarjan算法,RMQ问题,网上有很多资料。)

(3)排序

Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。

举例:

@ 给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。

(4) 作为其他数据结构和算法的辅助结构

如后缀树,AC自动机等

在使用字典树解决字符串问题的时候,因为处理的是大多是两类字符的问题,比如求A文章出现的单词中,以B为前缀的单词个数。就需要明确是以A还是以B为数据建立字典树。以B为数据建立字典树,前缀固定,文章可以动态改变,建树方便,查询麻烦。若以A为数据建立字典树,文章固定,可以求任意前缀出现的数目,建树麻烦,查询方便,所以需要根据题目不同的要求去权衡选择。

字典树是一个很简单的数据类型,不过能解决很多看起来很复杂的问题,而且时间复杂度很低,在实际应用中也很广泛。

代码实现

题目:http://acm.hdu.edu.cn/showproblem.php?pid=1251

1.树节点和初始化

typedef struct TrieNode{
    int v;//information upon trie node
    struct TrieNode* next[MAX];
    /*constructed function*/
    TrieNode(){
        //initial v to zero
        v=0;
        int i=0;
        /*assign next[i] to NULL*/
        for(;i<MAX;i++){
            next[i]=NULL;
        }
    };
}trieNode,*Trie;
//initial root of trie tree;
void init(Trie &T){
    T = new trieNode();
}

2.建树:

//insert a node into tree
void insert(Trie &T,char *str){
    trieNode *p = T;
    int i = 0;
    int len = strlen(str);
    int node;//记录数据对应节点的位置
    for(;i<len;i++){
         node = str[i]-'a';
        if(p->next[node]==NULL){
            p->next[node]= new TrieNode();
        }
        p=p->next[node];
        p->v++;
    }
}

3.查询:

int search(Trie &T,char *prefix){
    trieNode *p = T;
    int i=0;
    int len = strlen(prefix);
    for(;i<len;i++){
        int node = prefix[i]-'a';
        if(p->next[node]==NULL){
            return 0;
        }
        p=p->next[node];
    }
    return p->v;
}

AC自动机

基本概念讲解

AC自动机的基础是trie树。先通过模式串建立字典树,每个节点都维护一个fail指针,指向没有匹配之后跳转到的节点。fail指针需满足的条件为:s节点的fail指针指向的p节点需满足,字符串[root~p]与[root,s]字符串的后缀相同。

自动机建立过程举例:模式串为she,he,shr,her,say。
1.建立字典树,如图所示:

这里写图片描述

2.对于root节点的两个子节点s和h,也就是第一个节点,易知s和h的fail指针指向为root。

这里写图片描述

3.对于第二层的节点a,h,e,三个节点,我们考虑第二层的h节点。通过观察可知,[root,h1]的前缀h,与[root,h2]的后缀h是相同的,所以h2的fail节点是指向h1的。

理论证明:求节点n的fail指针?如果节点n的fail指针指向节点m,那么必定满足[root,n]的后缀,等于[root,m]。那么对于n的父节点n_parent来说,必定满足[root,n_parent]的后缀等于[root,n_parent_fail];因为字典树所固有的前缀相同的性质,所以n节点的fail指针必落在[root,n_parent_fail]这个树的分支上。

一维化表示:
这里写图片描述

如图,如果节点m和节点n的值相同,那么节点n的fail指针就指向节点m。即n->fail = n->parent->fail[ID[n]];如果两个节点的值不相等。如图:
这里写图片描述

就让图中的节点m1与其比较,m1的求法为:n->parent->fail->fail的子节点m1。

代码实现

注:ac自动机的代码和kmp代码一样,里面的逻辑很强,本人不是专门搞csp的,所以我只是搞懂了ac自动机的基本思想,关于代码实现也只是凭自己的理解实现的,不想测试了,所以代码可能有问题。如果你是专业的可以下面的代码就不读了。防止误人子弟。

1.根据模式串构建trie树

#define MAX_NODE_NUM 30
#define MAX_CHILD_NUM 26

typedef struct TNode{
    int count;//flag of whether the end of string
    struct TNode *next[MAX_CHILD_NUM];//child nodes
    struct TNode *fail;//fail point
    TNode(){
        count = 0;
        fail = NULL;
        memset(next,NULL,sizeof(next));
    }
}TNode,*Tree;

TNode node[MAX_NODE_NUM];

int nodeNum = 0;

void build(Tree& T){
    T = &node[nodeNum];
    nodeNum++;
}

void insert(Tree &T,string s){
    TNode *p=T;
    int index;
    int i = 0;
    for(;i < s.length();i++){
        index = s[i]-'a';
        if(p->next[index]==NULL){
            p->next[index]=&node[nodeNum];
            nodeNum++;
        }
        p=p->next[index];
    }
    p->count=1;
}

2.计算fail值

void build_ac(Tree &T){
    queue <Tree> q;
    TNode *p = T;
    p->fail=NULL;
    q.push(p);
    while(q.empty()){
        TNode *p = q.front();
        q.pop();
        int i;
        for(i=0;i<MAX_CHILD_NUM;i++){
            if(p->next[i]!=NULL){
                //temp为跳转位置
                TNode* temp =p->fail;
                //跳转位置为NULL说明没有匹配,即为根节点
                while(temp!=NULL){
                    /*如果n和m匹配*/
                    if(temp->next[i]!=NULL){
                        p->next[i]->fail=temp->next[i];
                        break;
                    }
                    temp=temp->fail;
                }
                if(temp==NULL) p->next[i]->fail=T;
                q.push(p->next[i]);//push into the queue
            }
        }
    }
}

AC自动机专题告一段落。之后如果遇到专门需要使用到ac自动机的时候在来重新深入了解吧。

——2017.2.28

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值