字符串——多模匹配

Rabin-Karp

基本思想

两种常见的字符串哈希:
在这里插入图片描述

Rabin_Karp是基于哈希的字符串匹配算法,主要基于上面的第二种。
在这里插入图片描述
如何在已知hash1的基础上表示出hash2?
在这里插入图片描述
如果两个字符串具有相同的哈希值要怎么办?
一旦匹配到和模式串具有相同哈希值的字符串,需要单独进行一次逐个位置的匹配。

代码

单模匹配

#include <iostream>
using namespace std;

#define TEST(func, s, t) { \
    printf("%s(%s, %s) = %d\n", #func, s, t, func(s, t)); \
}

#define MOD 9973
#define BASE 131

int hash_func(const char *s) {//求取当前模式串的哈希值 
    int h = 0, slen = strlen(s);
    for (int i = slen - 1, kbase = 1; i >= 0; i--) {//从后往前,越往前权重越大 
        h = (h + s[i] * kbase) % MOD;//新找到了一位,哈希值更新 
        kbase = kbase * BASE % MOD;//更新权重 
    }
    return h;
}

int RabinKarp(const char *s, const char *t) {
    int thash = hash_func(t);//模式串的哈希值 
    int nbase = 1, tlen;
    for (tlen = 0; t[tlen]; tlen++) nbase = nbase * BASE % MOD;//为了方便由hashi求出hashj过程中减去开头项,引入值nbase 
    for (int i = 0, h = 0; s[i]; i++) {
        h = h * BASE + s[i];//更新base 
        if (i >= tlen) h = (h - s[i - tlen] * nbase % MOD + MOD) % MOD;//位数足够之后才减去前面的 
        if (i + 1 < tlen) continue;//位数不够,不执行下面的比较 
        if (h != thash) continue; //哈希值不匹配,继续下一次 
        if (strncmp(s + i - tlen + 1, t, tlen) == 0) {//哈希值匹配,进行一次逐个位置的检查 
            return i - tlen + 1;//返回当前匹配串的开始位置 
        }
    }
    return -1;
}

int main() {
    char s[100], t[100];
    while (~scanf("%s%s", s, t)) { 
        TEST(RabinKarp, s, t);
    }
    return 0;
}

多模匹配

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

#define MOD 9973
#define BASE 131

int hash_func(string &s) {//求模式串的哈希值 
    int h = 0;
    for (int i = s.size() - 1, kbase = 1; i >= 0; i--) {
        h = (h + s[i] * kbase) % MOD;
        kbase = kbase * BASE % MOD;
    }
    return h;
}

void RabinKarp(string &s, vector<string> &t) {
    unordered_map<int, vector<int>> thash;//存放着哈希值与模式串下标的对应关系 
    for (int i = 0; i < t.size(); i++) thash[hash_func(t[i])].push_back(i);//遍历每一个模式串,求出对应哈希值,并存储对应关系 
    int nbase = 1, tlen;
    for (tlen = 0; t[0][tlen]; tlen++) nbase = nbase * BASE % MOD;
    for (int i = 0, h = 0; s[i]; i++) {
        h = h * BASE + s[i];
        if (i >= tlen) h = (h - s[i - tlen] * nbase % MOD + MOD) % MOD;
        if (i + 1 < tlen) continue;
        if (thash.find(h) == thash.end()) continue;//如果当前匹配到串的哈希值没有记录,继续向后查找 
        for (int j = 0; j < thash[h].size(); j++) {//如果查找到了,那么就遍历具有当前哈希值的每一个模式串 
            if (strncmp(s.c_str() + i - tlen + 1, t[thash[h][j]].c_str(), tlen) == 0) {//对于每一个模式串判断时候能够进行匹配 
                printf("pos %d : %s\n", i - tlen + 1, t[thash[h][j]].c_str());
            }
        }
    }
    return ;
}

int main() {
    int n;
    string s;
    vector<string> t(100);//存放若干个等待匹配的模式串 
    cin >> n;
    for (int i = 0; i < n; i++) cin >>t[i];
    while (cin >> s) {
        RabinKarp(s, t);
    }
    return 0;
}

