淺談最長迴文子串求法——字串雜湊

Torrentolf發表於2024-11-22

引入

眾所周知,\(manacher\)演算法可以在\(O(n)\)的時間複雜度內求出一個字串的最長迴文子串長度,但是\(manacher\)演算法對於初學者來說並不是很好理解(至少我是這麼認為的),因此我們考慮有沒有什麼更好理解的方式求出一個字串的最長迴文子串。

關於字串雜湊

字串雜湊是一種非常神奇的東西,我們可以\(O(n)\)地預處理出一字串的雜湊值(一般使用自然溢位法),然後我們發現一個字串的雜湊值相當於更換了其進位制位的字首和形式,因此我們其實在預處理完之後可以直接透過修改進位制位的減法\(O(1)\)求出該字串的一段子串的雜湊值。

與迴文串的關係

我們考慮一個字串是迴文串在雜湊下的充要條件,我們根據迴文串的定義可以知道一個字串為迴文串當且僅當其正序與反序完全相同而對於字串雜湊來說,想要判斷兩串是否相同只需判斷它們的雜湊值是否相等,所以判斷一個字串是否為迴文串只需要判斷其正向雜湊與反向雜湊值是否相等,而一個字串的正向雜湊與反向雜湊都可以\(O(n)\)預處理,然後對於它的每一個子串都可以\(O(1)\)提取出它的正向與反向雜湊,即可以\(O(1)\)判斷其所有子串是否為迴文串了。

進入正題

我們首先考慮迴文串其實有兩種形式——奇數迴文串與偶數迴文串。

奇數迴文串比較好處理,因為每一個奇數迴文串都存在一個迴文中心,但是偶數迴文串則沒有,因此我們可以考慮進行轉化,強制給偶數迴文串增加一個迴文中心,簡單的說就是將一個不存在於字串字符集中的字元插入至偶數迴文串中心,使其有迴文中心,方便判斷,為了方便,我們可以將整個字串的字元間都插入一個字元(一般為'#'),這樣就能將偶數迴文串與奇數迴文串以相同的方式處理了。(其實這裡與\(manacher\)演算法相同)

然後我們考慮直接列舉迴文串的迴文中心,統計每一個字元作為迴文中心的最長迴文串長度,但是這樣暴力擴充複雜度很顯然是\(O(n^2)\)的,因此我們考慮一下如何最佳化。

我們發現,我們需要求的是最長迴文串的長度,因此我們在從一個迴文中心向外擴充時不需要從最裡面開始考慮,直接從已經求出的最長長度開始擴充,因為我們的正反字串雜湊比較可以\(O(1)\)判斷已經處理的任何字串是否是迴文串,因此開始擴充時若判斷失敗可以直接跳過,而回文串的半徑\(r\)最多擴充到字串邊緣,因此我們的複雜度其實是\(O(n)\)的。

至此,我們已經完成了使用字串雜湊\(O(1)\)求字串最長迴文串的所有分析。

模板題:P3805 【模板】manacher - 洛谷 | 電腦科學教育新生態

模板程式碼

