hihoCoder1877 Approximate Matching - AC自動機 - 動態規劃(dp)

Approximate Matching

時間限制:1000ms
單點時限:1000ms
內存限制:512MB

描述

String matching, a common problem in DNA sequence analysis and text editing, is to find the occurrences of one certain string (called pattern) in a larger string (called text). In some cases, the pattern is not required to be exactly in the text, and minor differences are acceptable (due to possible typing mistakes). When given a pattern string and a text string, we say pattern P is approximately matched within text S, if there is a substring of S which is at most one letter different from P. Note that the length of this substring and the pattern must be identical. For example, pattern “abb” is approximately matched in text “babc” but not matched in “bbac”.

It is easy to check if a pattern is approximately matched in a text. So your task is to count the number of all text strings of length m in which the given pattern can be approximately matched, and both of the patterns and texts are binary strings in order not to handle big integers.

輸入

The first line of input is a single integer T (1 ≤ T ≤ 666), the number of test cases. Each test case begins with a line of two integers n,m (1 ≤ n,m ≤ 40), denoting the length of pattern string and text string. Then a single line of binary string P follows, which denotes the pattern. Note that there will be at most 15 test cases in which n ≥ 16.

輸出

For each test case, output a single line with one integer, representing the answer.

樣例輸入

5
3 4
110
4 7
1011
2 10
00
7 17
1001110
11 22
11101010001

樣例輸出

12
104
1023
72840
291544

 
 

題目大概意思:

給出一個長度爲 n(1n40)n(1≤n≤40) 的只由 0011 組成的字符串 SS ,對於一個字符串 TT ,若 TT 的存在長度爲 nn 的子串,滿足該子串與 SS 的不同的位的數量不超過 11 ,則稱 SSTT 近似匹配。問所有長度爲 m(1m40)m(1≤m≤40) 的只由 0011 組成的字符串中,與 SS 近似匹配的有多少個。

 
 

分析:

首先容易想到,與 SS 近似匹配,可以看作是與 SS 改變至多 11 位後所形成的 (n+1)(n+1) 條字符串 {Si}\{S_i\} 中的任意一條匹配。而找到所有與 SS 近似匹配的,可以通過計算與 SS 不近似匹配的字符串的個數 xx ,最後再用所有可能的字符串數 2m2^m 減去 xx 即可。於是我們只需要求出所有不與 {Si}\{S_i\} 中任意一條匹配的字符串的數量。
 

考慮樸素的方法,枚舉所有長度爲 mm 的串,並逐一判斷是否與 SS 近似匹配,但是所有可能的字符串的數量高達 2m2^m ,這顯然是行不通的。接下來,我們來考慮窮竭搜索中所進行的重複計算:

如果生成的字符串 TT 的後綴與 SiS_i 匹配,則無論 TT 除去後綴 SiS_i 後的剩餘部分是什麼,這條字符串都是不計入 xx 的,而窮竭搜索的過程中,卻把所有後綴爲 SiS_i 的字符串都遍歷了一遍。
 

可以發現是否與 SiS_i 匹配,只與生成的字符串的後綴有關,也就是說影響 xx 的計數的,只有當前字符串的後綴,因此我們可以把每一種對結果有影響的後綴作爲狀態,也就是把所生成的字符串的後綴與 SiS_i 的所有前綴的所有可能的匹配情況作爲狀態。 {Si}\{S_i\} 中共有 (n+1)(n+1) 條字符串,每條字符串有 (n+1)(n+1) 個不同的前綴(把長度爲 00 時的空串也當作一個前綴),則共有不超過 (n+1)2(n+1)^2 種狀態。有了每種後綴所代表的狀態,如果我們能夠知道在每一種狀態的末尾添加任意一個字符後的下一個匹配狀態,dpdp 方程就容易列出了:

    設 dp[L][State]dp[L][State] 爲生成字符串的長度爲 LL ,後綴狀態爲 StateState 的所有字符串的數量。其中不妨令 State=0State=0 爲與所有模式串 {S_i} 匹配長度均爲 00 時的狀態。

    那麼設當前狀態爲 curcur ,可以由狀態 preipre_i 添加字符 cic_i 後轉移而來,則有:

dp[0][0]=1dp[0][i]=0,i0dp[L][cur]=dp[L1][prei],L0 \begin{aligned} dp&[0][0]=1\\ dp&[0][i]=0,i≠0\\ dp&[L][cur]=\sum_{}^{}{dp[L-1][pre_i]},L>0 \end{aligned}

爲了讓動態規劃算法部分更加高效,我們需要預處理出每一種匹配狀態的編號,以及從某個狀態添加某個字符後的狀態轉移表,然後利用該表完成動態規劃。

