計蒜客 - 情報加密

計蒜客 - 情報加密

蒜頭君是我軍的情報專家,現在我軍有一份重要情報要發送出去,這是一份特別重要的情報,一旦被敵軍截獲,裏面有一些重要的片段會暴露我們的身份,所以蒜頭君需要改變一些字符,這樣即使敵軍截獲了我們的情報,也無法獲得正確信息。

比如,情報中如果包含 AAB, ABCCAB 就會暴露我們的身份,但是情報中的 AABCAB 把這三個片段都包含了,不過我們只需改變兩個字符使其變成 ABBCAB,這樣就不會暴露任意一個關鍵片段信息了。注意,我們的情報中只包含 A, B, C, D 這四個字母,我們修改時也只能使用這四個字母。

你要幫助蒜頭君通過改變最少的字符來加密情報。

輸入格式

第一行一個整數 nn (n50)(n\leq 50),表示可能關鍵的情報段數量。

接下來 nn 行,每行一個長度不超過 2020 的字符串,由 ABCD構成。

最後一行,一個長度不超過 10310^3 的字符串是,由 ABCD 構成。

2
A
DB
DBAADB

輸出格式

一行一個整數,表示最少修改字符的數量。若無解,則輸出 1-1

4

這道題是想要我們求出對於一個給定的字符串,至少修改多少次可以使得該字符串不出現任何一個字典中的子串。

解決方法是使用 AC 自動機 + 動態規劃。

這題字符範圍只有 A ~ D,所以 MAXC = 4,其餘的 AC 自動機的部分不需要修改,直接套用模板,依然只需要修改 solve() 函數。

我們定義 dp[][] 數組用來動態規劃。

狀態的定義 dp[i][p] 表示對於情報中前 i 個字符組成的串,在結點 p 的時候,最少需要修改多少次才能滿足要求。

初始化只需要設定 dp[0][0] = 0,因爲對於空串,顯然不需要修改。

下面我們考慮狀態轉移。

現在我站在 p 這個節點,手上已經有了情報的前 i 個字符,而且已經知道了修改多少次能夠使情報更安全。爲了構造一個不包含關鍵情報的串,我現在可以在前綴樹中沿着邊走,即選擇 child[p] 中的一者,走下去,走一步,情報長度就會加一。當遇到某個結點 child[p][C] 是一個單詞的結束時(sta[child[p][C]] == true),說明不能走這條路,要換一條,即可以嘗試 child[p][A] 或者 child[p][B] 等。

那麼,i 只能轉移到 i + 1p 只能轉移到 child[p]['A'] ~ child[p]['D'] 四者之一。

其中,如果 sta[child[p][x]]true,說明那裏是一個關鍵情報,不能選擇這條路,需要繞過,continue

這樣,對於大多數的情況,dp[i][child[p][x]] 的值顯然應該是等於 dp[i][p] + 1

可是,如果下一個(第 i + 1 個字符)恰好是我選擇的那個 child[p] 呢,比如,我現在已經擁有了 ABB,我決定往 C 走,而原來的情報就是 ABBC,那麼就可以直接用原來的情報,不需要修改也可以了。

在狀態轉移的過程中始終保留 dp[i][j] 的最小值。

int len = strlen(ch + 1);
for (int i = 1; i <= len; i++) {
	for (int p = 0; p < tot; p++) {
		if (sta[p]) {
			// 如果當前節點是一個關鍵情報,則不能走這條路
			continue;
		}
		for (int x = 0; x < MAXC; x++) {
			if (sta[child[p][x]]) {
				// 如果走下去的方向是關鍵情報,則需要換一條路
				continue;
			}
			// 到這裏,就是一個安全的路,可以做一次狀態轉移
			// 如果走過去的那個方向,與情報中下一個字符是一樣的,就不需要一次修改
			int modify = ch[i] == 'a' + x ? 0 : 1;
			dp[i + 1][child[p][x]] = min(dp[i + 1][child[p][x]], dp[i][p] + modify);
		}
	}
}

最後只需要遍歷 dp[len + 1][] 數組中最小值即可。注意動態規劃數組的第一個下標是 len + 1 是因爲我們需要確保整個情報都不包含關鍵信息。

由於可能出現無解的情況,而我們又需要記錄最小值,所以我們應該把 dp[][] 數組初始化爲無窮大,並且在遍歷的過程中跳過所有值爲無窮大的結點,這樣,最後返回的時候就可以直接判斷出是不是無解了。

int solve(char *ch) {
	cout << strlen(ch + 1) << endl;

	// 初始化 dp 數組
	memset(dp, 0x3F, sizeof(dp));
	dp[1][0] = 0; // 字符串下標從 1 開始

	int len = strlen(ch + 1);
	for (int i = 1; i <= len; i++) {
		for (int p = 0; p < tot; p++) {
			if (sta[p] || dp[i][p] == INF) {
				// 如果當前節點是一個關鍵情報,則不能走這條路
				continue;
			}
			for (int x = 0; x < MAXC; x++) {
				if (sta[child[p][x]]) {
					// 如果走下去的方向是關鍵情報,則需要換一條路
					continue;
				}
				// 到這裏,就是一個安全的路,可以做一次狀態轉移
				// 如果走過去的那個方向,與情報中下一個字符是一樣的,就不需要一次修改
				int modify = ch[i] == 'A' + x ? 0 : 1;
				dp[i + 1][child[p][x]] = min(dp[i + 1][child[p][x]], dp[i][p] + modify);
			}
		}
	}

	int ans = INF;
	for (int i = 0; i < tot; i++) {
		// 注意是 len + 1
		ans = min(ans, dp[len + 1][i]);
	}
	return ans == INF ? -1 : ans;
}

