BM算法詳解


來源

在沒有BM算法時,其原始算法是從后往前進行匹配,需要兩層循環,判斷以某個字符為結尾的子串是否和模式串相等,這種算法也稱作暴搜;

貼上代碼:

void BLS(string s, string p) {
    int s_len = s.size(), p_len = p.size();
    int j = 0, i = 0;
    
    while (j <= s_len - p_len) {
        for (i = p_len - 1; i >= 0 && p[i] == s[i + j]; --i) {}
        if (i < 0) {
            cout << "match: " << i + j + 1 << endl;
            j += p_len;
        }
        else
            j++;
    }
}

算法的思想還是比較容易理解的,i和j分別指的是,模式串中已經匹配的位數,模式串相對於原串移動的位數;

移動規則

算法包含了兩個重要的內容,分別是好后綴和壞字符的規則;

  • 壞字符:當模式串和原串的字符並不匹配時,原串中的字符就稱為壞字符;

  • 好后綴:模式串和原串的字符相等時所有的字符串,比如ABCD和BCD,那么它的好后綴則包括第一次匹配的D,和第二次匹配的CD,還有第三次的BCD;

例子:

BM算法的向右移動模式串的距離就是取壞字符和好后綴算法得到的最大值;

這兩個內容分別擁有着幾條規則,需要注意:

壞字符

  1. 壞字符沒出現在模式串中,這時可以把模式串移動到壞字符的下一個位置,繼續比較;

    比如說,ABC和D,其中D和A對齊,因為不匹配,所以D會移動到和B對齊;

  2. 壞字符出現在模式串中,這時可以把模式串中第一個出現(從后往前數第一個出現,也就是從前往后數,最靠右出現的)的壞字符和原串的壞字符對齊;

    比如說,BCCBCAD和AAD,假如其中AAD和BCC對齊,發現D和C無法匹配,移動到和BCA匹配,此時同樣不匹配,但是A存在於模式串中,所以移動到模式串中壞字符出現的上一次位置,也就是向后移一位,和CAD對齊;

    當首次比較就無法匹配時,那么肯定是運用壞字符規則,因為並不存在好后綴;


所以我們發現壞字符的移動規則公式為:

后移位數 = 壞字符的位置 - 模式串中的上一次出現位置

那用代碼表示則是

void preBmBc(string x, vector<int>& bmBc) {
    int i = 0;
    int len = (int)x.size();
    // 全部更新為自己的長度
    for (i = 0; i < ASIZE; ++i) {
        bmBc[i] = len;
    }

    // 計算字符串中每個字符距離字符串尾部的長度
    for (i = 0; i < x.size() - 1; ++i) {
        bmBc[x[i]] = len - i - 1;
    }
}

首先應該知道的是,bmBc存儲的是壞字符出現的位置距離模式串末尾的最大長度;

前一個循環用來處理第一種規則,因為遇見不匹配時,直接移動模式串的長度;

后一個循環處理第二種規則,需要注意的是,因為要保證最靠右原則,所以要從頭開始循環,從而使得當遇見相同的字符,后者可以將前者進行覆蓋。

好后綴

  1. 模式串中有子串匹配上好后綴,此時移動模式串,讓子串和好后綴對齊即可,如果超過一個子串匹配上好后綴,則選擇最靠左邊的子串對齊;

  2. 模式串中沒有子串匹配上后綴,接着尋找模式串中的一個和好后綴(子串)相等的最長前綴,並讓該前綴和好后綴對齊即可;

  3. 模式串中沒有子串匹配上好后綴,並且在模式串中找不到和好后綴(子串)相等的最長前綴,此時,直接移動模式到好后綴的下一個字符;

綜上,好后綴的規則:

后移位數 = 好后綴的位置 - 模式串中的上一次出現的位置

用代碼表示為:

我們先看suffix數組的計算

// 計算以i為邊界,與模式串后綴匹配的最大長度(區間的概念)
void suffix(string x, vector<int>& suff) {
//    int len = x.size();
//    int f = 0, q = len - 1, i;
//
//    // 設置最后一個為自己的長度
//    suff[len-1] = len;
//
//    // 從倒數第二個開始,
//    for (i =  len - 2; i >= 0; --i) {
//        if (i > q && suff[i + len - 1 - f] < i - q) {
//            suff[i] = suff[i + len - 1 + f];
//        }
//        else {
//            if (i < q) {
//                q = i;
//            }
//            f = i;
//            while (q >= 0 && x[q] == x[len - 1 - f + q]) {
//                --q;
//            }
//
//            suff[i] = f - q;
//        }
//    }
    
    int len = (int)x.size();
    int q;
    for (int i = len - 2; i >= 0; --i) {
        q = i;
        while (q >= 0 && x[q] == x[len-1-i+q]) {
            --q;
        }
        suff[i] = i - q;
    }
    
}

