WHUT第十二週訓練整理

WHUT第十二週訓練整理

寫在前面的話:我的能力也有限,錯誤是在所難免的!因此如發現錯誤還請指出一同學習!

索引

(難度由題目自身難度與本週做題情況進行分類,僅供新生參考!)

零、基礎知識過關

一、easy:01、06、07、08、09、10、11、16

二、medium:02、03、04、05、12、14、15、17、18

三、hard:13、19、20、21、22、23

四、神仙題:24

零、基礎知識過關

終於還是來字符串了,本週的題目基本上都是 KMP、Trie 字典樹以及 AC自動機的題目。

KMPO(n)O(n) 線性時間內實現原串與模式串之間的匹配。該算法的核心在於理解 nextnext 數組的含義以及作用,要想不只停留在做板子題就必須要把 nextnext​ 數組喫透了,忘記了一次就再重新學習一次,直到完全掌握。

我第一次瞭解 KMP 算法是在 B 站的 三哥講解KMP算法中文字幕版 學習到的,我還記得我至少看了 3 遍纔有一點理解了,所以大家學習的時候如果一次沒懂那肯定是很正常的,多看幾遍就慢慢有感覺了!而且以後肯定是會忘記的,忘了就複習!

Trie 樹:也稱之爲字典樹,因爲這顆樹上每個節點都代表着一個字符。使用字典樹最常見的作用就是對字符串進行快速的插入與查詢。

推薦網站:看動畫輕鬆理解「Trie樹」

AC 自動機:別想多了,這個東西不是用來自動幫你 AC 題目的。如果簡單得來說,KMP 是用來在原串中查詢是否出現過某個模式串,那麼 AC自動機就是用來在原串中查詢出現過多少個不同的模式串。該算法也是基於 Trie字典樹進行的,在字典樹上構建一個類似 nextnext 數組的 failfail 指針數組來實現快速地查詢。

推薦網站:(無圖)AC自動機總結 (有圖)AC自動機 算法詳解(圖解)及模板

一、easy

1001:親和串(KMP)

題意:給兩個字符串 S1S1S2S2,問是否能通過 S1S1 循環移位使得 S2S2 包含在 S1S1​ 中。

範圍:S1S21e5|S1|、|S2| \le 1e5

分析:問 S1S1 循環移位後是否能夠得到 S2S2,相當於是把兩個 S1S1 拼接起來的字符串中是否包含 S2S2。考慮到數據範圍,暴力不可取,於是上 KMPKMP,唯一需要注意的就是 S2S2 的長度不能超過 S1S1 的長度。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 2e5 + 10;

int Next[MAXN];

void getNext(string T, int len)
{
    int i = 0, j = -1;
    Next[0] = -1;
    while (i < len)
    {
        if (j == -1 || T[i] == T[j])
        {
            Next[++i] = ++j;
        }
        else
        {
            j = Next[j]; //  若字符不相同,則j值回溯
        }
    }
    return;
}

int indexKMP(string S, string T, int pos, int lenS, int lenT)
{
    int i = pos;
    int j = 0;
    getNext(T, lenT);            //  對串T作分析,得到next數組
    while (i < lenS && j < lenT) //  若i小於S的長度且j小於T的長度時循環繼續
    {
        if (j == -1 || S[i] == T[j]) //  兩字母相等則繼續,與樸素算法相比增加了 j = -1 判斷
        {
            i++;
            j++;
        }
        else //  指針後退重新開始匹配
        {
            j = Next[j]; //  j退回合適的位置,i值不變
        }
    }
    if (j >= lenT)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

int main()
{
    string s1, s2;
    while (cin >> s1 >> s2)
    {
        // 肯定不滿足要求,退出
        if (s1.length() < s2.length())
        {
            cout << "no" << endl;
            continue;
        }
        // 否則的話進行拼接KMP檢查是否能夠得到
        s1 = s1 + s1;
        if (indexKMP(s1, s2, 0, s1.length(), s2.length()))
        {
            cout << "yes" << endl;
        }
        else
        {
            cout << "no" << endl;
        }
    }
    return 0;
}

1006:Simpsons’ Hidden Talents(KMP)

題意:給兩個字符串 S1S1S2S2,求兩個字符串的最長公共前後綴長度。

範圍:S1S250000|S1|、|S2| \le 50000

分析:KMP 裸題,把兩個字符串進行拼接求 NextNext 數組即可。需要注意的是求出來的長度不能超過單個字符串的長度。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 10;

int Next[MAXN];

void getNext(string T, int len)
{
    int i = 0, j = -1;
    Next[0] = -1;
    while (i < len)
    {
        if (j == -1 || T[i] == T[j])
        {
            Next[++i] = ++j;
        }
        else
        {
            j = Next[j]; //  若字符不相同,則j值回溯
        }
    }
    return;
}

int main()
{
    string str1, str2;
    while (cin >> str1 >> str2)
    {
        string s = str1 + str2;
        getNext(s, s.length());
        int ans = Next[s.length()];
        // 0則直接輸出0,不用輸出字符串
        if (ans == 0)
        {
            cout << 0 << endl;
        }
        else
        {
            // 注意不能超過單個字符串的長度
            if (ans > str1.length())
                ans = str1.length();
            if (ans > str2.length())
                ans = str2.length();
            cout << str1.substr(0, ans) << " " << ans << endl;
        }
    }
    return 0;
}

1007:統計難題(Trie樹)

第三週字符串場 1005,詳見博客

1008:Immediate Decodability(Trie樹)

題意:給一些字符串,問是否存在某個字符串是另外一個字符串的前綴。

範圍:未明確指出

分析:也是 Trie 樹的裸題了,在插入的時候判斷是否經過其他的字符串即可。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100 + 10;

int trie[MAXN][2]; //數組形式定義字典樹,值存儲的是下一個字符的位置
int last[MAXN];
int pos = 1;
char str[MAXN];
int flag;

void Insert(char word[]) //在字典樹中插入某個單詞
{
	int i;
	int c = 0;
	for (i = 0; word[i]; i++)
	{
		int n = word[i] - '0';
		if (trie[c][n] == 0) //如果對應字符還沒有值
			trie[c][n] = pos++;
		else if (i == strlen(word) - 1)  // 如果對應字符有值,並且該串已經處理完,說明跟之間的字符串出現重疊
			flag = 1;  
		c = trie[c][n];
		if (last[c])  // 如果遇到了其他串的終結符,也說明出現了重疊
			flag = 1;
	}
	last[c] = 1;
}

int main()
{
	int kase = 1;
	while (~scanf("%s", str))
	{
        // 遇到9則判斷答案
		if (strcmp(str, "9") == 0)
		{
			cout << "Set " << kase++;
			if (flag)
			{
				cout << " is not immediately decodable" << endl;
			}
			else
			{
				cout << " is immediately decodable" << endl;
			}
            // 注意清空
			memset(trie, 0, sizeof(trie));
			memset(last, 0, sizeof(last));
			flag = 0;
			pos = 1;
			continue;
		}
		Insert(str);
	}
	return 0;
}

1009:Phone List(Trie樹)

題意:給 NN​ 個不同的數字串 S[i]S[i]​,問是否存在某個數字串是另外一個數字串的前綴。

範圍:1N10000 , S[i]<=101 \le N \le 10000~,~|S[i]| <= 10

分析:跟 1008 幾乎是一模一樣的問題,只需要稍微修改一點地方,比如輸入、數組大小等。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 10;

int trie[MAXN][10]; //數組形式定義字典樹,值存儲的是下一個字符的位置
int last[MAXN];  // 用來標記終結符
int pos = 1;
char str[MAXN];
int flag;

void Insert(char word[]) //在字典樹中插入某個單詞
{
    int i;
    int c = 0;
    for (i = 0; word[i]; i++)
    {
        int n = word[i] - '0';
        if (trie[c][n] == 0) //如果對應字符還沒有值
            trie[c][n] = pos++;
        else if (i == strlen(word) - 1)
            flag = 1;  // 同上題
        c = trie[c][n];
        if (last[c])
            flag = 1;
    }
    last[c] = 1;
}

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        // 注意清空
        memset(trie, 0, sizeof(trie));
        memset(last, 0, sizeof(last));
        pos = 1;
        flag = 0;
        int n;
        cin >> n;
        for (int i = 0; i < n; i++)
        {
            cin >> str;
            Insert(str);
        }
        if (flag)
        {
            cout << "NO" << endl;
        }
        else
        {
            cout << "YES" << endl;
        }
    }
    return 0;
}

