日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

动态规划之KMP字符匹配算法

發(fā)布時間:2024/4/11 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 动态规划之KMP字符匹配算法 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

動態(tài)規(guī)劃之KMP字符匹配算法

文章目錄

  • 動態(tài)規(guī)劃之KMP字符匹配算法
    • 一、問題引入
    • 二、 KMP 算法概述
    • 三、 狀態(tài)機概述
    • 四、 構(gòu)建狀態(tài)轉(zhuǎn)移圖
    • 五、 代碼實現(xiàn)
    • 六、 最后總結(jié)

本文的KMP算法是通過狀態(tài)機的改進版本,普通的KMP實現(xiàn)方法請點擊:👇
https://blog.csdn.net/wolfGuiDao/article/details/108299448

一、問題引入

KMP 算法(Knuth-Morris-Pratt 算法) 是?個著名的字符串匹配算法, 效率很?, 但是確實有點復(fù)雜。

先在開頭約定, 本?? pat 表?模式串, ?度為 M , txt 表??本串,?度為 N 。 KMP 算法是在 txt 中查找?串 pat , 如果存在, 返回這個?串的起始索引, 否則返回 -1。

讀者?過的 KMP 算法應(yīng)該是, ?波詭異的操作處理 pat 后形成?個?維的數(shù)組 next , 然后根據(jù)這個數(shù)組經(jīng)過??波復(fù)雜操作去匹配 txt 。 時間復(fù)雜度 O(N), 空間復(fù)雜度 O(M)。

其實它這個 next 數(shù)組就相當(dāng)于 dp 數(shù)組, 其中元素的含義跟 pat 的前綴和后綴有關(guān), 判定規(guī)則?較復(fù)雜, 不好理解。 本?則??個?維的 dp 數(shù)組(但空間復(fù)雜度還是 O(M)) , 重新定義其中元素的含義, 使得代碼?度??減少, 可解釋性??提?。

二、 KMP 算法概述

?先還是簡單介紹?下 KMP 算法和暴?匹配算法的不同在哪?, 難點在哪?, 和動態(tài)規(guī)劃有啥關(guān)系。

暴?的字符串匹配算法很容易寫, 看?下它的運?邏輯:

// 暴?匹配(偽碼) int search(String pat, String txt) {int M = pat.length;int N = txt.length;for (int i = 0; i <= N - M; i++) {for (int j = 0; j < M; j++) {if (pat[j] != txt[i + j])break;}// pat 全都匹配了if (j == M) return i;} // txt 中不存在 pat ?串return -1; }

對于暴?算法, 如果出現(xiàn)不匹配字符, 同時回退 txt 和 pat 的指針, 嵌套 for 循環(huán), 時間復(fù)雜度O(MN)O(MN)O(MN), 空間復(fù)雜度O(1)O(1)O(1) 最主要的問題是,如果字符串中重復(fù)的字符?較多, 該算法就顯得很蠢。

?如 txt = “aaacaaab” pat = “aaab”:

很明顯, pat 中根本沒有字符 c, 根本沒必要回退指針 i , 暴?解法明顯多做了很多不必要的操作。

KMP 算法的不同之處在于, 它會花費空間來記錄?些信息, 在上述情況中就會顯得很聰明:

再?如類似的 txt = “aaaaaaab” pat = “aaab”, 暴?解法還會和上?那個例??樣蠢蠢地回退指針 i , ? KMP 算法?會耍聰明:

因為 KMP 算法知道字符 b 之前的字符 a 都是匹配的, 所以每次只需要?較字符 b 是否被匹配就?了。

KMP 算法永不回退 txt 的指針 i , 不?回頭路(不會重復(fù)掃描txt ) , ?是借助 dp 數(shù)組中儲存的信息把 pat 移到正確的位置繼續(xù)匹配, 時間復(fù)雜度只需 O(N), ?空間換時間, 所以我認(rèn)為它是?種動態(tài)規(guī)劃算法。

