序列比对(二十六)——精准匹配之KMP算法、Trie树以及AC自动机

原创:hxj7

前文已经介绍过KMP算法和Trie树,本文将在此基础上介绍AC自动机。

之前的序列比对文章大都在利用动态规划算法解决字符串的非精准匹配(允许错配、插入和缺失),比如全局比对和局部比对问题。当然,后来我们还介绍了模序发现和中间字符串问题,并初次学习了如何用分支定界法解决这一类问题。

既然有非精准匹配问题,就有精准匹配问题。所谓精准匹配,就是两个字符串在比对时,不允许错配、插入和缺失。其实,笔者之前已经有过介绍:

KMP算法

《算法(四)(转载)KMP算法》一文介绍了KMP算法,该算法常用来解决如何在一个长字符串(模板串)中寻找一个短字符串(模式串)的问题。其特点就是要预先对模式串进行处理,对其构建一个next数组,该数组中存放着模式串每一个位置所对应的跳转指针,这样做的效果是模板串的指针不用回溯,一直在递增,从而增加效率。

KMP算法的完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXLEN 1000

void getNext(const char *pat, int *next, const int n) {
/* assume n >= 2 */
    int i, j;
    for (next[0] = -1, next[1] = 0, i = 1, j = 0; i < n - 1;) {
        while (j != -1 && pat[i] != pat[j])
            j = next[j];
        next[++i] = ++j;
    }
}

int KMP(const char *temp, const int m, const char *pat, const int n) {
    int i, j;
    int *next = (int*) malloc(sizeof(int) * n);
    getNext(pat, next, n);
    for (i = 0, j = 0; i < m && j < n;) {
        if (j == -1 || temp[i] == pat[j]) {
            i++;
            j++;
        } else {
            j = next[j];
        }
    }
    free(next);
    if (j < n)
        return -1;
    return i - j;
}

int main(void) {
    char t[MAXLEN], p[MAXLEN];
    int index;
    printf("the temp str: ");
    scanf("%s", t);
    printf("the pattern str: ");
    scanf("%s", p);
    if ((index = KMP(t, strlen(t), p, strlen(p))) == -1)
        printf("Not found!\n");
    else
        printf("Index is %d.\n", index);
    return 0;
} 

其效果如下:


image

Trie树(字典树)

《算法(五)字典树算法快速查找单词前缀》一文介绍了Trie树,该算法常用来解决如何在多个模板串中寻找一个模式串的问题。其特点就是将多个模板串装填进一个树结构中,虽然使用了较多的空间,但是查找效率得到了提升。

Trie树的完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N 26
#define MAXLEN 1000

struct Node {
    struct Node *a[N];
    int is_end;
};
typedef struct Node *tnode;

tnode init() {
    tnode p;
    int i;
    if ((p = (tnode) malloc(sizeof(struct Node))) == NULL) {
        fputs("Error: out of space!\n", stderr);
        exit(1);
    }
    for (i = 0; i < N; i++)
        p->a[i] = NULL;
    p->is_end = 0;
    return p;
}

void delete(tnode p) {
    int i;
    for (i = 0; i < N; i++)
        if (p->a[i] != NULL)
            delete(p->a[i]);
    free(p);
}

void insert(const char *s, tnode p) {
    int i;
    while (*s != '\n' && *s != '\0') {
        i = *s - 'a';
        if (p->a[i] == NULL) {
            p->a[i] = init();
        }
        p = p->a[i];
        s++;
    }
    p->is_end = 1;
}

int find(const char *s, tnode p) {
    int i;
    while (*s != '\n' && *s != '\0') {
        i = *s - 'a';
        if (p->a[i] == NULL)
            return 0;
        p = p->a[i];
        s++;
    }
    if (p->is_end)
        return 1;
    return 0;
}

void strLower(char *s) {
    while (*s != '\0') {
        if (*s >= 'A' && *s <= 'Z') {
            *s += 32;
        }
        s++;
    }
}