1010:單詞數(set)

題意:給一篇文章,統計其中不同單詞的數量。

範圍:未明確指出

分析:直接上集合 set,最後輸出集合的大小即可。

Code

#include <bits/stdc++.h>
using namespace std;

set<string> Set;  // set容器自動去重

int main()
{
    string str;
    while (getline(cin, str) && str != "#")
    {
        Set.clear();
        stringstream ss(str);  // 使用stringstream可以起到分割字符串的作用
        string s;
        while (ss >> s)
        {
            Set.insert(s);  // 只保留不同的字符串
        }
        cout << Set.size() << endl;
    }
    return 0;
}

1011:What Are You Talking About(map)

第三週字符串場 1016:詳見博客

1016:Keywords Search(AC自動機)

題意:給 NN​ 個模式串 S[i]S[i]​,再給出一個查詢串 StrStr​,問 StrStr​ 中出現了多少個模式串。

範圍:N10000 , S[i]50 , Str1000000N \le 10000~,~|S[i]| \le 50~,~|Str| \le 1000000​,字符串只包含小寫字母

分析:AC自動機板子題。

Code

#include <bits/stdc++.h>
using namespace std;

struct Trie
{
	int next[500010][26], fail[500010], end[500010];
	int root, L;
	int newnode()
	{
		for (int i = 0; i < 26; i++)
		{
			next[L][i] = -1;
		}
		end[L++] = 0;
		return L - 1;
	}

	void init()
	{
		L = 0;
		root = newnode();
	}

    // 往樹中插入字符串
	void insert(char buf[])
	{
		int len = (int)strlen(buf);
		int now = root;
		for (int i = 0; i < len; i++)
		{
			if (next[now][buf[i] - 'a'] == -1)
			{
				next[now][buf[i] - 'a'] = newnode();
			}
			now = next[now][buf[i] - 'a'];
		}
		end[now]++;
	}

    // 構造fail指針
	void build()
	{
		queue<int> Q;
		fail[root] = root;
		for (int i = 0; i < 26; i++)
		{
			if (next[root][i] == -1)
			{
				next[root][i] = root;
			}
			else
			{
				fail[next[root][i]] = root;
				Q.push(next[root][i]);
			}
		}
		while (!Q.empty())
		{
			int now = Q.front();
			Q.pop();
			for (int i = 0; i < 26; i++)
			{
				if (next[now][i] == -1)
				{
					next[now][i] = next[fail[now]][i];
				}
				else
				{
					fail[next[now][i]] = next[fail[now]][i];
					Q.push(next[now][i]);
				}
			}
		}
	}

    // 查詢字符串是否存在
	int query(char buf[])
	{
		int len = (int)strlen(buf);
		int now = root;
		int res = 0;
		for (int i = 0; i < len; i++)
		{
			now = next[now][buf[i] - 'a'];
			int temp = now;
			while (temp != root)
			{
				res += end[temp];
				end[temp] = 0;
				temp = fail[temp];
			}
		}
		return res;
	}
};

char buf[1000010];
Trie ac;

int main()
{
	int T;
	int n;
	scanf("%d", &T);
	while (T--)
	{
		scanf("%d", &n);
		ac.init();
		for (int i = 0; i < n; i++)
		{
			scanf("%s", buf);
			ac.insert(buf);
		}
		ac.build();
		scanf("%s", buf);
		printf("%d\n", ac.query(buf));
	}
	return 0;
}

二、medium

1002:Count the string(Next數組)

題意:給一個長度爲 NN 且只包含小寫字母的字符串 SS,問其所有前綴在串中出現次數的總和。

範圍:1N2e51 \le N \le 2e5

分析:根據數據範圍可知暴力不可取,因爲涉及到字符串的匹配問題,因此想到 KMPKMP 以及 NextNext 數組。Next[i]Next[i] 表示前 ii 個字符所組成的字符串的最大前後綴匹配的長度。考慮字符串 abcabcabcabc,那麼 NextNext 數組爲 [-1, 0, 0, 0, 1, 2, 3],next[6]=3next[6] = 3,說明 abcabcabcabc 前後匹配了 33 個字符 abcabc,此時對答案貢獻了 11aa11abab11abcabc。可以發現通過 NextNext 數組我們就可以快速統計所有前綴對答案的貢獻,需要注意統計的時候要統計非遞增點,比如上面的例子中計算了 Next[6]Next[6],則不需要統計 Next[5]Next[5],因爲答案已經包含在 Next[6]Next[6] 中了。另外每個前綴都出現了一次,所以答案需要加上 NN

詳見代碼。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 2e5 + 10;
const int MOD = 10007;

int n;

int Next[MAXN];

void getNext(string T, int len)
{
    int i = 0, j = -1;
    Next[0] = -1;
    while (i < len)
    {
        if (j == -1 || T[i] == T[j])
        {
            Next[++i] = ++j;
        }
        else
        {
            j = Next[j]; //  若字符不相同,則j值回溯
        }
    }
    return;
}

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        cin >> n;
        string str;
        cin >> str;
        getNext(str, n);
        int ans = n % MOD + Next[n] % MOD;
        for (int i = 1; i < n; i++)
        {
            // 到“峯點”才統計答案
            if (Next[i] + 1 != Next[i + 1])
            {
                ans = ans % MOD + Next[i] % MOD;
            }
        }
        cout << ans % MOD << endl;
    }
    return 0;
}

1003:Cyclic Nacklace(Next數組)

題意:給一個只包含小寫字母的字符串 SS,問至少需要往左邊或者右邊加上多少個字符才能讓該字符串成爲一個子字符串 subsub 週期循環 kk​ 次形成的字符串。

範圍:3S1e53 \le |S| \le 1e5

分析:題目說可以往左邊或者右邊進行加字符,但是兩邊實際上是等價的,因此我們只考慮往字符串右側加字符。我們需要考慮該字符串中重複的字符串連續出現了多少次,以及還需要多少個字符來滿足要求。使用 NextNext 失配數組我們可以知道前面有多少個與當前字符串前綴相同的字符串,考慮最後一個字符的 NextNext 值:

Next[len1]=0Next[len-1] = 0​,表示該字符串沒有公共的前後綴,那麼就是最壞的情況,我們需要重新加上該整個字符串來滿足要求。