KMP 算法的難點在于, 如何計算 dp 數(shù)組中的信息? 如何根據(jù)這些信息正確地移動 pat 的指針? 這個就需要確定有限狀態(tài)?動機來輔助了, 別怕這種??上的?學(xué)詞匯, 其實和動態(tài)規(guī)劃的 dp 數(shù)組如出?轍, 等你學(xué)會了也可以拿這個詞去嚇唬別?。

還有?點需要明確的是: 計算這個 dp 數(shù)組, 只和 pat 串有關(guān)。 意思是說, 只要給我個 pat , 我就能通過這個模式串計算出 dp 數(shù)組, 然后你可以給我不同的 txt , 我都不怕, 利?這個 dp 數(shù)組我都能在 O(N) 時間完成字符串匹配

具體來說, ?如上?舉的兩個例?:

txt1 = "aaacaaab" pat = "aaab" txt2 = "aaaaaaab" pat = "aaab"

我們的 txt 不同, 但是 pat 是?樣的, 所以 KMP 算法使?的 dp 數(shù)組是同?個。

只不過對于 txt1 的下?這個即將出現(xiàn)的未匹配情況:


dp 數(shù)組指? pat 這樣移動:

這個 j 不要理解為索引, 它的含義更準(zhǔn)確地說應(yīng)該是狀態(tài)(state) ,所以它會出現(xiàn)這個奇怪的位置

?對于 txt2 的下?這個即將出現(xiàn)的未匹配情況:

dp 數(shù)組指? pat 這樣移動:

明?了 dp 數(shù)組只和 pat 有關(guān), 那么我們這樣設(shè)計 KMP 算法就會?較漂亮:

public class KMP {private int[][] dp;private String pat;public KMP(String pat) {this.pat = pat;// 通過 pat 構(gòu)建 dp 數(shù)組// 需要 O(M) 時間} public int search(String txt) {// 借助 dp 數(shù)組去匹配 txt// 需要 O(N) 時間} }

這樣, 當(dāng)我們需要?同? pat 去匹配不同 txt 時, 就不需要浪費時間構(gòu)造 dp 數(shù)組了:

KMP kmp = new KMP("aaab"); int pos1 = kmp.search("aaacaaab"); //4 int pos2 = kmp.search("aaaaaaab"); //4