Shift-and

基本思想

将模式串中出现的每一个字符的出现位置进行编码,如下图所示,1表示在此位置出现。
在这里插入图片描述
接下来初始化一个p=0,每次我们执行如下操作。
在这里插入图片描述
如何理解?我们每次让p左移一位,当前p最左边的位置表示当前能够有机会匹配到的最大长度(从最左边1到最右边的距离,例如我当前是0100,说明上一次我匹配到了两位,接下来我要匹配第三位)。
只有上一次能够成功匹配,我们上一次的1才能左移,意味着我要匹配下一位;如果我们上一次我们没有成功匹配,那么上一次我们得到的结果将会是0,因为我们有一个|1的操作,意味着我可以得到001,此时我将匹配第一位。
匹配完一次之后,我们需要判断当前最左边的1是否已经移动了整个字符串的长度。
在这里插入图片描述

Shift-and使用的特殊情景:
在这里插入图片描述
此时可以给以上模式串进行以下编码:
a:101
b:101
c:110
d:010

代码

#include <iostream>
using namespace std;

#define TEST(func, s, t) { \
    printf("%s(%s, %s) = %d\n", #func, s, t, func(s, t)); \
}

int shift_and(const char *s, const char *t) {
    int code[256] = {0}, n = 0;//code表示每一个字符在此规则下的编码 
    for (n = 0; t[n]; n++) code[t[n]] |= (1 << n);//此时的|相当于+ 
    int p = 0;
    for (int i = 0; s[i]; i++) {
        p = (p << 1 | 1) & code[s[i]];
        if (p & (1 << (n - 1))) return i - n + 1;
    }
    return -1;
}

int main() {
    char s[100], t[100];
    while (~scanf("%s%s", s, t)) {
        TEST(shift_and, s, t);
    }
    return 0;
}

Shift-or

和Shift-and相比,Shift-or只有编码方式的变化。在Shift-or中,有效位用0表示,此时可以少掉左移以后的或1操作。
在这里插入图片描述

字典树

1.Trie字典树

基本思想

在这里插入图片描述
图中的节点有红色和白色之分,红色表示从根节点到当前位置记录了一个单词。
在这里插入图片描述
树的 节点 代表 集合
树的 边 代表 关系

在这里插入图片描述
为什么要如此设计?
以树形结构的形式进行存储大大减小了查询的开销,例如我们要查一个单词hello,我们只需要按照以下流程查找:查找h,如果未查到则直接判断不存在此单词,反之查找e,如果未查到则直接判断不存在此单词,反之查找l,以此类推。也就是说,我们只是相当于对这个单词遍历了一边,时间复杂度相当低。

代码

#include <iostream>
using namespace std;

#define BASE 26

typedef struct Node {//每个节点存放26个子节点,以及一个flag值表示走到当前节点的位置能否表示一个单词 
    struct Node *next[BASE];
    int flag;
} Node;

Node *getNewNode() {//创建一个节点 
    Node *p = (Node *)malloc(sizeof(Node));
    for (int i = 0; i < BASE; i++) p->next[i] = NULL;
    p->flag = 0;
    return p;
}

void insert(Node *root, const char *s) {//插入一个单词 
    Node *p = root;//当前检查到的位置 
    for (int i = 0; s[i]; i++) {
        int ind = s[i] - 'a';//当前字符的下标 
        if (p->next[ind] == NULL) p->next[ind] = getNewNode();//如果当前位置为空,表示这棵树上不存在当前位置的字符,此时需要创建 
        p = p->next[ind];//让p走到下一个位置 
    }
    p->flag = 1;//表示走到当前位置是一个单词 
    return ;
}

int find(Node *root, const char *s) {//查找单词 
    Node *p = root;//表示当前检查到的位置 
    for (int i = 0; s[i]; i++) {
        int ind = s[i] - 'a';//当前字符表示的下标 
        p = p->next[ind];//p走到下一个位置 
        if (p == NULL) return 0;//p为空表示匹配到一半时断掉了,也即不存在此单词 
    }
    return p->flag;//即使每一个字符都成功匹配,我们也需要判断最走到后一个节点的路径是否表示了一个完整的单词 
}

void clear(Node *root) {//递归的方式进行空间释放 
    if (root == NULL) return ;
    for (int i = 0; i < BASE; i++) clear(root->next[i]);
    free(root);
    return ;
}

void output(Node *root, int k, char *buff) {//按照字典序输出每一个单词 
    buff[k] = 0;//用于下面输出的遍历,一但输出到当前位置可以立即停止 
    if (root->flag) {//找到了单词 
        printf("find : %s\n", buff);
    }
    for (int i = 0; i < BASE; i++) {//每一个子节点 
        if (root->next[i] == NULL) continue;
        buff[k] = i + 'a';//根据下标求出对应的字符 
        output(root->next[i], k + 1, buff);//深搜的方式向下继续查找 
    }
    return ;
}

int main() {
    int op;
    char s[100];
    Node *root = getNewNode();
    do {
        cin >> op;
        if (op == 3) break;
        cin >> s;
        switch (op) {
            case 1: {
                printf("insert %s to trie\n", s);
                insert(root, s);
            } break;
            case 2: {
                printf("find %s from trie : %d\n",s, find(root, s));
            } break;
        }
    } while (1);
    output(root, 0, s);
    clear(root);
    return 0;
}

2.双数组字典树

为什么要有双数组字典树?和Trie字典树相比,双数组字典树可以大大节省存储空间。

基本思想

在这里插入图片描述

双数组字典树使用了两个数组base和check去维护了每一个节点的子节点以及flag值两个关系。
一个节点的子节点序号为base+i,其中i为此子节点的字符所对应的编号。例如a对应0,这里维护了子节点的关系。但是有一个问题,例如两个点的base值分别为8,9,这样的话第一个点走‘c’的孩子和第二个点走‘b’的孩子编号相同,显然有问题,因此引入了check数组。
check数组的绝对值为当前点父节点的编号,同时其正负表示了到达当前点能否形成一个单词,负数表示可以,正数表示不可以。
本程序的重点问题在于base值的确定。

双数组字典树有一个缺陷——不支持单词的随机插入。再在程中,双数组字典树一般和Trie字典树结合使用,先将所有的信息插入到Trie字典树中,之后再将Trie字典树转化为双数组字典树,我们可以再双数组字典树中将base和check数组导出,此时我们便可以只拿着这两个数组去做字符串的匹配工作。即离线构造,在线查询。

代码

#include <iostream>
using namespace std;
#define MAXSIZE 100000
#define BASE 26
typedef struct DANode{//双数组字典树节点定义 
	int base,check;
}DANode; 
DANode trie[MAXSIZE+5];
typedef struct Node {//每个节点存放26个子节点,以及一个flag值表示走到当前节点的位置能否表示一个单词 
    struct Node *next[BASE];
    int flag;
} Node;
int da_trie_root=1;//根节点的编号 
Node *getNewNode() {//创建一个节点 
    Node *p = (Node *)malloc(sizeof(Node));
    for (int i = 0; i < BASE; i++) p->next[i] = NULL;
    p->flag = 0;
    return p;
}

void insert(Node *root, const char *s) {//插入一个单词 
    Node *p = root;//当前检查到的位置 
    for (int i = 0; s[i]; i++) {
        int ind = s[i] - 'a';//当前字符的下标 
        if (p->next[ind] == NULL) p->next[ind] = getNewNode();//如果当前位置为空,表示这棵树上不存在当前位置的字符,此时需要创建 
        p = p->next[ind];//让p走到下一个位置 
    }
    p->flag = 1;//表示走到当前位置是一个单词 
    return ;
}