Next[len1]=kNext[len-1] = k,此時有長度爲 kk 的公共前後綴,如圖我們只需要添加 len2klen-2k 長度的字符即可。

在這裏插入圖片描述

Notice:有毒吧,用cin、cout又有問題,scanf、printf就沒問題

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 10;

char str[MAXN];
int Next[MAXN];

void getNext(string T, int len)
{
    int i = 0, j = -1;
    Next[0] = -1;
    while (i < len)
    {
        if (j == -1 || T[i] == T[j])
        {
            Next[++i] = ++j;
        }
        else
        {
            j = Next[j]; //  若字符不相同,則j值回溯
        }
    }
    return;
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T--)
    {
        memset(Next, 0, sizeof(Next));
        scanf("%s", str);
        int n = strlen(str);
        getNext(str, n);
        int len = n - Next[n];
        if (Next[n] == 0)
        {
            printf("%d\n", n);
        }
        else if (n % len == 0)
        {
            printf("0\n");
        }
        else
        {
            printf("%d\n", len - n % len);
        }
    }
    return 0;
}

1004:Period(Next數組)

題意:給一個長度爲 NN 的字符串 SS,判斷該字符串的每個前綴是否是週期字符串,即由一個子字符串循環 k>1k > 1 次形成。

範圍:2S1e62 \le S \le 1e6

分析:根據上一題的結論我們知道 iNext[i]i-Next[i] 表示當前以 S[i]S[i] 結尾的字符串的循環節長度 LL,若 i%L==0i\%L == 0 則說明滿足題意。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e6 + 10;

int Next[MAXN];

void getNext(string T, int len)
{
    int i = 0, j = -1;
    Next[0] = -1;
    while (i < len)
    {
        if (j == -1 || T[i] == T[j])
        {
            Next[++i] = ++j;
        }
        else
        {
            j = Next[j]; //  若字符不相同,則j值回溯
        }
    }
    return;
}

int main()
{
    int n;
    int kase = 1;
    while (cin >> n, n)
    {
        string str;
        cin >> str;
        getNext(str, n);
        cout << "Test case #" << kase++ << endl;
        for (int i = 2; i <= n; i++)
        {
            // i-next[i]現在也算是一種套路了,求當前串的最小循環串
            int len = i - Next[i];
            if (Next[i] > 0 && i % len == 0)
            {
                cout << i << " " << i / len << endl;
            }
        }
        cout << endl;
    }
    return 0;
}

1005:剪花布條(不可重複kmp計數)

題意:給一個字符串 SS 以及一個模式串 PP,問 SS 中最多能夠分出多少個 PP 來。

範圍:SP1000|S|、|P| \le 1000

分析:板子題,稍微修改一點。在 KMPKMP 匹配的過程中當匹配的長度到達模式串的長度時,答案++,不進行 NextNext​ 回溯,而是重新開始匹配。

詳見代碼。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000 + 10;

int Next[MAXN];

void getNext(string T, int len)
{
    int i = 0, j = -1;
    Next[0] = -1;
    while (i < len)
    {
        if (j == -1 || T[i] == T[j])
        {
            Next[++i] = ++j;
        }
        else
        {
            j = Next[j]; //  若字符不相同,則j值回溯
        }
    }
    return;
}

int KMP_Count(string p, int m, string str, int n)
{
    //x是模式串,y是主串
    int i, j;
    int ans = 0;
    getNext(str, n);
    i = j = 0;
    while (i < n)
    {
        while (-1 != j && str[i] != p[j])
        {
            j = Next[j];
        }
        i++, j++;
        if (j >= m)
        {
            // j要重新開始計數,i多走了一步,退回來
            ans++;
            j = -1;
            i--;
        }
    }
    return ans;
}

int main()
{
    string s1, s2;
    while (cin >> s1 && s1 != "#")
    {
        cin >> s2;
        cout << KMP_Count(s2, s2.length(), s1, s1.length()) << endl;
    }
    return 0;
}

1012:(拆分字符串+trie樹)

題意:給一些字符串,問其中帽子字符串的數量,帽子字符串定義爲由其他給出的兩個字符串組成的字符串。

範圍:字符串數量不超過 5000050000​

分析:這道題目數據範圍沒有給清楚,但是簡單的做法就是可以通過的。先把所有的字符串保存到 trie 樹中,再遍歷一次所有字符串,將每個字符串每個位置斷開在樹中查找兩側的字符串是否都存在,是則輸出。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 10;

int trie[MAXN][26]; //數組形式定義字典樹,值存儲的是下一個字符的位置
int pos = 1;
int last[MAXN];
string str[MAXN];
int flag;

void Insert(string word) //在字典樹中插入某個單詞
{
    int i;
    int c = 0;
    int len = word.length();
    for (i = 0; i < len; i++)
    {
        int n = word[i] - 'a';
        if (trie[c][n] == 0) //如果對應字符還沒有值
            trie[c][n] = pos++;
        c = trie[c][n];
    }
    last[c] = 1;
}

int Find(string word)
{
    int i;
    int c = 0;
    int len = word.length();
    for (i = 0; i < len; i++)
    {
        int n = word[i] - 'a';
        if (trie[c][n] == 0)
            return 0;
        c = trie[c][n];
    }
    return last[c];
}

int main()
{
    string s;
    int n = 0;
    while (cin >> s)
    {
        str[n++] = s;
        Insert(s);
    }
    for (int i = 0; i < n; i++)
    {
        s = str[i];
        int len = s.length();
        // 暴力拆分檢查兩側字符串是否都存在
        for (int j = 1; j < len; j++)
        {
            string s1 = s.substr(0, j), s2 = s.substr(j);
            if (Find(s1) && Find(s2))
            {
                cout << s << endl;
                break;  // 別忘了退出!
            }
        }
    }
    return 0;
}

1014:Intelligent IME(Trie樹)

題意:在九宮格輸入法中,給出用戶的 NN 個數字組合串,再給出 MM​ 個小寫字母串,問這些字母串能夠通過九宮格中用戶的某個數字組合打出來。

範圍:1N,M50001 \le N, M \le 5000,字符串長度不超過 66

分析:題意也比較清晰,需要進行字符串的快速查詢,上 Trie。根據輸入的數字序列進行建樹,然後再用符號串在樹上匹配,匹配成功就計數。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 10;

int trie[MAXN][10]; //數組形式定義字典樹,值存儲的是下一個字符的位置
int pos = 1;
int last[MAXN], num[MAXN];
string alph[8] = {"abc", "def", "ghi", "jkl", "nmo", "pqrs", "tuv", "wxyz"};

void Insert(string word, int id) //在字典樹中插入某個單詞
{
    int i;
    int c = 0;
    int len = word.length();
    for (i = 0; i < len; i++)
    {
        int n = word[i] - '0';
        if (trie[c][n] == 0) //如果對應字符還沒有值
            trie[c][n] = pos++;
        c = trie[c][n];
    }
    last[c] = id;  // 以編號標記終結符
}

// 查找該字符所對應的數字
int id(char ch)
{
    for (int i = 0; i < 8; i++)
    {
        if (alph[i].find_first_of(ch) != string::npos)
            return i + 2;
    }
    return 0;
}