  • 代碼描述:
#include <iostream> #include <string> #include <vector> using namespace std;vector<int> GetNext(string& match) {vector<int> next(match.size() + 1,0);next[0] = -1;int i = 2;int k = 0;while(i < match.size()){if(match[i - 1] == match[k]){next[i] = k + 1;k = next[i];i++;}else if(k == -1){next[i] = 0;i++;}else {k = next[k];}}return next; }int main() {string str;string match;while(getline(cin,str)){getline(cin,match);vector<int> next = GetNext(match);int flag = 1;int i = 0;int j = 0;while(i < str.size()){if(str[i] == match[j]){i++;j++;if(j == match.size()){flag = 0;cout<<i - match.size()<<" ";j = next[j];}}else{j = next[j];if(j == -1){j = 0;i++;}}}if(flag)cout<<-1<<endl;}return 0; }

三、 狀態(tài)機概述

為什么說 KMP 算法和狀態(tài)機有關(guān)呢? 是這樣的, 我們可以認(rèn)為 pat 的匹配就是狀態(tài)的轉(zhuǎn)移。 ?如當(dāng) pat = “ABABC”:

如上圖, 圓圈內(nèi)的數(shù)字就是狀態(tài), 狀態(tài) 0 是起始狀態(tài), 狀態(tài) 5( pat.length ) 是終?狀態(tài)。

開始匹配時 pat 處于起始狀態(tài), ?旦轉(zhuǎn)移到終?狀態(tài), 就說明在 txt 中找到了 pat 。 ?如說當(dāng)前處于狀態(tài) 2, 就說明字符 “AB” 被匹配:

另外, 處于不同狀態(tài)時, pat 狀態(tài)轉(zhuǎn)移的?為也不同。 ?如說假設(shè)現(xiàn)在匹配到了狀態(tài) 4, 如果遇到字符 A 就應(yīng)該轉(zhuǎn)移到狀態(tài) 3, 遇到字符 C 就應(yīng)該轉(zhuǎn)移到狀態(tài) 5, 如果遇到字符 B 就應(yīng)該轉(zhuǎn)移到狀態(tài) 0:

具體什么意思呢, 我們來?個個舉例看看。 ?變量 j 表?指向當(dāng)前狀態(tài)的指針, 當(dāng)前 pat 匹配到了狀態(tài) 4:

如果遇到了字符 “A”, 根據(jù)箭頭指?, 轉(zhuǎn)移到狀態(tài) 3 是最聰明的:

如果遇到了字符 “B”, 根據(jù)箭頭指?, 只能轉(zhuǎn)移到狀態(tài) 0(?夜回到解放前) :

如果遇到了字符 “C”, 根據(jù)箭頭指?, 應(yīng)該轉(zhuǎn)移到終?狀態(tài) 5, 這也就意味著匹配完成:

當(dāng)然了, 還可能遇到其他字符, ?如 Z, 但是顯然應(yīng)該轉(zhuǎn)移到起始狀態(tài) 0,因為 pat 中根本都沒有字符 Z:

這?為了清晰起?, 我們畫狀態(tài)圖時就把其他字符轉(zhuǎn)移到狀態(tài) 0 的箭頭省略, 只畫 pat 中出現(xiàn)的字符的狀態(tài)轉(zhuǎn)移:

好了,上面我們已經(jīng)逼逼賴賴了那么多,重點來了,KMP 算法最關(guān)鍵的步驟就是構(gòu)造這個狀態(tài)轉(zhuǎn)移圖 。

要確定狀態(tài)轉(zhuǎn)移的?為, 得明確兩個變量, ?個是當(dāng)前的匹配狀態(tài), 另?個是遇到的字符;確定了這兩個變量后, 就可以知道這個情況下應(yīng)該轉(zhuǎn)移到哪個狀態(tài)。

為了描述狀態(tài)轉(zhuǎn)移圖, 我們定義?個?維 dp 數(shù)組, 它的含義如下:

dp[j][c] = next 0 <= j < M, 代表當(dāng)前的狀態(tài) 0 <= c < 256, 代表遇到的字符(ASCII 碼) 0 <= next <= M, 代表下?個狀態(tài) dp[4]['A'] = 3 表?: 當(dāng)前是狀態(tài) 4, 如果遇到字符 A, pat 應(yīng)該轉(zhuǎn)移到狀態(tài) 3dp[1]['B'] = 2 表?: 當(dāng)前是狀態(tài) 1, 如果遇到字符 B, pat 應(yīng)該轉(zhuǎn)移到狀態(tài) 2

根據(jù)我們這個 dp 數(shù)組的定義和剛才狀態(tài)轉(zhuǎn)移的過程, 我們可以先寫出 KMP算法的 search 函數(shù)代碼:

public int search(String txt) {int M = pat.length();int N = txt.length();// pat 的初始態(tài)為 0int j = 0;for (int i = 0; i < N; i++) {// 當(dāng)前是狀態(tài) j, 遇到字符 txt[i],// pat 應(yīng)該轉(zhuǎn)移到哪個狀態(tài)?j = dp[j][txt.charAt(i)];// 如果達到終?態(tài), 返回匹配開頭的索引if (j == M) return i - M + 1;} // 沒到達終?態(tài), 匹配失敗return -1; }

到這?, 應(yīng)該還是很好理解的吧, dp 數(shù)組就是我們剛才畫的那幅狀態(tài)轉(zhuǎn)移圖,

四、 構(gòu)建狀態(tài)轉(zhuǎn)移圖

重重重點又來了!如何構(gòu)建狀態(tài)轉(zhuǎn)移圖???

要確定狀態(tài)轉(zhuǎn)移的?為, 必須明確兩個變量, ?個是當(dāng)前的
匹配狀態(tài), 另?個是遇到的字符
, ?且我們已經(jīng)根據(jù)這個邏輯確定了 dp數(shù)組的含義, 那么構(gòu)造 dp 數(shù)組的框架就是這樣:

for 0 <= j < M: # 狀態(tài)for 0 <= c < 256: # 字符dp[j][c] = next

這個 next 狀態(tài)應(yīng)該怎么求呢? 顯然, 如果遇到的字符 c 和 pat[j] 匹配的話, 狀態(tài)就應(yīng)該向前推進?個, 也就是說 next = j + 1 , 我們不妨稱這種情況為狀態(tài)推進:

如果字符 c 和 pat[j] 不匹配的話, 狀態(tài)就要回退(或者原地不動) , 我們不妨稱這種情況為狀態(tài)重啟

那么, 如何得知在哪個狀態(tài)重啟呢? 解答這個問題之前, 我們再定義?個名字: 影?狀態(tài) , ?變量 X 表?。 所謂影?狀態(tài), 就是和當(dāng)前狀態(tài)具有相同的前綴。 ?如下?這種情況:

當(dāng)前狀態(tài) j = 4 , 其影?狀態(tài)為 X = 2 , 它們都有相同的前綴 “AB”。 因為狀態(tài) X 和狀態(tài) j 存在相同的前綴, 所以當(dāng)狀態(tài) j 準(zhǔn)備進?狀態(tài)重啟的時候(遇到的字符 c 和 pat[j] 不匹配) , 可以通過 X 的狀態(tài)轉(zhuǎn)移圖來獲得最近的重啟位置。

?如說剛才的情況, 如果狀態(tài) j 遇到?個字符 "A", 應(yīng)該轉(zhuǎn)移到哪?呢??先只有遇到 "C" 才能推進狀態(tài), 遇到 "A" 顯然只能進?狀態(tài)重啟。 狀態(tài)j 會把這個字符委托給狀態(tài) X 處理, 也就是 dp[j]['A'] = dp[X]['A'] :

為什么這樣可以呢? 因為既然 j 這邊已經(jīng)確定字符 "A" ?法推進狀態(tài),只能回退, ?且 KMP 就是要盡可能少的回退, 以免多余的計算。 那么 j就可以去問問和??具有相同前綴的 X , 如果 X 遇? “A” 可以進?「狀態(tài)推進」 , 那就轉(zhuǎn)移過去, 因為這樣回退最少。

當(dāng)然, 如果遇到的字符是 “B”, 狀態(tài) X 也不能進?「狀態(tài)推進」 , 只能回退, j 只要跟著 X 指引的?向回退就?了:

你也許會問, 這個 X 怎么知道遇到字符 “B” 要回退到狀態(tài) 0 呢? 因為 X永遠跟在 j 的?后, 狀態(tài) X 如何轉(zhuǎn)移, 在之前就已經(jīng)算出來了。動態(tài)規(guī)劃算法不就是利?過去的結(jié)果解決現(xiàn)在的問題嗎?

結(jié)合下面的過程理解影子狀態(tài)的更新:

這樣, 我們就細(xì)化?下剛才的框架代碼:

int X # 影?狀態(tài) for 0 <= j < M:for 0 <= c < 256:if c == pat[j]:# 狀態(tài)推進dp[j][c] = j + 1else:# 狀態(tài)重啟# 委托 X 計算重啟位置dp[j][c] = dp[X][c]

五、 代碼實現(xiàn)

如果之前的內(nèi)容你都能理解, 恭喜你, 現(xiàn)在就剩下?個問題: 影?狀態(tài) X是如何得到的呢? 下?先直接看完整代碼吧:

public class KMP {private int[][] dp;private String pat;public KMP(String pat) {this.pat = pat;int M = pat.length();// dp[狀態(tài)][字符] = 下個狀態(tài)dp = new int[M][256];// base casedp[0][pat.charAt(0)] = 1;// 影?狀態(tài) X 初始為 0int X = 0;// 當(dāng)前狀態(tài) j 從 1 開始for (int j = 1; j < M; j++) {for (int c = 0; c < 256; c++) {if (pat.charAt(j) == c)dp[j][c] = j + 1;elsedp[j][c] = dp[X][c];} // 更新影?狀態(tài)X = dp[X][pat.charAt(j)];}} public int search(String txt) {...} }

代碼解釋:

先解釋?下這??代碼:

// base case dp[0][pat.charAt(0)] = 1;

這?代碼是 base case, 只有遇到 pat[0] 這個字符才能使?fàn)顟B(tài)從 0 轉(zhuǎn)移到 1,遇到其它字符的話還是停留在狀態(tài) 0(Java 默認(rèn)初始化數(shù)組全為 0) 。

影?狀態(tài) X 是先初始化為 0, 然后隨著 j 的前進?不斷更新的。 下?看看到底應(yīng)該如何更新影?狀態(tài) X :

int X = 0; for (int j = 1; j < M; j++) {...// 更新影?狀態(tài)// 當(dāng)前是狀態(tài) X, 遇到字符 pat[j],// pat 應(yīng)該轉(zhuǎn)移到哪個狀態(tài)?X = dp[X][pat.charAt(j)]; }

更新 X 其實和 search 函數(shù)中更新狀態(tài) j 的過程是?常相似的:

int j = 0; for (int i = 0; i < N; i++) {// 當(dāng)前是狀態(tài) j, 遇到字符 txt[i],// pat 應(yīng)該轉(zhuǎn)移到哪個狀態(tài)?j = dp[j][txt.charAt(i)];... }

其中的原理?常微妙, 注意代碼中 for 循環(huán)的變量初始值, 可以這樣理解:
后者是在 txt 中匹配 pat , 前者是在 pat 中匹配 pat[1..end] , 狀態(tài)X 總是落后狀態(tài) j ?個狀態(tài), 與 j 具有最?的相同前綴。 所以我把 X?喻為影?狀態(tài), 似乎也有?點貼切。另外, 構(gòu)建 dp 數(shù)組是根據(jù) base case dp[0][…] 向后推演。 這就是我認(rèn)為KMP 算法就是?種動態(tài)規(guī)劃算法的原因。

最后看KMP算法的完整代碼:

public class KMP {private int[][] dp;private String pat; ?public KMP(String pat) {this.pat = pat;int M = pat.length();// dp[狀態(tài)][字符] = 下個狀態(tài)dp = new int[M][256];// base casedp[0][pat.charAt(0)] = 1;// 影子狀態(tài) X 初始為 0int X = 0;// 構(gòu)建狀態(tài)轉(zhuǎn)移圖(稍改的更緊湊了)for (int j = 1; j < M; j++) {for (int c = 0; c < 256; c++)dp[j][c] = dp[X][c];dp[j][pat.charAt(j)] = j + 1;// 更新影子狀態(tài)X = dp[X][pat.charAt(j)];}} ?public int search(String txt) {int M = pat.length();int N = txt.length();// pat 的初始態(tài)為 0int j = 0;for (int i = 0; i < N; i++) {// 計算 pat 的下一個狀態(tài)j = dp[j][txt.charAt(i)];// 到達終止態(tài),返回結(jié)果if (j == M) return i - M + 1;}// 沒到達終止態(tài),匹配失敗return -1;} }

C++代碼

//封裝一個KMP算法用來快速查找字符串匹配 class KMP {public:KMP(const std::string& pat):_pat(pat){int M = _pat.size();//dp[狀態(tài)][字符] = 下一個狀態(tài);把dp初始化為全0dp = std::vector<std::vector<int>>(M,std::vector<int>(256,0));//base case :一開始dp中存的是0,那么規(guī)定最開始為0狀態(tài),遇到第一個字符為pat轉(zhuǎn)換為1狀態(tài)dp[0][_pat[0]] = 1;//博客中說的影子狀態(tài),初始化為0狀態(tài)int X = 0;//開始循環(huán)構(gòu)造狀態(tài)轉(zhuǎn)換圖即dpfor(int j = 1;j < M;j++){for(int c = 0;c < 256;c++){dp[j][c] = dp[X][c];}dp[j][_pat[j]] = j + 1;//更新影子狀態(tài)X = dp[X][_pat[j]];}}int Search(const std::string& txt){int M = _pat.size();int N = txt.size();//pat的初始狀態(tài)為0狀態(tài)int j = 0;//循環(huán)for(int i = 0;i < N;i++){j = dp[j][txt[i]];//如果經(jīng)過狀態(tài)轉(zhuǎn)換圖dp可以到達終態(tài)M,就說明在txt中成功找到匹配的字符串,直接返回其在txt中的下標(biāo)索引if(j == M){return i - M + 1;}}//代碼走到這里代表遍歷完txt還沒有找到匹配的return出去,就直接return -1;return -1;}private://dp存放狀態(tài)轉(zhuǎn)換圖;dp[1]['A'] = 2;代表 狀態(tài)1 如果遇到字符'A',pat 就轉(zhuǎn)換為2狀態(tài)std::vector<std::vector<int>> dp;//用戶輸入pat待匹配的字符串std::string _pat; };

六、 最后總結(jié)

傳統(tǒng)的 KMP 算法是使??個?維數(shù)組 next 記錄前綴信息, ?本?是使??個?維數(shù)組 dp 以狀態(tài)轉(zhuǎn)移的?度解決字符匹配問題, 但是空間復(fù)雜度仍然是 O(256M)=O(M)O(256M) = O(M)O(256M)=O(M)。

在 pat 匹配 txt 的過程中, 只要明確了「當(dāng)前處在哪個狀態(tài)」 和「遇到的字符是什么」 這兩個問題, 就可以確定應(yīng)該轉(zhuǎn)移到哪個狀態(tài)(推進或回退) 。

對于?個模式串 pat , 其總共就有 M 個狀態(tài), 對于 ASCII 字符, 總共不會超過 256 種。 所以我們就構(gòu)造?個數(shù)組 dp[M][256] 來包含所有情況, 并且明確 dp 數(shù)組的含義:

dp[j][c] = next 表?, 當(dāng)前是狀態(tài) j , 遇到了字符 c , 應(yīng)該轉(zhuǎn)移到狀態(tài)next

明確了其含義, 就可以很容易寫出 search 函數(shù)的代碼。

對于如何構(gòu)建這個 dp 數(shù)組, 需要?個輔助狀態(tài) X , 它永遠?當(dāng)前狀態(tài)j 落后?個狀態(tài), 擁有和 j 最?的相同前綴, 我們給它起了個名字叫「影?狀態(tài)」 。

在構(gòu)建當(dāng)前狀態(tài) j 的轉(zhuǎn)移?向時, 只有字符 pat[j] 才能使?fàn)顟B(tài)推進( dp[j][pat[j]] = j+1 ) ; ?對于其他字符只能進?狀態(tài)回退, 應(yīng)該去請教影?狀態(tài) X 應(yīng)該回退到哪?( dp[j][other] = dp[X][other] , 其中other 是除了 pat[j] 之外所有字符)

對于影?狀態(tài) X , 我們把它初始化為 0, 并且隨著 j 的前進進?更新,更新的?式和 search 過程更新 j 的過程?常相似( X = dp[X][pat[j]] )

總結(jié)

以上是生活随笔為你收集整理的动态规划之KMP字符匹配算法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。