int find(DANode *trie, const char *s) {//查找单词
	int p=da_trie_root; //p为当前节点父节点的编号 
    for (int i = 0; s[i]; i++) { 
        int ind=trie[p].base+s[i]-'a';//当前节点的编号 
        if(abs(trie[ind].check)!=p)return 0;//如果当前节点的父节点不是p,说明找不到 
        p=ind;//更新p 
    }
    return trie[p].check<0;//即使每一个字符都成功匹配,我们也需要判断最走到后一个节点的路径是否表示了一个完整的单词 
}

void clear(Node *root) {//递归的方式进行空间释放 
    if (root == NULL) return ;
    for (int i = 0; i < BASE; i++) clear(root->next[i]);
    free(root);
    return ;
}
int getbase(Node*root,int ind,DANode*trie)//获取当前节点的base值 
{
	int base=2;//从2开始 
	int flag;
	do{
		flag=1;
		for(int i=0;i<BASE;i++)
		{
			if(!root->next[i])continue;
			if(!trie[base+i].check)continue;//如果当前子节点的check值已存在,说明不能取这个位置 
			base++;//更新base 
			flag=0;
			break;
		}
		if(flag)break;
	}while(1);
	return base;
}
void convert_to_double_array_trie(Node*root,int ind,DANode*trie)//将trie转化为datrie 
{
	trie[ind].base=getbase(root,ind,trie);//获取当前节点的base值 
	for(int i=0;i<BASE;i++)//对于每一个子节点设置check 
	{
		if(!root->next[i])continue;
		trie[trie[ind].base+i].check=(root->next[i]->flag?-ind:ind); //设置每一个子节点的check值 
	}
	for(int i=0;i<BASE;i++)//递归对每一个子节点进行转换 
	{
		if(!root->next[i])continue;
		convert_to_double_array_trie(root->next[i],trie[ind].base+i,trie);
	}
	return ;
}
int main() {
    int n;
    cin>>n;
    char s[100];
    Node *root = getNewNode();
    for(int i=0;i<n;i++)
    {
    	cin>>s;
    	insert(root,s);
	}
	convert_to_double_array_trie(root,da_trie_root,trie);
	while (scanf("%s", s) != EOF) {
        printf("find %s from double array trie : %d\n", s, find(trie, s));
    }
    clear(root);
    return 0;
}

3.可持久化字典树

为什么要引入可持久化字典树?或者说可持久化字典树为我们解决了什么特定的问题?
在这里插入图片描述
普通的字典树只支持在当前插入的所有单词中查找是否存在单词x,二可持久化字典树支持在已插入的第i到第j个单词里面查找是否存在x。

基本思想

每插入一个新的单词都创建一个新的根节点,故在可持久化字典树里不只有一个根节点。每次创建的新根不仅包含本次插入的单词,同时也继承了之前所有能够表示的单词。

初始状态是一个0节点,我们插入新的单词abc
在这里插入图片描述
接下来插入def,我们有两个指针,分别指向abc和def的根
在这里插入图片描述
把字母d插入
在这里插入图片描述
蓝色节点下面是a,而绿色节点下面没有a,故需要继承,从绿色节点引一条边
在这里插入图片描述

最终结果,因为蓝色节点下面没有d,所以def可以一直向下插入
在这里插入图片描述
插入abd的最终结果
在这里插入图片描述
插入abg的最终结果
在这里插入图片描述

代码