int Find(string word)
{
    int i;
    int c = 0;
    int len = word.length();
    for (i = 0; i < len; i++)
    {
        int n = id(word[i]);
        if (trie[c][n] == 0)
            return 0;  // 不存在則返回0
        c = trie[c][n];
    }
    return last[c];
}

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        memset(trie, 0, sizeof(trie));
        memset(last, 0, sizeof(last));
        memset(num, 0, sizeof(num));
        pos = 1;
        int n, m;
        cin >> n >> m;
        for (int i = 1; i <= n; i++)
        {
            string s;
            cin >> s;
            Insert(s, i);
        }
        for (int i = 0; i < m; i++)
        {
            string s;
            cin >> s;
            int ans = Find(s);
            if (ans)
                num[ans]++;
        }
        for (int i = 1; i <= n; i++)
        {
            cout << num[i] << endl;
        }
    }
    return 0;
}

1015:Flying to the Mars(思維+Map)

第四周 1008:詳見博客

1017:病毒侵襲(AC自動機)

題意:給 NN 個模式串 S[i]S[i],再給 MM 個查詢串 Str[i]Str[i],問每個查詢串中包含了多少個模式串,輸出模式串的編號。最後輸出匹配到模式串的查詢串數量。

範圍:1N500 , 1M1000 , 20S[i]200 , 7000Str[i]100001 \le N \le 500~,~1 \le M \le 1000~,~20 \le |S[i]| \le 200~,~7000 \le |Str[i]| \le 10000,字符串字符都是 ASCIIASCII​ 碼可見字符

分析:顯明是 AC 自動機,我們需要做的是在 Trie 樹上每個節點加上標記 endend,如果該結點是某個模式串的最後一個字符,那麼 endend 就等於該模式串的編號。這樣用查詢串上樹上匹配的時候某個節點匹配成功且 endend00,說明匹配成功了一個模式串,記錄下編號,最終的答案數量增加。

Notice:需要注意本題中的字符不都是小寫字母,可以把之間 2626 的地方都改成 130130

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 10;

struct Trie
{
    // 題目說了是ASCII可見字符,數組要開大
    int next[MAXN][130], fail[MAXN], end[MAXN];
    set<int> Set;
    int root, L;
    int newnode()
    {
        for (int i = 0; i < 130; i++)
        {
            next[L][i] = -1;
        }
        end[L++] = 0;
        return L - 1;
    }

    void init()
    {
        L = 0;
        root = newnode();
    }

    void insert(char buf[], int id)
    {
        int len = (int)strlen(buf);
        int now = root;
        for (int i = 0; i < len; i++)
        {
            // 數組開到130那buf[i]就不需要-'a'了
            if (next[now][buf[i]] == -1)
            {
                next[now][buf[i]] = newnode();
            }
            now = next[now][buf[i]];
        }
        end[now] = id;
    }

    void build()
    {
        queue<int> Q;
        fail[root] = root;
        for (int i = 0; i < 130; i++)
        {
            if (next[root][i] == -1)
            {
                next[root][i] = root;
            }
            else
            {
                fail[next[root][i]] = root;
                Q.push(next[root][i]);
            }
        }
        while (!Q.empty())
        {
            int now = Q.front();
            Q.pop();
            for (int i = 0; i < 130; i++)
            {
                if (next[now][i] == -1)
                {
                    next[now][i] = next[fail[now]][i];
                }
                else
                {
                    fail[next[now][i]] = next[fail[now]][i];
                    Q.push(next[now][i]);
                }
            }
        }
    }

    void query(char buf[])
    {
        int len = (int)strlen(buf);
        int now = root;
        for (int i = 0; i < len; i++)
        {
            now = next[now][buf[i]];
            int temp = now;
            while (temp != root)
            {
                // 加入set集合
                if (end[temp])
                {
                    Set.insert(end[temp]);
                }
                temp = fail[temp];
            }
        }
    }
};

char buf[1000010];
Trie ac;

int main()
{
    int n, m;
    scanf("%d", &n);
    ac.init();
    for (int i = 0; i < n; i++)
    {
        scanf("%s", buf);
        ac.insert(buf, i + 1);
    }
    ac.build();
    scanf("%d", &m);
    int cnt = 0;
    for (int i = 0; i < m; i++)
    {
        scanf("%s", buf);
        ac.Set.clear();  // 清空再使用
        ac.query(buf);
        // 如果匹配成功則輸出
        if (ac.Set.size())
        {
            cout << "web " << i + 1 << ":";
            // set中有序
            for (auto x : ac.Set)
            {
                cout << " " << x;
            }
            cout << endl;
            cnt++;
        }
    }
    cout << "total: " << cnt << endl;
    return 0;
}

1018:病毒侵襲持續中(AC自動機)

題意:給 NN 個模式串 S[i]S[i],再給出一個查詢串 StrStr​,問查詢串中出現了哪些模式串,出現了多少次。

範圍:1N1000 , 1S[i]50 , Str2e61 \le N \le 1000~,~1\le S[i] \le 50~,~|Str| \le 2e6

分析:在上題的代碼上稍微修改即可,增加數組 numnum 保存某個編號的模式串出現次數,當匹配成功時則對應 num++num++​

Code

// mid
// ac自動機
// 字符串計數
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 10;

int n, m;

char buf[2000010], str[1010][100];

struct Trie
{
    int next[MAXN][130], fail[MAXN], end[MAXN];
    int root, L;
    int newnode()
    {
        for (int i = 0; i < 130; i++)
        {
            next[L][i] = -1;
        }
        end[L++] = 0;
        return L - 1;
    }

    void init()
    {
        L = 0;
        root = newnode();
    }

    void insert(char buf[], int id)
    {
        int len = (int)strlen(buf);
        int now = root;
        for (int i = 0; i < len; i++)
        {
            if (next[now][buf[i]] == -1)
            {
                next[now][buf[i]] = newnode();
            }
            now = next[now][buf[i]];
        }
        end[now] = id;
    }

    void build()
    {
        queue<int> Q;
        fail[root] = root;
        for (int i = 0; i < 130; i++)
        {
            if (next[root][i] == -1)
            {
                next[root][i] = root;
            }
            else
            {
                fail[next[root][i]] = root;
                Q.push(next[root][i]);
            }
        }
        while (!Q.empty())
        {
            int now = Q.front();
            Q.pop();
            for (int i = 0; i < 130; i++)
            {
                if (next[now][i] == -1)
                {
                    next[now][i] = next[fail[now]][i];
                }
                else
                {
                    fail[next[now][i]] = next[fail[now]][i];
                    Q.push(next[now][i]);
                }
            }
        }
    }

    void query(char buf[])
    {
        int num[1010];
        memset(num, 0, sizeof(num));
        int len = (int)strlen(buf);
        int now = root;
        for (int i = 0; i < len; i++)
        {
            now = next[now][buf[i]];
            int temp = now;
            while (temp != root)
            {
                if (end[temp])
                {
                    // 對應編號數量增加
                    num[end[temp]]++;
                }
                temp = fail[temp];
            }
        }
        for (int i = 1; i <= n; i++)
        {
            if (num[i])
            {
                printf("%s: %d\n", str[i], num[i]);
            }
        }
    }
};

Trie ac;

int main()
{
    while (~scanf("%d", &n))
    {
        ac.init();
        for (int i = 1; i <= n; i++)
        {
            scanf("%s", str[i]);
            ac.insert(str[i], i);
        }
        ac.build();
        scanf("%s", buf);
        ac.query(buf);
    }
    return 0;
}

三、hard

1013:T9(Trie樹+dfs)