int main(int argc, char *argv[]) {
    FILE *fpp, *fpw;
    char buf[MAXLEN];
    tnode root = init();
    if (argc < 3) {
        fputs("Usage: trie.exe <pattern.txt> <words.txt>\n", stderr);
        exit(2);
    }
    if ((fpw = fopen(argv[2], "r")) == NULL) {
        fprintf(stderr, "Error: cannot open %s\n", argv[2]);
        exit(4);
    }
    while (fgets(buf, MAXLEN, fpw) != NULL) {
        strLower(buf);
        insert(buf, root);
    }
    if (! feof(fpw)) {
        fprintf(stderr, "Error: error while reading %s\n", argv[2]);
        exit(8);
    }
    fclose(fpw);
    if ((fpp = fopen(argv[1], "r")) == NULL) {
        fprintf(stderr, "Error: cannot open %s\n", argv[1]);
        exit(4);
    }
    while (fgets(buf, MAXLEN, fpp) != NULL) {
        strLower(buf);
        if (find(buf, root)) 
            printf("%s", buf);
    }
    if (! feof(fpp)) {
        fprintf(stderr, "Error: error while reading %s\n", argv[1]);
        exit(8);
    }    
    fclose(fpp);
    delete(root);
}

其效果如下:


image

其中,pat.txt文件内容(里面是多个模式串)如下:

how
are
you

words.txt文件内容(里面是多个模板串)如下:

lily
we
are
friends

AC自动机

本文要介绍的AC自动机算法,是用来解决如何有效地在一个模板串中查找多个模式串的问题。该算法是建立在KMP算法和Trie树基础上的。其特点是:

  • 预先将多个模式串装填进Trie树中。
  • 在进行查找(匹配)之前,对Trie树中的每个有效节点构建fail指针,该指针的作用类似KMP算法中next数组的作用:如果到该节点时匹配失败,就可以利用fail指针跳转到下一个可利用节点继续进行匹配,从而避免了模板串的回溯而提升查找效率。

如同KMP算法的next数组充分利用了模式串的内部信息,AC自动机中的fail指针也是充分利用了多个模式串的内部信息,每一次跳转都是跳到“最大可利用后缀子字符串”的节点。

