Rabin-Karp算法 (拉賓-卡普)

Rabin-karp算法是樸素字符串匹配算法的一個特例。當字母表∑爲d進制數時,即∑={0,1,2,…d-1}。如當d=10時字母表中的每個字符都是一個十進制數。我們在比較兩個長度爲m的子串時,可以把這兩個子串當作整數進行比較,而不用逐個字符比較,從而在某種程度上減少算法時間。

把一個由d進制數字組成的字符串轉換成相應的十進制整數,這是大家曾經都寫過的東西,一個可能的簡單實現如下:

int ConvertToInt(const char* str, int d)
{
	int ans =0;
	int m = strlen(str);
	for(int i=0; i<m; i++)
	{
		ans = ans*d + str[i]-'0';
	}
	return ans;
}

這就是所謂的霍納法則(Horner’s rule)

對一個長度爲m的子串,求其對應的整數值,其複雜度爲O(m),如果不做一些特殊的處理,每次把子串和模式P比較時,先求子串對應的整數t,再與模式P對應的整數值p比較,算法的複雜度並沒有得到改進。如果用t(s)表示當前位移下子串T[s+1..s+m]的值,我們注意到把模式向右滑動一個窗口之後,下一個m位的子串T[s+2..s+m+1]對應的整數值t(s+1)和t(s)相比,只是去掉了最高位數字T[s+1],增加了一個最低位的數字T[s+m+1]。因而t(s+1)和t(s)之間有如下關係式:

t(s+1)=d*(t(s)-T[s+1]*d^(m-1))+T[s+m+1]

例如對於文本十進制數字組成的文本”2359023141526739921”,d=10,m=5,當s=6時,t(s)=”31415”,T[s+1]=T[7]=3,T[s+m+1]=T[12]=2,所以

t(s+1) =10*(31415-3*10^4)+2=14152

因而我們初始時,只用求p=P[1..m],t0=T[1..m],t(s+1)的求值可以在每次循環迭代中去求,避免了多次函數調用的開銷。因而一個可能的實現如下:

void Bad_Rabin_Karp_Matcher(const char* T, const char* P, int d)
{
	int n = strlen(T);
	int m = strlen(P);
	int h = pow(static_cast<double>(d),m-1);//h=d^(m-1)
	int p=0;
	int t=0;
	
	for(int i=0; i<m; i++)
	{
		p = p*d + P[i]-'0';//p=P[0,m-1]
		t = t*d + T[i]-'0';//t=T[0,m-1]
	}
	for(int s=0; s<=n-m; s++)
	{
		if(p==t)
		{
			cout<<"Pattern occurs with shift"<<s<<endl;
		}
		if(s<n-m)
		{
			t = d*(t-h*(T[s]-'0')) + T[s+m]-'0';
		}
	}
}

注:

(1) 在前面講解過程中數組下標從1開始,而在實際編程中數組下標是從0開始的,所以代碼和描述的有點差異,但是原理是一樣的。

(2)注意到我們求p,t,h的過程,不管用的是int還是long long型,當m增大時,肯定會產生溢出,比如說一個32位的int型表示的最大整數也就是20多億。

(3)一般做過一些ACM題目的同學,知道在這種情況我們可以通過對一個數求模來防止溢出,記其爲q。

(4)這裏又出現了一個問題,當p≠t (mod q)時,的確可以推出p≠q;但是當p=t (mod q)時,推不出p=t。即可能出現所謂的僞命中點,即p=t (mod q)但是p≠q。所以當p=t (mod q)時,我們要進行額外檢查,依次比較這m個字符,看其是否真的命中。

(5)通過把q設置爲一個較大的素數,可以有效的減少僞命中點數,因而可以減小額外檢查的開銷。

通過以上分析,改進後一個可能的算法實現如下:

void Rabin_Karp_Matcher(const char* T, const char* P, int d, int q)
{
	int n = strlen(T);
	int m = strlen(P);
	int p = 0;
	int t = 0;
	int h = d;//當m=1時有問題,此處應該爲h=1,下面循環中k初始爲1
	for(int k=2; k<m; k++)
	{
		h = h*d % q;
	}
	for(int i=0; i<m; i++)
	{
		p = (p*d + P[i]-'0') % q;
		t = (t*d + T[i]-'0') % q;
	}
	
	for(int s=0; s<=n-m; s++)
	{
//		cout<<"p="<<p<<" t="<<t<<endl;
		if(p==t)
		{
			if(strncmp(T+s,P,m)==0)
			{
				cout<<"Pattern occurs with shift"<<s<<endl;
			}
		}
		if(s<n-m)
		{	
			t = (d*(t-h*(T[s]-'0'))+T[s+m]-'0') % q;//此處也有問題,t可能小於0
		}
	}
}

但是運行結果和和預期不符,比如說你輸入P=”111111”,T=”1”,結果只輸出了一個位移值0,輸入T=”1234”,P=”34”居然沒有有效的位移輸出。

分析代碼,唯一可能的原因就是求餘過程出現到了問題,因此我們在循環中比較p和t的值之前先輸出它們的值,便於分析。果不其然,拿P=”111111”,T=”1”的輸出作爲示例,除了第一個位移p和t相等,後面的位移t居然是負值。也就是說對q求餘的結果可能出現[-q+1,-1]範圍的負值,爲了計算的正確性,我們要保證迭代的過程中t都爲正值。當t<0時,只需加上q即可。當然初始計算p和t的值時,p和t是不可能爲負值的。修改完代碼後,上面舉的第二個輸入用例運行正確,但是第一個仍然有問題,分析發現,原來當模式P只含一個字符時m=1,h=d^(m-1)=d^0=1,而前面我在求h時,只想到h是m-1個的相乘,把h初始化爲了d,疏忽了m可能取1,所以出現了此種情況。到此分析結束,完整正確的函數及測試代碼如下:

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

void Rabin_Karp_Matcher(const char* T, const char* P, int d, int q)
{
	int n = strlen(T);
	int m = strlen(P);
	int p = 0;
	int t = 0;
	int h = 1;
	for(int k=1; k<m; k++) //預處理
	{
		h = h*d % q;
	}
	for(int i=0; i<m; i++)
	{
		p = (p*d + P[i]-'0') % q;
		t = (t*d + T[i]-'0') % q;
	}

	for(int s=0; s<=n-m; s++)//匹配
	{
		cout<<"p="<<p<<" t="<<t<<endl;
		if(p==t)
		{
			if(strncmp(T+s,P,m)==0)
			{
				cout<<"Pattern occurs with shift"<<s<<endl;
			}
		}
		if(s<n-m)
		{	
			t = (d*(t-h*(T[s]-'0'))+T[s+m]-'0') % q;
			if(t<0)
			{
				t = t+q;
			}
		}
	}
}


int main(int argc, char *argv[])
{
	const int Max_Length = 1000;
	const int d = 10;
	const int q = 13;
	char T[Max_Length];
	char P[Max_Length];
	while(gets(T))
	{
		gets(P);
		Rabin_Karp_Matcher(T,P,d,q);
		cout<<"next case:"<<endl;
	}
    system("PAUSE");
    return EXIT_SUCCESS;
}

算法分析:預處理時間Θ(m),即求h,p,t的時間爲,匹配時間在最壞情況下爲Θ((n-m-1)m),因爲可能出現每次都是可能命中點的情況。如T=a^n,P=a^m,此種情況下驗證時間爲Θ((n-m-1)m)。當然實際中,可能的命中點一般很少。假設有c個,則算法的期望匹配時間爲O(n-m+1 +cm)=O(m+n),當m<<n時,期望匹配時間爲O(n).



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章