1. 程式人生 > 其它 >Solution -「LOCAL」Minimal DFA

Solution -「LOCAL」Minimal DFA

\(\mathscr{Description}\)

  Private link.

  令 \(\Sigma=\{\texttt a,\texttt b\}\),對於所有形式語言 \(L\subseteq\Sigma^n\)\(L\) 的最小 DFA 狀態數的最大值,以及取到這一最大值時,\(|L|\) 的最小值和最大值。

  \(n\le10^3\)

\(\mathscr{Solution}\)

  引理 對於任意串 \(x,y\in\Sigma^\star\),定義 \(x\equiv_Ly\),當且僅當 \(\forall z\in\Sigma^\star,xz\in L\equiv yz\in L\)

。語言 \(L\) 的最小 DFA 狀態數就是等價關係 \(\equiv_L\)\(\Sigma^\star\) 上劃分出的等價類數目。

  直接考慮 \(L\) 的最小 DFA 的形態,可以發現:

  • 對於任意狀態,從開始狀態轉移到它的路徑長度唯一;
  • 不存在兩個狀態的轉移完全相同(輸入同樣字元,轉移向同一個狀態)。

  根據性質一,我們將 DFA 的 DAG 分層,那麼總狀態數就是每層的狀態數之和。令 \(Q_i~(i=0,1,\dots,m)\) 為第 \(i\) 層的狀態,則 \(Q_0=\{q_s\}\) 為開始狀態,\(Q_m=\{q_t\}\) 為唯一接受狀態,此時對於第 \(k\)

層,從 \(Q_{k-1}\)\(Q_k\) 的限制來看,顯然 \(|Q_k|\le2|Q_{k-1}|\);從 \(Q_{k+1}\)\(Q_k\) 的限制來看,為了滿足性質二,\(Q_{k+1}\) 最多為 \(Q_k\) 內的某個點提供 \((|Q_{k+1}|+1)^2-1\) 種轉移的選擇(\(1\) 表示空狀態,即分別列舉 \(\texttt a\)\(\texttt b\) 轉移向誰,不能同時為空)。根據這一構造,每個 \(|Q_k|\) 的上界顯然可以同時取到,所以第一問就解決了。

  設 \(Q_p\) 為最後一層滿足 \(Q_p=2|Q_{p-1}|\)

的狀態,此後,\(Q_k\) 的大小都將由性質二的限制決定。前後都已經固定,\(|L|\) 只取決於 \(\delta_p:Q_p\times\Sigma\rightarrow Q_{p+1}\) 的長相。

  先來刻畫 \(Q_{p+1}\) 內每個點對應的字尾串數量。令 \(G_k(x)~(k=p+1,\dots,m)\) 表示 \(Q_k\) 中,狀態數量關於狀態對應字尾串數量的 GF,那麼 \(G_k(x)=(G_{k+1}(x)+1)^2-1\),其實就是 \(G_k(x)=(x+1)^{2^{m-k}}-1\)

  然後,討論一下 \(|Q_p|\)\(|Q_{p+1}|\) 的大小關係:

  若 \(|Q_p|<|Q_{p+1}|\),注意到此時必然有 \(|Q_{p+1}|=2|Q_p|+1\)\(\delta_p\) 的當務之急是讓每個 \(q\in Q_{p+1}\) 都有前驅。因此先讓 \(Q_p\) 內每個點隨便在 \(Q_{p+1}\) 裡連兩個,剩下一個狀態放棄一個轉移最小化 \(|L|\),增加一個到 \(Q_{p+1}\) 內字尾最多的狀態的轉移最大化 \(|L|\)

  若 \(|Q_p|\ge|Q_{p+1}|\),我們需要同時關注“每個狀態都有前驅”以及“最小/最大化 \(L\)”的要求。先描述出“在 \(Q_{p+1}\) 裡連兩個後繼”的貢獻,令 \(F(x)\) 表示連線方案數關於連線到後繼的字尾總數的 GF,那麼 \(F(x)=(G_{p+1}(x)+1)^2-1=(x+1)^{2^{m-p+1}}-1\)。接著貪心考慮最小化與最大化 \(L\) 的構造方案:

  • 最小化 \(|L|\),先用 \(Q_p\) 內的 \(|Q_{p+1}|\) 個狀態直接各自連一條轉移邊到 \(Q_{p+1}\) 內的狀態,求到剩下能選的連線方案的 GF \(F_1(x)\),在 \(F_1(x)\) 裡按指標升序貪心選擇。
  • 最大化 \(|L|\),先用 \(Q_p\) 內的 \(|Q_{p+1}|\) 個狀態直接各自連一條轉移邊到 \(Q_{p+1}\) 內的狀態,再連一條轉移邊到 \(Q_{p+1}\) 內字尾串最多的狀態,其餘和最小化類似。

  撇開高精度計算,複雜度是 \(\mathcal O(n)\) 的。

\(\mathcal{Code}\)

# Rainybunny #

import sys

if __name__ == "__main__":
    sys.stdin = open('dfa.in', 'r')
    sys.stdout = open('dfa.out', 'w')

    n = int(input())
    ans = [0 for _ in range(3)]

    m = 0
    while 1 << m + 1 <= n - m - 1: m += 1
    ## the last limited level's G.F. is f_m(x)=(x+1)^{2^m}-1.
    # sys.stderr.write(str(m) + '\n')

    ans[0] = (1 << n - m) - 1
    for i in range(0, m + 1):
        ans[0] += (1 << (1 << i)) - 1

    lef = 1 << n - m - 1; rig = (1 << (1 << m)) - 1
    if (lef < rig):
        sys.stderr.write('type 1\n')
        ans[1] = 1 << ((1 << m) + m - 1)
        ans[2] = ans[1] + (1 << m)
    else:
        sys.stderr.write('type 2\n')
        ## get f_m(x)(->bino[0]) and f_m^2(x)(->bino[1])
        bino = [[0 for _ in range((1 << m) + 1)],
          [0 for _ in range((1 << m + 1) + 1)]]
        bino[0][0] = bino[1][0] = 1
        for i in range(1, (1 << m) + 1):
            bino[0][i] = bino[0][i - 1] * ((1 << m) - i + 1) // i
        for i in range(1, (1 << m + 1) + 1):
            bino[1][i] = bino[1][i - 1] * ((1 << m + 1) - i + 1) // i

        ## get ans[1].
        for i in range(1, (1 << m) + 1):
            ans[1] += i * bino[0][i]
            bino[1][i] -= bino[0][i]
        rest = lef - rig; cur = 1
        while rest and rest > bino[1][cur]:
            ans[1] += bino[1][cur] * cur
            rest -= bino[1][cur]
            cur += 1
        ans[1] += cur * rest

        ## get ans[2].
        for i in range(1, (1 << m) + 1):
            ans[2] += bino[0][i] * (i + (1 << m))
            bino[1][i] += bino[0][i] # recover it
            bino[1][i + (1 << m)] -= bino[0][i]
        rest = lef - rig; cur = 1 << m + 1
        while rest and rest > bino[1][cur]:
            ans[2] += bino[1][cur] * cur
            rest -= bino[1][cur]
            cur -= 1
        ans[2] += cur * rest

    print("%d %d %d" % (ans[0], ans[1], ans[2]))

    sys.stdin.close()
    sys.stdout.close()