題意:模擬九宮格輸入法,給出 ww 個該用戶輸入單詞的頻率 p[i]p[i]​,再給出該用戶輸入的數字串,對於該數字串,輸出用戶每按下一個數字當前最可能要輸出的字符串。

範圍:0w1000 , 1p[i]100 , 0 \le w \le 1000~,~1 \le p[i] \le 100~,~字符串長度不超過 100100​

分析:首先題目涉及到對一些模式串的快速查詢,所以需要在字典樹 Trie 上進行,其次我們需要對每個用戶的輸入數字串進行逐位分析,分析出每輸入一個數字時最可能的字符串,可能性是由 p[i]p[i]​ 決定的,因此需要在樹上加上權值,每次查詢的時候在樹上進行 dfs,記錄下每一層的最優解,即輸入每一位數字時當前最可能的解。

細節很多,詳見代碼。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e5 + 10;

int trie[MAXN][26]; //數組形式定義字典樹,值存儲的是下一個字符的位置
int num[MAXN];  // num表示各個節點的總權值
int pos = 1;
string str, ans[MAXN];  // ans保存每一步的答案
map<char, string> mp;  // 數字到字母的映射

void Insert(string word, int w) //在字典樹中插入某個單詞
{
    int i;
    int c = 0;
    int len = word.length();
    for (i = 0; i < len; i++)
    {
        int n = word[i] - 'a';
        if (trie[c][n] == 0) //如果對應字符還沒有值
            trie[c][n] = pos++;
        c = trie[c][n];
        num[c] += w;  // 累計權值
    }
}

int Find(string word)
{
    int i;
    int c = 0;
    int len = word.length();
    for (i = 0; i < len; i++)
    {
        int n = word[i] - 'a';
        if (trie[c][n] == 0)
            return 0;
        c = trie[c][n];
    }
    return num[c];  // 返回權值
}

// 已經構成的串保存在s中
void dfs(string s)
{
    int len1 = s.length(), len2 = str.length();
    if (len1 > len2)  // 大於所求串,退出
        return;
    int res1 = Find(s), res2 = Find(ans[len1]);
    if (!res1 && len1)  // 確實沒找到,退出
        return;
    if (res1 > res2)
    {
        ans[len1] = s;  // 保存每一步的最優解
    }
    string temp = s;
    // 嘗試該數字對應的每個字母
    for (int i = 0; i < mp[str[len1]].length(); i++)
    {
        temp += mp[str[len1]][i];
        dfs(temp);
        temp = s;  // 注意回溯
    }
}

int main()
{
    mp['2'] = "abc";
    mp['3'] = "def";
    mp['4'] = "ghi";
    mp['5'] = "jkl";
    mp['6'] = "mno";
    mp['7'] = "pqrs";
    mp['8'] = "tuv";
    mp['9'] = "wxyz";
    int T;
    cin >> T;
    int kase = 1;
    while (T--)
    {
        // 注意清空
        memset(trie, 0, sizeof(trie));
        memset(num, 0, sizeof(num));
        pos = 1;
        int n;
        cin >> n;
        for (int i = 0; i < n; i++)
        {
            string s;
            int w;
            cin >> s >> w;
            Insert(s, w);
        }
        cout << "Scenario #" << kase++ << ":" << endl;
        int m;
        cin >> m;
        for (int i = 0; i < m; i++)
        {
            cin >> str;
            string temp;
            int len = str.length();
            // 清空答案
            for (int j = 0; j <= len; j++)
                ans[j].clear();
            dfs(temp);
            // 輸出答案
            for (int i = 1; i < len; i++)
            {
                if (ans[i].length())
                    cout << ans[i] << endl;
                else
                    cout << "MANUALLY" << endl;
            }
            cout << endl;
        }
        cout << endl;
    }
    return 0;
}

1019:考研路茫茫——單詞情結(AC自動機+矩陣快速冪)

題意:給出 NN​ 個小寫字母組成的詞根,現在問長度不超過 LL​,只由小寫字母組成,且至少包含一個詞根的單詞數量,答案需要 %264\% 2^{64}​

範圍:0<N<6 , 0<L<2310 < N < 6~,~0 < L < 2^{31},字符串長度不超過 55

分析:頂不住了,看了半天題解還是沒有喫透,終點就是在於矩陣的構造,有興趣的同學可以嘗試理解一下!

HDU 2243 AC自動機->DP->附矩陣乘法板子

Code

// hard
// ac自動機+矩陣快速冪
// https://www.cnblogs.com/WABoss/p/5168429.html
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
int tn, ch[33][26], fail[33];
bool flag[33];
void insert(char *s)
{
    int x = 0;
    for (int i = 0; s[i]; ++i)
    {
        int y = s[i] - 'a';
        if (ch[x][y] == 0)
            ch[x][y] = ++tn;
        x = ch[x][y];
    }
    flag[x] = 1;
}
void init(int x)
{
    memset(fail, 0, sizeof(fail));
    queue<int> que;
    for (int i = 0; i < 26; ++i)
    {
        if (ch[0][i])
            que.push(ch[0][i]);
    }
    while (!que.empty())
    {
        int x = que.front();
        que.pop();
        for (int i = 0; i < 26; ++i)
        {
            if (ch[x][i])
                que.push(ch[x][i]), fail[ch[x][i]] = ch[fail[x]][i];
            else
                ch[x][i] = ch[fail[x]][i];
            flag[ch[x][i]] |= flag[ch[fail[x]][i]];
        }
    }
}
void init()
{
    memset(fail, 0, sizeof(fail));
    queue<int> que;
    for (int i = 0; i < 26; ++i)
    {
        if (ch[0][i])
            que.push(ch[0][i]);
    }
    while (!que.empty())
    {
        int now = que.front();
        que.pop();
        for (int i = 0; i < 26; ++i)
        {
            if (ch[now][i])
                que.push(ch[now][i]), fail[ch[now][i]] = ch[fail[now]][i];
            else
                ch[now][i] = ch[fail[now]][i];
            flag[ch[now][i]] |= flag[ch[fail[now]][i]];
        }
    }
}
struct Mat
{
    unsigned long long m[66][66];
    int n;
};
Mat operator*(const Mat &m1, const Mat &m2)
{
    Mat m = {0};
    m.n = m1.n;
    for (int i = 0; i < m.n; ++i)
    {
        for (int j = 0; j < m.n; ++j)
        {
            for (int k = 0; k < m.n; ++k)
                m.m[i][j] += m1.m[i][k] * m2.m[k][j];
        }
    }
    return m;
}
int main()
{
    int n, l;
    char str[6];
    while (~scanf("%d%d", &n, &l))
    {
        tn = 0;
        memset(ch, 0, sizeof(ch));
        memset(flag, 0, sizeof(flag));
        while (n--)
        {
            scanf("%s", str);
            insert(str);
        }
        init();
        Mat se = {0}, sm = {0};
        se.n = sm.n = 2;
        for (int i = 0; i < 2; ++i)
            se.m[i][i] = 1;
        sm.m[0][0] = 26;
        sm.m[0][1] = 1;
        sm.m[1][1] = 1;
        n = l;
        while (n)
        {
            if (n & 1)
                se = se * sm;
            sm = sm * sm;
            n >>= 1;
        }
        unsigned long long tot = se.m[0][1] * 26;

        Mat te = {0}, tm = {0};
        te.n = tm.n = tn + 1 << 1;
        for (int i = 0; i < te.n; ++i)
            te.m[i][i] = 1;
        for (int i = 0; i <= tn; ++i)
        {
            tm.m[i + tn + 1][i + tn + 1] = tm.m[i][i + tn + 1] = 1;
        }
        for (int i = 0; i <= tn; ++i)
        {
            if (flag[i])
                continue;
            for (int j = 0; j < 26; ++j)
            {
                if (flag[ch[i][j]])
                    continue;
                ++tm.m[i][ch[i][j]];
            }
        }
        Mat tmp = tm;
        tmp.n = tn + 1;
        n = l;
        while (n)
        {
            if (n & 1)
                te = te * tm;
            tm = tm * tm;
            n >>= 1;
        }
        Mat tmp2;
        tmp2.n = tn + 1;
        for (int i = 0; i <= tn; ++i)
        {
            for (int j = tn + 1; j < te.n; ++j)
                tmp2.m[i][j - tn - 1] = te.m[i][j];
        }
        tmp = tmp * tmp2;
        unsigned long long res = 0;
        for (int i = 0; i <= tn; ++i)
        {
            res += tmp.m[0][i];
        }
        printf("%llu\n", tot - res);
    }
    return 0;
}