為什么要計算suffix數組呢,因為我們需要通過suffix去計算bmGs。

suffix數組的定義為:以i為邊界,與模式串后綴匹配的最大長度;有可能是和整個后綴相等的子串又或者只是最長前綴又或者都不匹配,它們都會有不同的長度,都不匹配自然是模式串的長度,因為要移動到好后綴的下一個字符。

接着看怎么計算bmGs數組,代碼:

// 好后綴算法的預處理
/*
 有三種情況
 1.模式串中有子串匹配上好后綴
 2.模式串中沒有子串匹配上好后綴,但找到一個最大前綴
 3.模式串中沒有子串匹配上好后綴,但找不到一個最大前綴


 3種情況獲得的bmGs[i]值比較

 3 > 2 > 1

 為了保證其值越來越小

 所以按順序處理3->2->1情況
 */
void preBmGs(string s, vector<int>& bmGs) {

    int i, j;
    int len = (int)s.size();

    vector<int> suff(len, 0);

    suffix(s, suff);

    //全部初始為自己的長度,處理第三種情況
    for (i = 0; i < len; ++i) {
        bmGs[i] = len;
    }

    // 處理第二種情況
    for (i = len - 1; i >= 0; --i) {
        if (suff[i] == i+1) { // 找到合適位置
            for (j = 0; j < len-1; ++j) {
                if (bmGs[j] == len) { // 保證每個位置至多只能被修改一次
                    bmGs[j] = len - 1 - i;
                }
            }
        }
    }

    // 處理第一種情況,順序是從前到后
    for (i = 0; i < len - 1; ++i) {
        bmGs[len - 1- suff[i]] = len - 1 - i;
    }

}

bmGs[i]數組表示存在好后綴時,模式串應該移動的距離,i表示壞字符的位置;

先看第三種情況

很容易理解,自然是模式串的長度;

再看第二種情況

為什么會讓suffix[i] = i + 1,因為suffix存儲的是i位置字符的最長匹配長度,所以其代表的是j...i的長度,只有當表達式成立的時候,說明其是個前綴;

找到了以后,從后往前掃,可以保證最長,因為從i位置往前的前綴是匹配的,那么其移動的距離自然是len - 1 - i;

其加了個判斷,避免后面再遇到前綴時,不再允許更改;

再看第一種情況

因為是子串,不再是前綴,不用加上判斷,況且前者已經處理過;

len - 1- suff[i],因為suffix[i]的長度和好后綴的長度相同,所以得到的是好后綴的前一個字符;

i表示的是以i開始往前數的子串,從該位置要移動到最后一個位置,自然距離就是len - 1 - i

總函數

BM算法的核心在於其移動規則,接下來看最后的處理部分;

void BM(string pattern, string text) {
    int i, j;
    int m = (int)pattern.size(), n = (int)text.size();
    vector<int> bmGs(m);
    vector<int> bmBc(ASIZE);

    // 處理好后綴算法
    preBmGs(pattern, bmGs);
    // 處理壞字符算法
    preBmBc(pattern, bmBc);

    j = 0;

    while (j <= n - m) {
        // 模式串向左邊移動
        for (i = m - 1; i >= 0 && pattern[i] == text[i + j]; --i);

        // 給定字符串向右邊移動
        if (i < 0) {
            cout << j << endl;
            j += m; // 移動到模式串的下一個位置
        }
        else {
            if (text[i+j] == 'P') {
                cout << "find" << endl;
            }
            // 取移動位數的最大值向右移動,前者好后綴,向右移動,后者壞字符,向左移動
            //cout << text[i+j] << bmBc[text[i+j]] << endl;
            j += max(bmGs[i], bmBc[text[i+j]] - m + 1 + i);
        }
    }
}

其中 j 表示模式串和原串的距離;

循環中那個for循環,計算着匹配距離,從模式串的長度開始,如果 i 為-1,自然其和模式串是相等的,如果不相等,則按照移動規則來移動(取最大值哦)。

實例

了解一下BM算法是怎么運作的:

  1. 初始化

  2. 首次出現壞字符

    從后往前進行匹配,我們發現S和E並不匹配,而且是一開始比較就無法匹配上,則整個字符串肯定無法匹配;S則稱為壞字符(即原串中無法匹配的字符),因為S並沒出現在模式串中,故我們只能使用第一種規則,即將模式串移到S的下一位對齊;

  3. 壞字符存在模式串中

    移動到模式串中上一次出現的位置;

  4. 開始匹配

    相等,開始匹配;

  5. 存在前綴,和好后綴(子串)相等

    發現不存在子串和好后綴相等,但存在前綴E,移動到和好后綴的E對齊;

  6. 壞字符存在模式串中

    發現P和E不相等,P存在模式串中,運用壞字符規則;

  7. 相等匹配完成

完成!

文章參考以下內容:

阮一峰

博客圓


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM