計蒜客 - 新年禮物

計蒜客 新年禮物

新年了,蒜廠 BOSS 要給小蒜頭們發新年禮物,新年禮物有很多份,怎麼分配這些禮物呢?蒜廠 BOSS 打算讓大家玩一個遊戲。

蒜頭們可以從抽獎箱裏抽出 NN 個字符串,第 ii 個是 xix_i,按抽出的順序從 11 編號。一個蒜頭可以得到的禮物個數決定於一個特別的子序列(不要求連續)。當且僅當 xix_ixjx_j 的前綴,xix_i 也是 xjx_j 的後綴時,字符串 xix_ixjx_j(i<ji \lt j) 能在一個子序列中。一個蒜頭可以得到的禮物個數符合要求的子序列中最長的那個的長度。

輸入格式

第一行輸入一個整數 NN,緊接着輸入 NN 行字符串,每個字符串僅包含小寫或大寫字母。

輸入數據總共少於 2×1062\times 10^6 個字符。

輸出格式

答案輸出在一行,一個整數,表示這個蒜頭能得到的禮物個數。

樣例 1

5
A
B
AA
BBB
AAA
3

樣例 2

5
A
ABA
BBB
ABABA
AAAAAB
3

這道題首先要用拓展 KMP 做一次預處理,記錄每個字符串哪些位置的前綴和後綴相同。

private static int[] extendedKMP(String t) {
    int n = t.length();
    int[] next = new int[n];
    next[0] = n;
    int p = 0;
    while (p < n - 1 && t.charAt(p) == t.charAt(p + 1)) {
        p++;
    }
    next[1] = p;
    int k = 1, l;
    for (int i = 2; i < n; i++) {
        p = k + next[k] - 1;
        l = next[i - k];
        if (i + l - 1 < p) {
            next[i] = l;
        } else {
            int j = p - i + 1;
            if (j < 0) {
                j = 0;
            }
            while (i + j < n && t.charAt(i + j) == t.charAt(j)) {
                j++;
            }
            next[i] = j;
            k = i;
        }
    }
    return next;
}

然後我們就可以把字符串插入到前綴樹中了。在插入的過程中,始終用 dp[] 數組記錄下當前滿足條件的最大值,最後只需要遍歷 dp[] 數組,找到最大值就是答案。

public static void main(String[] args) {
    int n = in.nextInt();
    Trie t = new Trie();
    int[] dp = new int[n];
    for (int i = 0; i < n; i++) {
        String s = in.next();
        int[] next = extendedKMP(s);
        t.insert(s, next);
    }
    int ans = 0;
    for (int i = 0; i < n; i++) {
        ans = Math.max(ans, dp[i]);
    }
    System.out.println(ans);
}

在插入的過程中,當滿足 isEnd() 這個條件的時候,word 到當前位置爲止的前綴是一個已經存在的子串 xix_i,所以前綴的性質滿足,只需要檢查是不是後綴。

這個前綴的長度 len = i + 1,如果距離最後爲 len 的那個位置上,它滿足的前綴長度恰好是 len,那麼就是滿足的。

if (node.isEnd()) {
    // 當滿足 isEnd() 這個條件的時候,word 到當前位置爲止的前綴是一個已經存在的子串 xi
    // 所以前綴的性質滿足,只需要檢查是不是後綴
    // 這個前綴的長度 len = i + 1
    int len = i + 1;
    // 如果距離最後爲 len 的那個位置上,它滿足的前綴長度恰好是 len,那麼就是滿足的
    if (next[word.length() - len] == len) {
        // TODO
    }
}

對於滿足條件的值,我們需要記錄下當前滿足條件的最大值。

爲了編程的方便,我在前綴樹的結點中增加了一個字段,用來記錄當前結點對應的單詞的編號,即對應 dp[] 數組的下標。

private int index;

public void setEnd(int index) {
    end = true;
    this.index = index;
}

public int getIndex() {
    return index;
}

最後,在上面的 TODO 中填上如下內容:

if (next[word.length() - len] == len) {
    dp[index] = Math.max(dp[index], dp[node.getIndex()] + 1);
}

注意 dp[] 數組應該被初始化爲 1,而不是 0。

import java.util.Arrays;
import java.util.HashMap;
import java.util.Scanner;

public class Main {

    static Scanner in = new Scanner(System.in);

    private static int[] extendedKMP(String t) {
        int n = t.length();
        int[] next = new int[n];
        next[0] = n;
        if (n == 1) {
            // 注意 corner case,否則會出現數組越界
            return next;
        }
        int p = 0;
        while (p < n - 1 && t.charAt(p) == t.charAt(p + 1)) {
            p++;
        }
        next[1] = p;
        int k = 1, l;
        for (int i = 2; i < n; i++) {
            p = k + next[k] - 1;
            l = next[i - k];
            if (i + l - 1 < p) {
                next[i] = l;
            } else {
                int j = p - i + 1;
                if (j < 0) {
                    j = 0;
                }
                while (i + j < n && t.charAt(i + j) == t.charAt(j)) {
                    j++;
                }
                next[i] = j;
                k = i;
            }
        }
        return next;
    }

    public static void main(String[] args) {
        int n = in.nextInt();
        Trie t = new Trie();
        int[] dp = new int[n];
        // 賦初值爲 1,因爲至少自己是滿足條件的
        Arrays.fill(dp, 1);
        for (int i = 0; i < n; i++) {
            String s = in.next();
            int[] next = extendedKMP(s);
            t.insert(s, next, i, dp);
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            ans = Math.max(ans, dp[i]);
        }
        System.out.println(ans);
    }

}

class Trie {

    private TrieNode root;

    /**
     * Initialize your data structure here.
     */
    public Trie() {
        root = new TrieNode();
    }

    /**
     * Inserts a word into the trie.
     */
    public void insert(String word, int[] next, int index, int[] dp) {
        TrieNode node = root;
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            if (!node.contains(c)) {
                node.put(c);
            }
            node = node.get(c);
            if (node.isEnd()) {
                // 當滿足 isEnd() 這個條件的時候,word 到當前位置爲止的前綴是一個已經存在的子串 xi
                // 所以前綴的性質滿足,只需要檢查是不是後綴
                // 這個前綴的長度 len = i + 1
                int len = i + 1;
                // 如果距離最後爲 len 的那個位置上,它滿足的前綴長度恰好是 len,那麼就是滿足的
                if (next[word.length() - len] == len) {
                    dp[index] = Math.max(dp[index], dp[node.getIndex()] + 1);
                }
            }
        }
        node.setEnd(index);
    }
}

class TrieNode {
    private HashMap<Character, TrieNode> links;
    private boolean end;
    private int index;

    public TrieNode() {
        links = new HashMap<>();
        end = false;
    }

    public boolean isEnd() {
        return end;
    }

    public void setEnd(int index) {
        end = true;
        this.index = index;
    }

    public int getIndex() {
        return index;
    }

    public TrieNode get(char c) {
        return links.get(c);
    }

    public void put(char c) {
        links.put(c, new TrieNode());
    }

    public boolean contains(char c) {
        return links.containsKey(c);
    }

}

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

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

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