在預處理時,我們可以採用樸素的枚舉 SiS_i 的所有可能的前綴然後去重的方法得到每一種狀態的編號,對於 O(n)O(n) 條字符串,共有 O(n2)O(n^2) 種前綴,則去重的時間複雜度爲 O(n3log2n)O(n^3·\log_2{n});然後通過反覆刪除每個前綴的首個字符並查找的方法計算狀態轉移表,由於共有 O(n2)O(n^2) 種狀態,每種狀態都有 O(n)O(n) 種前綴,因此共有 O(n3)O(n^3) 個前綴,比較兩個字符串的時間複雜度爲 O(n)O(n),故對於前綴中的每一個,在使用二叉查找樹的情況下要在 O(nlog2n)O(n·\log_2{n}) 的時間複雜度內判斷這個前綴是否是某種已存在的狀態,故這樣的預處理算法的時間複雜度是 O(n4log2n)O(n^4·\log_2{n}) ,在此題的數據範圍內勉勉強強,當然我們可以使用字符串哈希的算法將字符串的比較的時間複雜度優化爲 O(1)O(1) ,使得預處理的時間複雜度降低爲 O(n3log2n)O(n^3·\log_2{n}) ,但較爲繁瑣。

我們還可以使用 AhoCorasickAho-Corasick 算法,在構建 ACAC 自動機的同時爲每一種狀態賦予編號並得出狀態轉移表。這種算法的複雜度爲 O(Len)O(\sum{Len}) ,此題中由於模式串的長度與數量相同,故構建 ACAC 自動機的時間複雜度爲 O(n2)O(n^2) ,顯著優於樸素的算法。

在動態規劃算法中,共有不超過 O(n3)O(n^3) 種狀態,每個狀態由不超過 O(n)O(n) 個狀態更新而來,故時間複雜度上界爲 O(n4)O(n^4) ,而由於字符串僅由 0,10,1 組成,重複的狀態較多,因此這是一個較爲寬鬆的時間複雜度上界。

 
 

下面貼代碼:

#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;

typedef long long ll;

const int INF = 1 << 24;
const int MAX_N = 42;
const int MAX_STATE = 1650;
const int MAX_W = 2;

struct P
{
	P* fail;
	P* next[MAX_W];
	int cnt;
	P()
	{
		fail = NULL;
		cnt = 0;
		memset(next, 0, sizeof(next));
	}
};
P pool[MAX_STATE];
int pt;

void insert(const char* const str, P* root);
void buildac(P* root);

char ch[MAX_N];

ll dp[MAX_N][MAX_STATE];


int main()
{
	int T, n, m;
	scanf("%d", &T);

	while (T--)
	{
		scanf("%d%d \n%s", &n, &m, ch);

		pt = 0;
		P* root = new(pool + pt++) P();

		for (int i = 0; i < n; ++i)
		{
			ch[i] -= '0' - 1;
		}
		insert(ch, root);
		for (int i = 0; i < n; ++i)
		{
			ch[i] = ch[i] == 1 ? 2 : 1;
			insert(ch, root);
			ch[i] = ch[i] == 1 ? 2 : 1;
		}
		buildac(root);

		dp[0][0] = 1;
		for (int i = 1; i < pt; ++i)
		{
			dp[0][i] = 0;
		}
		for (int t = 0; t < m; ++t)
		{
			for (int i = 0; i < pt; ++i)
			{
				dp[t + 1][i] = 0;
			}

			for (int i = 0; i < pt; ++i)
			{
				for (int j = 0; j < MAX_W; ++j)
				{
					int k = pool[i].next[j] - pool;
					if (pool[k].cnt)
					{
						continue;
					}
					dp[t + 1][k] += dp[t][i];
				}
			}
		}

		ll ans = 0;
		for (int i = 0; i < pt; ++i)
		{
			ans += dp[m][i];
		}
		printf("%lld\n", (1ll << m) - ans);
	}
	return 0;
}

void insert(const char* const str, P* root)
{
	P* p = root;
	int i = -1, index;
	while (str[++i])
	{
		index = str[i] - 1;
		if (p->next[index] == NULL)
		{
			p->next[index] = new(pool + pt++) P();
		}
		p = p->next[index];
	}
	p->cnt = 1;
}

void buildac(P* root)
{
	root->fail = NULL;
	queue<P*> q;
	q.push(root);
	while (!q.empty())
	{
		P* const tmp = q.front();
		P* p = NULL;
		q.pop();

		for (int i = 0; i < MAX_W; i++)
		{
			if (tmp->next[i])
			{
				if (tmp == root)
				{
					tmp->next[i]->fail = root;
				}
				else
				{
					p = tmp->fail;
					while (p)
					{
						if (p->next[i])
						{
							tmp->next[i]->fail = p->next[i];
							break;
						}
						p = p->fail;
					}
					if (!p) tmp->next[i]->fail = root;
					if (p && p->cnt)
					{
						tmp->cnt = 1;
					}
				}

				q.push(tmp->next[i]);
			}
			else
			{
				tmp->next[i] = tmp == root ? root : tmp->fail->next[i];
			}
		}
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章