笔者在学习该算法时,主要参考了这一篇文章(https://www.jianshu.com/p/641142c6d477),看里面的图非常有助于理解AC自动机原理。

关于AC自动机的实现有几点说明:

  • 在对Trie树里所有有效节点构建fail指针的过程中,采用了BFS(广度优先搜索)的策略,具体的实现是利用了队列这种数据结构。采用BFS的原因是Trie树中某一层节点的fail指针指向的节点一定是来自于上面的层(可能是上面第一层,也有可能是上面第二层等等,甚至是根节点)。
  • 无论是Trie树还是队列,都是利用一种链表实现的,不得不感叹,对链表进行操作真是很容易出错,经常要填坑。

AC自动机的完整代码如下:

#include <stdio.h>
#include <stdlib.h>
#define ALPHA_NUM 26
#define QUEUE_EMPTY NULL
#define BUFSIZE 64
#define MAXTEMP 10000

/* Data structure for trie */
struct TNode {
    struct TNode *a[ALPHA_NUM];
    struct TNode *fail;
    struct TNode *parent;
    int c;
    int is_end;
};
typedef struct TNode *tnode;

tnode initTrie(int c, tnode parent) {
    tnode p;
    int i;
    if ((p = (tnode) malloc(sizeof(struct TNode))) == NULL) {
        fputs("Error: out of space!\n", stderr);
        exit(1);
    }
    for (i = 0; i < ALPHA_NUM; i++)
        p->a[i] = NULL;
    p->fail = NULL;
    p->parent = parent;
    p->c = c;
    p->is_end = 0;
    return p;
}

void deleteTrie(tnode p) {
    int i;
    for (i = 0; i < ALPHA_NUM; i++)
        if (p->a[i] != NULL)
            deleteTrie(p->a[i]);
    free(p);
}

void insertTrie(const char *s, tnode p) {
    int i;
    while (*s != '\n' && *s != '\0') {
        i = *s - 'a';
        if (p->a[i] == NULL) {
            p->a[i] = initTrie(*s, p);
        }
        p = p->a[i];
        s++;
    }
    p->is_end = 1;
}

/*
void printTrie(tnode p) {
    int i;
    printf("%c ", p->c);
    for (i = 0; i < ALPHA_NUM; i++) {
        if (p->a[i] != NULL)
            printTrie(p->a[i]);
    }
}
*/

void backtrack(char *s, const int n, tnode p) {
    int i, j, c;
    for (i = 0; i < n - 1 && p != NULL && p->parent != NULL; i++) {
        s[i] = p->c;
        p = p->parent;
    }
    if (i >= n - 1) {
        fputs("Error: pattern word is too long!\n", stderr);
        exit(2);
    }
    for (j = 0; j < i / 2; j++) {
        c = s[j];
        s[j] = s[i - 1 - j];
        s[i - 1 - j] = c;
    }
    s[i] = '\0';
}

/* Data structure for queue */
typedef tnode QNodeType;

struct QNode {
    QNodeType value;
    struct QNode *next;
};
typedef struct QNode *qnode;

qnode initQueue(QNodeType v) {
    qnode p;
    if ((p = (qnode) malloc(sizeof(struct QNode))) == NULL) {
        fputs("Error: out of space!\n", stderr);
        exit(1);
    }
    p->value = v;
    p->next = NULL;
    return p;
}

void addQueue(QNodeType v, qnode head) {
    qnode p;
    for (p = head; p->next != NULL; p = p->next)
        ;
    p->next = initQueue(v);
}

int isEmptyQueue(qnode head) {
    return head->next == NULL ? 1 : 0;
}

QNodeType popQueue(qnode head) {
    qnode p;
    QNodeType q;
    if (isEmptyQueue(head))
        return QUEUE_EMPTY;
    else {
        p = head->next;
        head->next = p->next;
        q = p->value;
        free(p);
        return q;
    }
}

void destroyQueue(qnode head) {
    while (popQueue(head) != QUEUE_EMPTY)
        ;
    free(head);
}

void strLower(char *s) {
    while (*s != '\0') {
        if (*s >= 'A' && *s <= 'Z') {
            *s += 32;
        }
        s++;
    }
}

void getFailPointer(tnode root) {
    int i;
    qnode head = initQueue(NULL);
    tnode this, tmp;
    addQueue(root, head);
    while (! isEmptyQueue(head)) {
        this = popQueue(head);
        for (i = 0; i < ALPHA_NUM; i++) {
            if (this->a[i] != NULL) {
                addQueue(this->a[i], head);
                tmp = this;
                while (tmp->fail != NULL) {
                    if (tmp->fail->a[i] != NULL) {
                        this->a[i]->fail = tmp->fail->a[i];
                        break;
                    } else {
                        tmp = tmp->fail;
                    }
                }
                if (tmp->fail == NULL)
                    this->a[i]->fail = root;
            }
        }       
    }
    destroyQueue(head);
}

void ACauto(char *buf, const int n, char *temp, tnode root) {
    char *s = temp;
    tnode p, tmp;
    int i;
    for (p = root; *s != '\0'; s++) {
        i = *s - 'a';
        while (p->a[i] == NULL && p->fail != NULL)  /* 这里必须先判断p->a[i],否则有可能错过root->a[i] != NULL的情况 */
            p = p->fail;
        if ((p = p->a[i]) == NULL)
            p = root;
        for (tmp = p; tmp->fail != NULL; tmp = tmp->fail)
            if (tmp->is_end) {
                backtrack(buf, BUFSIZE, tmp);
                printf("%s\n", buf);
            }
    }
}

int main(int argc, char *argv[]) {
    FILE *fpp, *fpt;
    char buf[BUFSIZE];
    char temp[MAXTEMP];
    tnode root = initTrie('\0', NULL);
    int n;
    if (argc < 3) {
        fputs("Usage: trie.exe <pattern.txt> <template.txt>\n", stderr);
        exit(2);
    }
    if ((fpp = fopen(argv[1], "r")) == NULL) {
        fprintf(stderr, "Error: cannot open %s\n", argv[1]);
        exit(4);
    }
    while (fgets(buf, BUFSIZE, fpp) != NULL) {
        strLower(buf);
        insertTrie(buf, root);
    }
    if (! feof(fpp)) {
        fprintf(stderr, "Error: error while reading %s\n", argv[1]);
        exit(8);
    }
    fclose(fpp);
    if ((fpt = fopen(argv[2], "r")) == NULL) {
        fprintf(stderr, "Error: cannot open %s\n", argv[2]);
        exit(4);
    }
    n = fread(temp, 1, MAXTEMP - 1, fpt);
    temp[n] = '\0';
    strLower(temp);
    if (! feof(fpt)) {
        fprintf(stderr, "Error: error while reading %s\n", argv[2]);
        exit(8);
    }    
    fclose(fpt);
    getFailPointer(root);
    ACauto(buf, BUFSIZE, temp, root);
    deleteTrie(root);
}

其效果如下:


image

其中pat.txt文件内容(里面是多个模式串)如下:

how
are
you

temp.txt文件内容(里面是模板串)如下:

iloveautumnhowaboutyou

至此,本文就全部结束了,谢谢大家看此长文。如果有发现错误或者有任何建议和意见,欢迎指正!

(公众号:生信了)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容