#include<iostream>
#include<cstring>
using namespace std ;
namespace IO
{
    const size_t SIZE = 1 << 16 ;
	namespace Read
	{
		#define isdigit(ch) (ch >= '0' && ch <= '9')
		char buf[SIZE] , *_now = buf , *_end = buf ;
		char getchar()
		{
			if(_now == _end)
			{
				_now = _end = buf ;
				_end += fread(buf , 1 , SIZE , stdin) ;
				if(_now == _end)
					return EOF ;
			}
			return *(_now++) ;
		}
		template<typename T>
		void read(T& w)
		{
			w = 0 ;
			short f = 1 ;
			char ch = getchar() ;
			while(!isdigit(ch))
			{
				if(ch == '-')
					f = -1 ;
				ch = getchar() ;
			}
			while(isdigit(ch))
				w = (w << 1) + (w << 3) + (ch ^ 48) , ch = getchar() ;
			w *= f ;
		}
		#define sb(ch) (ch == ' ' || ch == '\n')
		void read(char* s)
		{
			char ch = getchar() ;
			while(sb(ch))
				ch = getchar() ;
			int len = 0 ;
			while(!sb(ch) && ch != EOF)
				s[len++] = ch , ch = getchar() ;
		}
		void read(string& s)
		{
			char ch = getchar() ;
			while(sb(ch))
				ch = getchar() ;
			while(!sb(ch) && ch != EOF)
				s.push_back(ch) , ch = getchar() ;
		}
		#undef sb
		class qistream
		{
			public:
			template<typename T>
			qistream& operator>>(T& a)
			{
				read(a) ;
				return *this ;
			}
            qistream& operator>>(char* s)
            {
                read(s) ;
                return *this ;
            }
		} qcin ;
	}
	namespace Write
	{
		char buf[SIZE] , *p = buf ;
		void flush()
		{
			fwrite(buf , 1 , p - buf , stdout) ;
			p = buf ;
		}
		void putchar(char ch)
		{
			if(p == buf + SIZE)
				flush() ;
			*p = ch ;
			++p ;
		}
		class Flush{public:~Flush(){flush() ;};}_;
		template<typename T>
		void write(T x)
		{
			char st[50] ;
			int len = 0 ;
			if(x < 0)
				putchar('-') , x = -x ;
			do
			{
				st[++len] = x % 10 + '0' ;
				x /= 10 ;
			} while(x) ;
			while(len)
				putchar(st[len--]) ;
		}
		void write(char c)
		{
			putchar(c) ;
		}
		void write(const char* s)
		{
			int siz = strlen(s) ;
			for(int i = 0 ; i < siz ; ++i)
				putchar(s[i]) ;
		}
		void write(char* s)
		{
			int siz = strlen(s) ;
			for(int i = 0 ; i < siz ; ++i)
				putchar(s[i]) ;
		}
		void write(string& s)
		{
			int siz = s.size() ;
			for(int i = 0 ; i < siz ; ++i)
				putchar(s[i]) ;
		}
		class qostream
		{
			public:
			template<typename T>
			qostream& operator<<(T x)
			{
				write(x) ;
				return *this ;
			}
            qostream& operator<<(const char* s)
            {
                write(s) ;
                return *this ;
            }
		} qcout ;
	}
	using Read::qcin ;
	using Write::qcout ;
}
using namespace IO ;
char in[11000005] = {} ;
char str[22000005] = {} ;
int len = 0 ;
#define ull unsigned
ull hashz[22000005] = {} , hashf[22000005] = {} , p[22000005] = {} ;
ull gethashz(int i , int j)
{
	return hashz[j] - hashz[i - 1] * p[j - i + 1] ;
}
ull gethashf(int i , int j)
{
	return hashf[i] - hashf[j + 1] * p[j - i + 1] ;
}
bool check(int i , int j)
{
	return gethashz(i , j) == gethashf(i , j) ;
}
int main()
{
    qcin>>in ;
    len = strlen(in) ;
	for(int i = 0 ; i <= len * 2 ; ++i)
	{
		if(i % 2)
			str[i] = in[i / 2] ;
		else
			str[i] = '#' ;
	}
	len = strlen(str) ;
	hashz[0] = str[0] , hashf[len + 1] = 0 , p[0] = 1 ;
	for(int i = 1 ; i < len ; ++i)
		hashz[i] = hashz[i - 1] * 131 + str[i] , p[i] = p[i - 1] * 131 ;
	for(int i = len - 1 ; i >= 0 ; --i)
		hashf[i] = hashf[i + 1] * 131 + str[i] ;
	int maxr = 0 ;
	for(int i = 0 ; i < len ; ++i)
	{
		while(i - maxr >= 0 && i + maxr < len && check(i - maxr , i + maxr))
			++maxr ;
	}
	qcout<<maxr - 1<<'\n' ;
    return 0 ;
}

寫在最後

當然字串雜湊的常數要大於\(manacher\)的,而且空間上不注意的話容易炸,(雖然我看到網上都是二分的\(O(n \log n)\)的字串雜湊做法),當然,字串雜湊比較好理解,而且更適合於不同問題的解決(題外話)

如果你不會字串雜湊,請看這篇文章

相關文章