#include<iostream>
using namespace std;
#define MAX_N 10000
#define BASE 26
int rt[MAX_N+5]={0};//记录每一个根节点的序号 
int ch[MAX_N*30][BASE]={0};//记录每一个节点的序号 
int val[MAX_N*30]={0};//到达每一个点包含的单词数量 
int cnt=0;//记录编号,递增 
void insert(int o,int lst,const char*s)//在o为根节点的树插入s,并继承以lst为根的树 
{
	for(int i=0;s[i];i++)//遍历字符串每一位 
	{
		int ind=s[i]-'a';
		ch[o][ind]=++cnt;//节点编号赋值 
		for(int j=0;j<BASE;j++)//对于当前节点每一条边 
		{
			if(ch[o][j])continue;//只会出现一次,即在上方赋值过 
			ch[o][j]=ch[lst][j];//未出现则赋值为lst节点以j为边的下一个节点的序号 
		}
		o=ch[o][ind];//更新o为o以ind为边的下一个节点 
		lst=ch[lst][ind];
		val[o]+=val[lst];//将lst为节点的val值拷贝给o节点 
	}
	val[o]+=1;
	return ;
}
int find_once(int a,const char*s)//在a中查s出现的数量 
{
	int p=rt[a];//p记录当前编号 
	for(int i=0;s[i];i++)
	{
		p=ch[p][s[i]-'a'];
	}
	return val[p];
}
int query(int a,int b,const char*s)//作差,查询a,b区间是否出现s 
{
	int x1=find_once(b,s);
	int x2=find_once(a-1,s);
	return x1-x2;
}
int main()
{
	int n;
	cin>>n;
	char s[100];
	for(int i=1;i<=n;i++)
	{
		cin>>s;
		rt[i]=++cnt;//根节点编号赋值 
		insert(rt[i],rt[i-1],s); 
	}
	int a,b;
	while (~scanf("%d%d%s", &a, &b, s)) {
        printf("from %d to %d, find %s : %d\n",
            a, b, s, query(a, b, s)
        );
    }
	return 0;
}

AC自动机

基本思想

AC自动机是结合了KMP算法的思想,以字典树为基础实现的。
在KMP中,对于模式串每个位置都记录了最大匹配长度,这样可以在失配的时候,直接据此对模式串进行一个大距离的移动,而不需要逐一匹配。在若干个单词构成的字典树里,如果我们想知道在一个模式串中这些单词出现的有哪些,我们同样可以在失配的时候直接转向当前位置对应的下一位置,而不需要从头重新进行匹配。
在这里插入图片描述
每一个节点都有一个fail指针,指向在此位置失配后要转向的位置。
在这里插入图片描述

代码

/*************************************************************************
	> File Name: 11.ac.cpp
	> Author: hug
	> Mail: hug@haizeix.com
	> Created Time: 浜? 3/26 21:10:59 2024
 ************************************************************************/

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

#define BASE 26

typedef struct Node {
    int flag;
    struct Node *next[26];
    struct Node *fail;//失败指针 
    const char *s;//记录当前点所表示的单词,便于直接输出 
} Node;

Node *getNewNode() {//初始化一个节点 
    Node *p = (Node *)malloc(sizeof(Node));
    p->flag = 0;
    p->fail = NULL;
    p->s = NULL;
    for (int i = 0; i < BASE; i++) p->next[i] = NULL;
    return p;
}

void insert(Node *root, const char *s) {//插入一个单词 
    Node *p = root;//p指向当前位置 
    for (int i = 0; s[i]; i++) {//遍历单词的每一位 
        int ind = s[i] - 'a';//当前单词对应的下标 
        if (p->next[ind] == NULL) p->next[ind] = getNewNode();//如果当前字符不存在字典树中,创建一个新的节点 
        p = p->next[ind];//p往后走一个,走到当前位置 
    }
    p->s = strdup(s);//当前位置是一个单词的末尾,把s赋值给p->s 
    p->flag = 1;
    return ;
}

void build_ac(Node *root) {//建立ac自动机,以层序遍历的方式 
    queue<Node *> q;
    for (int i = 0; i < BASE; i++) {//对于根节点 
        if (root->next[i] == NULL) continue;
        root->next[i]->fail = root;//对于根节点的下面一层节点,失败指针可以直接连接到根节点 
        q.push(root->next[i]);//进入队列 
    }
    while (!q.empty()) {//不断从队列取出Node 
        Node *cur = q.front(), *p;
        q.pop();
        for (int i = 0; i < BASE; i++) {//对于每一个cur,确定他每一个孩子节点的fail 
            if (cur->next[i] == NULL) continue;
            p = cur->fail;//p指向cur的fail 
            while (p && p->next[i] == NULL) p = p->fail;//如果p后面不能接i+'a',则继续向上寻找p,一直到NULL为止 
            if (p == NULL) p = root;
            else p = p->next[i];
            cur->next[i]->fail = p;
            q.push(cur->next[i]);
        }
    }
    return ;
}