1020:Wireless Password(AC自動機+狀壓DP)

題意: 有 MM 個小寫字母字符串,現在問至少選擇其中 KK 個字符串構成長度爲 NN 的字符串方案數爲多少?答案 %20090717\%20090717​,選擇的字符串可以重疊。

範圍:1N25 , 0KM101 \le N \le 25~,~0 \le K \le M \le 10,字符串長度不超過 1010

分析:由於選擇的字符串前後綴是可以重疊的,因此使用 AC自動機 來處理會比較方便。對於計算方案數的問題,暴力不可取,考慮進行 DP,在 AC自動機 上跑 DP 也是常見的套路了。本道題 DP 數組是三維的,dp[i][j][k]dp[i][j][k]​ 前面兩維就是常見的處理到字符串前 ii​ 位且處於自動機上 jj​ 結點,本題特殊的就在於第三維 kk​,因爲我們需要記錄下這 MM​ 個字符串出現的情況,總不能開個 MM​ 維數組來記錄吧, 因此考慮狀態壓縮,把這 MM​ 個字符的選擇情況壓縮成長度爲 MM​ 的二進制數。因此 DP 數組大小爲 NM10210N*M*10*2^{10}​,可以接受。

所以我們處理完 AC自動機 之後,對於自動機上的每一個結點,更新其所能到達結點的 dp 值並且更新二進制串,最後遍歷一遍所有 i==Ni==N 的 dp 點,如果二進制串中 11 的數量小於 kk 則忽略,否則更新答案。

細節很多,詳見代碼。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100 + 10;
const int MOD = 20090717;

int n, m, k;

char buf[100];

int dp[30][MAXN][1 << 10];  // 表示10位的二進制需要2^10的空間,即1<<10

struct Trie
{
    int next[MAXN][26], fail[MAXN], end[MAXN];
    int root, L;
    int newnode()
    {
        for (int i = 0; i < 26; i++)
        {
            next[L][i] = -1;
        }
        end[L++] = 0;
        return L - 1;
    }

    void init()
    {
        L = 0;
        root = newnode();
    }

    void insert(char buf[], int id)
    {
        int len = (int)strlen(buf);
        int now = root;
        for (int i = 0; i < len; i++)
        {
            if (next[now][buf[i] - 'a'] == -1)
            {
                next[now][buf[i] - 'a'] = newnode();
            }
            now = next[now][buf[i] - 'a'];
        }
        end[now] = (1 << id);  // 二進制對應位上改成1
    }

    void build()
    {
        queue<int> Q;
        fail[root] = root;
        for (int i = 0; i < 26; i++)
        {
            if (next[root][i] == -1)
            {
                next[root][i] = root;
            }
            else
            {
                fail[next[root][i]] = root;
                Q.push(next[root][i]);
            }
        }
        while (!Q.empty())
        {
            int now = Q.front();
            Q.pop();
            end[now] |= end[fail[now]];  // // 與後綴所包含的字符串的二進制進行結合
            for (int i = 0; i < 26; i++)
            {
                if (next[now][i] == -1)
                {
                    next[now][i] = next[fail[now]][i];
                }
                else
                {
                    fail[next[now][i]] = next[fail[now]][i];
                    Q.push(next[now][i]);
                }
            }
        }
    }

    void solve()
    {
        dp[0][0][0] = 1;  // 第一個點必須手動設置
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < L; j++)
            {
                for (int k = 0; k < (1 << m); k++)
                {
                    if (dp[i][j][k])
                    {
                        for (int x = 0; x < 26; x++)
                        {
                            int newx = i + 1;  // 下一個字符
                            int newy = next[j][x];  // 下一個結點
                            int newz = k | end[newy];  // 二進制結合
                            dp[newx][newy][newz] = dp[newx][newy][newz] % MOD + dp[i][j][k] % MOD;
                            dp[newx][newy][newz] %= MOD;
                        }
                    }
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < (1 << m); i++)
        {
            // 二進制中沒有k個1則略過
            bitset<32> bit(i);
            if (bit.count() < k)
                continue;
            for (int j = 0; j < L; j++)
            {
                ans = ans % MOD + dp[n][j][i] % MOD;
                ans %= MOD;
            }
        }
        cout << ans << endl;
    }
};

Trie ac;

int main()
{
    while (~scanf("%d%d%d", &n, &m, &k), n + m + k)
    {
        memset(dp, 0, sizeof(dp));
        ac.init();
        for (int i = 0; i < m; i++)
        {
            scanf("%s", buf);
            ac.insert(buf, i);
        }
        ac.build();
        ac.solve();
    }
    return 0;
}

1021:Ring(AC自動機+DP)

題意:給 MM 個小寫字母字符串 S[i]S[i] 及其權值 wiw_i,現在問構造一個長度爲 NN 個字符串,其權值最大能是多少。若 MM 個字符串在該字符串中出現重疊,則其權值分開統計。

範圍:0<N50 , 0<M100 , 1wi100 , S[i]1000 < N \le 50~,~0 < M \le 100~,~1 \le w_i \le 100~,~|S[i]| \le 100

分析:跟上題有些類似,但是難度稍低。因此字符串可以重疊,上 AC自動機,且求最大權值,不可暴力,無貪心策略,考慮 DP。dp[i][j]dp[i][j]​ 表示處理到字符串前 ii​ 個字符且位於自動機上第 jj​ 個結點時的最優解,那麼 dp[i][j]dp[i+1][next[j][x]]+w[j]dp[i][j] \rightarrow dp[i+1][next[j][x]]+w[j]​,其中 x[0,26)x \in [0, 26)​

本題還需要記錄路徑 pathpath,可以在 dp 的過程中一併處理。

詳見代碼。

Code

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1200 + 10;
const int MOD = 20090717;

int n, m;

char buf[20];

int dp[60][MAXN];

string path[60][MAXN];  // 記錄路徑

struct Trie
{
    int next[MAXN][26], fail[MAXN], end[MAXN];
    int root, L;
    int newnode()
    {
        for (int i = 0; i < 26; i++)
        {
            next[L][i] = -1;
        }
        end[L++] = 0;
        return L - 1;
    }

    void init()
    {
        memset(next, 0, sizeof(next));
        memset(fail, 0, sizeof(fail));
        memset(end, 0, sizeof(end));
        L = 0;
        root = newnode();
    }