但是別急,還有一點點小問題。

在使用 AC 自動機進行動態規劃的時候,要特別注意,算 fail 要把單詞結尾標記傳遞下來,避免出現某個後綴是另外一個單詞的情況。

if (sta[fail[p]]) {
	sta[p] = 1;
}
// 即 sta[p] |= sta[fail[p]]

具體的分析可以參考這一篇文章:https://b.ejq.me/2017/03/01/aczi-dong-ji-yong-yu-dong-tai-gui-hua-shi-de-biao-ji-chuan-di/

#include <bits/stdc++.h>

using namespace std;

const int MAXC = 4; // 這裏字符範圍只有 A ~ D
const int MAXN = 1007;
const int INF = 0x3F3F3F3F;

int child[MAXN][MAXC], fail[MAXN], sta[MAXN], Q[MAXN];
int tot;
int dp[MAXN][MAXN];

/**
 * AC 自動機
 */
struct AC_Automaton {

    /**
     * 清空
     */
    void clear() {
        memset(child, 255, sizeof(child));
        memset(fail, 0, sizeof(fail));
        tot = 0;
        memset(sta, 0, sizeof(sta));
    }

    /**
     * 插入單詞
     * @param ch 單詞,該單詞下標從 1 開始
     */
    void insert(char *ch) {
        int p = 0, l = strlen(ch + 1);
        for (int i = 1; i <= l; i++) {
            if (child[p][ch[i] - 'A'] == -1) child[p][ch[i] - 'A'] = ++tot;
            p = child[p][ch[i] - 'A'];
        }
        sta[p] = 1; // 以結點 p 的字符串是否存在,由於本題只需要是否存在,設置 true 就好了,像上一題還需要統計個數,可以改爲 sta[p]++,表示有多少個
    }

    /**
     * 對插入了單詞的前綴樹構造失敗指針
     */
    void build() {
        int l = 0, r = 0;
        for (int i = 0; i < MAXC; i++)
            if (child[0][i] == -1)
                child[0][i] = 0;
            else
                Q[++r] = child[0][i];
        while (l < r) {
            int p = Q[++l];
            if (sta[fail[p]]) {
                sta[p] = 1;
            }
            for (int i = 0; i < MAXC; i++)
                if (child[p][i] == -1)
                    child[p][i] = child[fail[p]][i];
                else {
                    fail[child[p][i]] = child[fail[p]][i];
                    Q[++r] = child[p][i];
                }
        }
    }

    /**
     *
     * @param ch 字符串,下標從 1 開始
     * @return 最小修改次數
     */
    int solve(char *ch) {

        // 初始化 dp 數組
        memset(dp, 0x3F, sizeof(dp));
        dp[1][0] = 0; // 字符串下標從 1 開始

        int len = strlen(ch + 1);
        for (int i = 1; i <= len; i++) {
            for (int p = 0; p < tot; p++) {
                if (sta[p] || dp[i][p] == INF) {
                    // 如果當前節點是一個關鍵情報,則不能走這條路
                    continue;
                }
                for (int x = 0; x < MAXC; x++) {
                    if (sta[child[p][x]]) {
                        // 如果走下去的方向是關鍵情報,則需要換一條路
                        continue;
                    }
                    // 到這裏,就是一個安全的路,可以做一次狀態轉移
                    // 如果走過去的那個方向,與情報中下一個字符是一樣的,就不需要一次修改
                    int modify = ch[i] == 'A' + x ? 0 : 1;
                    dp[i + 1][child[p][x]] = min(dp[i + 1][child[p][x]], dp[i][p] + modify);
                }
            }
        }

        int ans = INF;
        for (int i = 0; i < tot; i++) {
            // 注意是 len + 1
            ans = min(ans, dp[len + 1][i]);
        }
        return ans == INF ? -1 : ans;
    }
} T;

int main() {
    // 構造 AC 自動機
    auto ac = new AC_Automaton();
    ac->clear();
    int n;
    scanf("%d", &n);
    // 讀入單詞,加入前綴樹
    char *s = (char *) malloc(sizeof(char) * MAXN);
    for (int i = 0; i < n; i++) {
        // 字符串下標從 1 開始
        scanf("%s", s + 1);
        ac->insert(s);
    }
    // 根據前綴樹中的單詞構造失敗指針,即構造字典
    ac->build();
    // 給定的文章,下標從 1 開始
    char *t = (char *) malloc(sizeof(char) * MAXN);
    scanf("%s", t + 1);
    // 執行操作,輸出結果
    printf("%d\n", ac->solve(t));
    return 0;
}

歡迎關注我的個人博客以閱讀更多優秀文章:凝神長老和他的朋友們(https://www.jxtxzzw.com)

也歡迎關注我的其他平臺:知乎( https://s.zzw.ink/zhihu )、知乎專欄( https://s.zzw.ink/zhuanlan )、嗶哩嗶哩( https://s.zzw.ink/blbl )、微信公衆號( 凝神長老和他的朋友們 )
凝神長老的二維碼們

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