void find_ac(Node *root, const char *s) {//寻找ac自动机中在s中出现的所有单词 
    Node *p = root, *q;//p指向当前节点 
    for (int i = 0; s[i]; i++) {
        int ind = s[i] - 'a';
        while (p && p->next[ind] == NULL) p = p->fail;//如果当前节点下无法找到该字符,则不断向上走 
        if (p == NULL) p = root;
        else p = p->next[ind];
        q = p;
        while (q) {//为什么要while?如果s中包含world,ac自动机中又包含world,rld,ld,那么while才可以保证输出每一个存在的单词 
            if (q->flag) {
                int len = strlen(q->s);
                printf("find [%d, %d] = %s\n", i - len + 1, i, q->s);
            }
            q = q->fail;
        }
    }
    return ;
}

void clear(Node *root) { 
    if (root == NULL) return ;
    for (int i = 0; i < BASE; i++) {
        if (root->next[i] == NULL) continue;
        clear(root->next[i]);
    }
    free(root);
    return ;
}

int main() {
    int n;
    char s[100];
    Node *root = getNewNode();
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%s", s);
        insert(root, s);
    }
    build_ac(root);
    scanf("%s", s);
    find_ac(root, s);
    return 0;
}

线索化优化

借鉴线索二叉树的思想,这里要充分利用空指针。如果一个节点cur的next[i]为NULL,那么让他指向cur->fail->next[i]。
在这里插入图片描述

代码

/*************************************************************************
	> File Name: 11.ac.cpp
	> Author: hug
	> Mail: hug@haizeix.com
	> Created Time: 浜? 3/26 21:10:59 2024
 ************************************************************************/

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

#define BASE 26

typedef struct Node {
    int flag;
    struct Node *next[26];
    struct Node *fail;
    const char *s;
} Node;

vector<Node *> node_vec;

Node *getNewNode() {
    Node *p = (Node *)malloc(sizeof(Node));
    p->flag = 0;
    p->fail = NULL;
    p->s = NULL;
    for (int i = 0; i < BASE; i++) p->next[i] = NULL;
    node_vec.push_back(p);
    return p;
}

void insert(Node *root, const char *s) {
    Node *p = root;
    for (int i = 0; s[i]; i++) {
        int ind = s[i] - 'a';
        if (p->next[ind] == NULL) p->next[ind] = getNewNode();
        p = p->next[ind];
    }
    p->s = strdup(s);
    p->flag = 1;
    return ;
}

void build_ac(Node *root) {
    queue<Node *> q;
    q.push(root);
    while (!q.empty()) {
        Node *cur = q.front(), *p;
        q.pop();
        for (int i = 0; i < BASE; i++) {
            if (cur->next[i] == NULL) {
                if (cur == root) cur->next[i] = root;
                else cur->next[i] = cur->fail->next[i];
                continue;
            }
            p = cur->fail;
            if (p == NULL) p = root;
            else p = p->next[i];
            cur->next[i]->fail = p;
            q.push(cur->next[i]);
        }
    }
    return ;
}

void find_ac(Node *root, const char *s) {
    Node *p = root, *q;
    for (int i = 0; s[i]; i++) {
        int ind = s[i] - 'a';
        p = p->next[ind];
        q = p;
        while (q) {
            if (q->flag) {
                int len = strlen(q->s);
                printf("find [%d, %d] = %s\n", i - len + 1, i, q->s);
            }
            q = q->fail;
        }
    }
    return ;
}

void clear() {
    for (int i = 0; i < node_vec.size(); i++) {
        free(node_vec[i]);
    }
    return ;
}

int main() {
    int n;
    char s[100];
    Node *root = getNewNode();
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%s", s);
        insert(root, s);
    }
    build_ac(root);
    scanf("%s", s);
    find_ac(root, s);
    clear();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值