    void insert(string buf, int w)
    {
        int len = buf.length();
        int now = root;
        for (int i = 0; i < len; i++)
        {
            if (next[now][buf[i] - 'a'] == -1)
            {
                next[now][buf[i] - 'a'] = newnode();
            }
            now = next[now][buf[i] - 'a'];
        }
        end[now] = w;
    }

    void build()
    {
        queue<int> Q;
        fail[root] = root;
        for (int i = 0; i < 26; i++)
        {
            if (next[root][i] == -1)
            {
                next[root][i] = root;
            }
            else
            {
                fail[next[root][i]] = root;
                Q.push(next[root][i]);
            }
        }
        while (!Q.empty())
        {
            int now = Q.front();
            Q.pop();
            end[now] += end[fail[now]]; // 注意累計
            for (int i = 0; i < 26; i++)
            {
                if (next[now][i] == -1)
                {
                    next[now][i] = next[fail[now]][i];
                }
                else
                {
                    fail[next[now][i]] = next[fail[now]][i];
                    Q.push(next[now][i]);
                }
            }
        }
    }

    void solve()
    {
        memset(dp, -1, sizeof(dp));
        dp[0][0] = 0;
        path[0][0] = "";
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < L; j++)
            {
                // 如果無法到達該狀態,那是白扯,跳過
                if (dp[i][j] != -1)
                {
                    for (int x = 0; x < 26; x++)
                    {
                        int newx = i + 1;
                        int newy = next[j][x];
                        // 更新答案,記錄路徑
                        if (dp[i][j] + end[newy] > dp[newx][newy])
                        {
                            dp[newx][newy] = dp[i][j] + end[newy];
                            path[newx][newy] = path[i][j] + (char)(x + 'a');
                        }
                        else if (dp[i][j] + end[newy] == dp[newx][newy])
                        {
                            string str = path[i][j] + (char)(x + 'a');
                            if (str < path[newx][newy])
                                path[newx][newy] = str;
                        }
                    }
                }
            }
        }
        // 找到最優解中路徑字典序最小的
        int ans = 0, row;
        string str;
        for (int i = 0; i <= n; i++)
        {
            for (int j = 0; j < L; j++)
            {
                if (ans < dp[i][j])
                {
                    ans = dp[i][j];
                    row = i;
                }
            }
        }
        if (ans == 0)
        {
            cout << endl;
            return;
        }
        str = "";
        for (int i = 0; i < L; i++)
        {
            if (dp[row][i] == ans && (str > path[row][i] || str == ""))
                str = path[row][i];
        }
        cout << str << endl;
    }
};

Trie ac;

string s[MAXN];

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        cin >> n >> m;
        ac.init();
        for (int i = 0; i < m; i++)
        {
            cin >> s[i];
        }
        for (int i = 0; i < m; i++)
        {
            int x;
            cin >> x;
            ac.insert(s[i], x);
        }
        ac.build();
        ac.solve();
    }
    return 0;
}

1022:DNA repair(AC自動機+DP)

題意:給出 NN 個有害的基因序列 S[i]S[i](只包含 ACGTACGT),再給出一個當前待修復基因序列 StrStr,問至少需要修改多少位置上的鹼基才能消除 StrStr​ 中的所有還有基因序列。

範圍:1N50 , 0S[i]20 , 0Str10001 \le N \le 50~,~0 \le |S[i]| \le 20~,~0 \le |Str| \le 1000​

分析:感覺上是可以用 AC自動機 處理後轉換成區間重疊問題進行貪

心的,應該是能做的,不過最後還是選擇了 DP。dp[i][j]dp[i][j] 表示處理到字符串前 ii 個字符且位於自動機上第 jj 個結點時的至少需要修改的點數量。

轉移方程:

Str[i+1]==next[j][x]Str[i+1] == next[j][x]​結點的字符: dp[i][j]dp[i+1][next[j][x]]dp[i][j] \rightarrow dp[i+1][next[j][x]]​x[0,26)x \in [0, 26)​

Str[i+1]!=next[j][x]Str[i+1] != next[j][x]結點的字符: dp[i][j]dp[i+1][next[j][x]]+1dp[i][j] \rightarrow dp[i+1][next[j][x]]+1x[0,26)x \in [0, 26)​

最後我們只需要遍歷一遍所有 i==Stri == |Str| 的 dp 點取最小值即可。

詳見代碼。

Code

#include <iostream>
#include <map>
#include <queue>
#include <string>
#include <cstring>
using namespace std;

const int MAXN = 1500 + 10;
const int INF = 0x3f3f3f3f;

int n, kase = 1;

char buf[MAXN];

int dp[MAXN][MAXN];

map<char, int> mp; // 鹼基到數字的映射

struct Trie
{
    int next[MAXN][4], fail[MAXN], end[MAXN];
    int root, L;
    int newnode()
    {
        for (int i = 0; i < 4; i++)
        {
            next[L][i] = -1;
        }
        end[L++] = 0;
        return L - 1;
    }

    void init()
    {
        memset(next, 0, sizeof(next));
        memset(fail, 0, sizeof(fail));
        memset(end, 0, sizeof(end));
        L = 0;
        root = newnode();
    }

    void insert(string buf)
    {
        int len = buf.length();
        int now = root;
        for (int i = 0; i < len; i++)
        {
            int idx = mp[buf[i]];
            if (next[now][idx] == -1)
            {
                next[now][idx] = newnode();
            }
            now = next[now][idx];
        }
        end[now] = 1;  // 該字符串需要修改
    }

    void build()
    {
        queue<int> Q;
        fail[root] = root;
        for (int i = 0; i < 4; i++)
        {
            if (next[root][i] == -1)
            {
                next[root][i] = root;
            }
            else
            {
                fail[next[root][i]] = root;
                Q.push(next[root][i]);
            }
        }
        while (!Q.empty())
        {
            int now = Q.front();
            Q.pop();
            // 如果後綴中有出現有害基因,則該字符串需要修改
            if (end[fail[now]])
                end[now] = 1;
            for (int i = 0; i < 4; i++)
            {
                if (next[now][i] == -1)
                {
                    next[now][i] = next[fail[now]][i];
                }
                else
                {
                    fail[next[now][i]] = next[fail[now]][i];
                    Q.push(next[now][i]);
                }
            }
        }
    }

    void solve()
    {
        string str;
        cin >> str;
        int len = str.length();
        for (int i = 0; i <= len; i++)
        {
            for (int j = 0; j < L; j++)
            {
                dp[i][j] = INF;
            }
        }
        dp[0][0] = 0;
        for (int i = 0; i < len; i++)
        {
            for (int j = 0; j < L; j++)
            {
                // 必須能夠到達該狀態
                if (dp[i][j] < INF)
                {
                    for (int x = 0; x < 4; x++)
                    {
                        int newx = i + 1;
                        int newy = next[j][x];
                        // 轉移狀態後有害,則略過
                        if (end[newy])
                            continue;
                        // 相同則無須修改
                        if (mp[str[newx - 1]] == x)
                        {
                            dp[newx][newy] = min(dp[newx][newy], dp[i][j]);
                        }
                        // 否則dp值+1
                        else
                        {
                            dp[newx][newy] = min(dp[newx][newy], dp[i][j] + 1);
                        }
                    }
                }
            }
        }
        // 取最小值
        int ans = INF;
        for (int i = 0; i < L; i++)
        {
            ans = min(ans, dp[len][i]);
        }
        if (ans >= INF)
            ans = -1;
        cout << "Case " << kase++ << ": " << ans << endl;
    }
};

Trie ac;

int main()
{
    mp['A'] = 0;
    mp['C'] = 1;
    mp['G'] = 2;
    mp['T'] = 3;
    while (cin >> n, n)
    {
        ac.init();
        for (int i = 0; i < n; i++)
        {
            cin >> buf;
            ac.insert(buf);
        }
        ac.build();
        ac.solve();
    }
    return 0;
}

1023:Lost’s revenge(AC自動機+DP+Hash)

題意:給 NN 個模式基因序列 S[i]S[i],再給一個基因序列串 SS,現在可以隨意調換 SS 中鹼基的位置,問可以得到的最大匹配數,模式串可以重疊。

範圍:1N50 , S[i]10 , S401 \le N \le 50~,~|S[i]| \le 10~,~|S| \le 40,字符串只包含 ACGTACGT

分析:模式串可以重疊,使用 AC自動機,求最大匹配數,不暴力,無貪心策略,考慮 DP。此外,因爲 SS​ 的順序可以隨意調換,只需要考慮串中四種鹼基的數量,可以狀壓DP,也可以使用 Hash,這裏使用 Hash。

因爲字符串 SS​ 的長度最多爲 4040​,因此單種鹼基的出現次數最多也爲 4040​,所以 HashHash​ 數組的大小爲 4040404040*40*40*40​Hash[i][j][k][l]Hash[i][j][k][l]​ 表示 AA​ 出現 ii​ 次、BB​ 出現 jj​ 次、CC​ 出現 kk​ 次、DD​ 出現 ll​ 次時該狀態的編號。

dp[i][j]dp[i][j]​ 表示位於自動機 ii​ 結點且當前狀態的 hashhash​ 值爲 jj​ 時的最大匹配數。

轉移方程:dp[next[i][x]][hash2]=max(dp[next[i][x]][hash2],dp[i][hash1]+end[next[i][x]])dp[next[i][x]][hash2] = max(dp[next[i][x]][hash2], dp[i][hash1]+end[next[i][x]])​

ii 可以轉移到下個字符爲 xx 結點 next[i][x]next[i][x]hash1hash1 爲狀態 iihashhash 值,hash2hash2 爲狀態 next[i][x]next[i][x] 的狀態,endend 爲以下個節點爲結尾的字符串數量。

詳見代碼。

Code

#include <iostream>
#include <map>
#include <queue>
#include <string>
#include <cstring>
using namespace std;

const int MAXN = 500 + 10;
const int INF = 0x3f3f3f3f;

int n, kase = 1;

char buf[MAXN];

// num保存各個鹼基的使用數量 hash表示各個鹼基數量情況下的編號
int dp[MAXN][15000], num[4], Hash[45][45][45][45];

map<char, int> mp;  // 映射鹼基到數字

struct Trie
{
    int next[MAXN][4], fail[MAXN], end[MAXN];
    int root, L;
    int newnode()
    {
        for (int i = 0; i < 4; i++)
        {
            next[L][i] = -1;
        }
        end[L++] = 0;
        return L - 1;
    }

    void init()
    {
        memset(num, 0, sizeof(num));
        memset(next, 0, sizeof(next));
        memset(fail, 0, sizeof(fail));
        memset(end, 0, sizeof(end));
        L = 0;
        root = newnode();
    }

    void insert(string buf)
    {
        int len = buf.length();
        int now = root;
        for (int i = 0; i < len; i++)
        {
            int idx = mp[buf[i]];
            if (next[now][idx] == -1)
            {
                next[now][idx] = newnode();
            }
            now = next[now][idx];
        }
        end[now]++;
    }

    void build()
    {
        queue<int> Q;
        fail[root] = root;
        for (int i = 0; i < 4; i++)
        {
            if (next[root][i] == -1)
            {
                next[root][i] = root;
            }
            else
            {
                fail[next[root][i]] = root;
                Q.push(next[root][i]);
            }
        }
        while (!Q.empty())
        {
            int now = Q.front();
            Q.pop();
            // 累計後綴中的匹配數量
            if (end[fail[now]])
                end[now] += end[fail[now]];
            for (int i = 0; i < 4; i++)
            {
                if (next[now][i] == -1)
                {
                    next[now][i] = next[fail[now]][i];
                }
                else
                {
                    fail[next[now][i]] = next[fail[now]][i];
                    Q.push(next[now][i]);
                }
            }
        }
    }

    void solve()
    {
        string str;
        cin >> str;
        int len = str.length();
        memset(dp, -1, sizeof(dp));
        // 計算字符串中各個鹼基的數量
        for (int i = 0; i < len; i++)
        {
            num[mp[str[i]]]++;
        }
        // 爲所有鹼基數量的情況進行編號
        int idx = 0;
        for (int i = 0; i <= num[0]; i++)
        {
            for (int j = 0; j <= num[1]; j++)
            {
                for (int k = 0; k <= num[2]; k++)
                {
                    for (int l = 0; l <= num[3]; l++)
                    {
                        Hash[i][j][k][l] = idx++;
                    }
                }
            }
        }
        dp[0][0] = 0;
        int ans = 0;
        // 枚舉所有鹼基數量情況,更新dp
        for (int r1 = 0; r1 <= num[0]; r1++)
            for (int r2 = 0; r2 <= num[1]; r2++)
                for (int r3 = 0; r3 <= num[2]; r3++)
                    for (int r4 = 0; r4 <= num[3]; r4++)
                    {
                        // 當前情況下的hash值
                        int now = Hash[r1][r2][r3][r4];
                        // 第一個要跳過
                        if (now == 0)
                            continue;
                        for (int i = 0; i < L; i++)
                        {
                            for (int x = 0; x < 4; x++)
                            {
                                // 子狀態的hash值
                                int temp;
                                if (x == 0 && r1 >= 1)
                                    temp = Hash[r1 - 1][r2][r3][r4];
                                else if (x == 1 && r2 >= 1)
                                    temp = Hash[r1][r2 - 1][r3][r4];
                                else if (x == 2 && r3 >= 1)
                                    temp = Hash[r1][r2][r3 - 1][r4];
                                else if (x == 3 && r4 >= 1)
                                    temp = Hash[r1][r2][r3][r4 - 1];
                                else
                                    continue;
                                // 子狀態無法到達則略過
                                if (dp[i][temp] == -1)
                                    continue;
                                // 更新dp值
                                dp[next[i][x]][now] = max(dp[next[i][x]][now], dp[i][temp] + end[next[i][x]]);
                                ans = max(ans, dp[next[i][x]][now]);
                            }
                        }
                    }
        cout << "Case " << kase++ << ": " << ans << endl;
    }
};

Trie ac;

int main()
{
    mp['A'] = 0;
    mp['C'] = 1;
    mp['G'] = 2;
    mp['T'] = 3;
    while (cin >> n, n)
    {
        ac.init();
        for (int i = 0; i < n; i++)
        {
            cin >> buf;
            ac.insert(buf);
        }
        ac.build();
        ac.solve();
    }
    return 0;
}

四、神仙題

1024:Resource Archiver(AC自動機+SPFA+狀壓DP)

AC自動機的fail指針跑spfa狀壓DP求最短串?????

有興趣的同學移步大佬博客看一看吧,估計我這輩子也做不出來這種題吧(呸!)

【END】感謝觀看

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