【算法】利用有限自動機進行字符串匹配

Timus 1102. Strange Dialog 要求判斷給定的輸入是否爲合法的對話。

1102. Strange Dialog

Time Limit: 1.0 second
Memory Limit: 16 MB

One entity named "one" tells with his friend "puton" and their conversation is interesting. "One" can say words "out" and "output", besides he calls his friend by name. "Puton" can say words "in", "input" and "one". They understand each other perfect and even write dialogue in strings without spaces.

You have N strings. Find which of them are dialogues.

Input

In the first line of input there is one non-negative integer N ≤ 1000. Next N lines contain non-empty strings. Each string consists of small Latin letters. Total length of all strings is no more then 107 characters.

Output

Output consists of N lines. Line contains word "YES", if string is some dialogue of "one" and "puton", otherwise "NO".

Sample

input output
6
puton
inonputin
oneputonininputoutoutput
oneininputwooutoutput
outpu
utput
YES
NO
YES
NO
NO
NO

Problem Author: Katya Ovechkina
Problem Source: Tetrahedron Team Contest May 2001


題意

one 和 puton 這兩個人進行交談。one 只能夠說:out、output 和 puton 這三個單詞。而 puton 只能說 in、input 和 one 這三個單詞。她們之間的對話是由單詞直接連接而成(單詞之間沒有空格)。

你的任務是判斷給定的輸入(該輸入僅包含小寫拉丁字母)是否爲合法的對話。

數學背景

一個有限自動機(deterministic finite automaton, DFA) M 是一個 5-元組(Q, q0, A, Σ, δ),其中:

  • Q 是狀態的有限集合
  • q0 ∈ Q 是初始狀態
  • A Q 是一個接受狀態集合
  • Σ 是有限的輸入字母表
  • δ 是一個從 Q × Σ 到 Q 的函數,稱爲 M轉移函數

有限自動機開始於狀態 q0,每次讀入輸入字符串的一個字符。如果有限自動機在狀態 q 時讀入了輸入字符 a,則它從狀態 q 變爲狀態 δ(q, a)(進行了一次轉移)。每當其當前狀態 q 屬於 A 時,就說自動機 M 接受了迄今爲止所讀入的字符串。沒有被接受的輸入稱爲被拒絕的輸入。

很多字符串匹配算法都要建立一個有限自動機,它通過對文本字符串 T 進行掃描的方法,找出模式 P 的所有出現位置。用於字符串匹配的自動機都是非常有效的:它們只對每個文本字符檢查一次,並且檢查每個文本字符的時間爲常數。因此,在建立好自動機後所需要的時間爲 Θ(n)。

解題思路

我們的任務是判斷輸入是否只由題目中所給出的六個單詞組成。這是一個多模式字符串匹配問題,共有六個模式。

現在,首先需要根據所給的模式構造出相應的字符串匹配自動機,如下所示:

  • 狀態集合 Q = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 99 }
  • 初始狀態 q0 = 0
  • 接受狀態集合 A = { 0, 1, 2, 3 }
  • 輸入字母表 Σ = { a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z }
  • 轉移函數 δ 用狀態轉換圖表示如下:    

這個字符串匹配自動機有以下幾個特點:

  • 她是一個多模式的字符串匹配自動機
  • 她有一個特殊的狀態 99,每當該自動機在狀態 q 讀入了輸入字符 a 時,如果在上面的狀態轉換圖中找不到相應的有向邊,就轉移到這個特殊狀態。並且,立即停止該自動機,返回匹配失敗。
  • 她有多個接受狀態,從初始狀態 0 開始連續編號。這是爲了在程序中方便判斷是否匹配。

這個狀態轉換圖中的構造由以下六個模式開始:

one 0 –> 4 –> 5 –> 0*
puton 0 –> 11 –> 12 –> 13 –> 14 –> 0*
in 0 –> 7 -> 1*
out 0 –> 4 –> 6 –> 1*
input 0 –> 7 –> 1* –> 8 –> 9 -> 2*
output 0 –> 4 –> 6 –> 1* –> 8 –> 9 -> 2*

然後,就需要仔細考慮各種狀態之間的轉移關係了。

最後,相應的 C# 程序如下所示:

using System;

namespace Skyiv.Ben.Timus
{
  // http://acm.timus.ru/problem.aspx?space=1&num=1102
  sealed class T1102
  {
    static readonly int[] a = { 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 3, 4, 5, 0, 0, 0, 6, 7, 0, 0, 0, 0, 0 };
    static readonly int[,] delta =
    {
      // *   e   i   n   o   p   t   u        有限自動機的轉移函數
      { 99, 99,  7, 99,  4, 11, 99, 99 }, //  0 接受狀態,初始狀態
      { 99, 99,  7, 99,  4,  8, 99, 99 }, //  1 接受狀態
      { 99, 99,  7, 99, 10, 11, 99, 99 }, //  2 接受狀態
      { 99,  0,  7, 99,  4, 11, 99, 99 }, //  3 接受狀態
      { 99, 99, 99,  5, 99, 99, 99,  6 }, //  4 狀態
      { 99,  0, 99, 99, 99, 99, 99, 99 }, //  5 狀態
      { 99, 99, 99, 99, 99, 99,  1, 99 }, //  6 狀態
      { 99, 99, 99,  1, 99, 99, 99, 99 }, //  7 狀態
      { 99, 99, 99, 99, 99, 99, 99,  9 }, //  8 狀態
      { 99, 99, 99, 99, 99, 99,  2, 99 }, //  9 狀態
      { 99, 99, 99,  3, 99, 99, 99,  6 }, // 10 狀態
      { 99, 99, 99, 99, 99, 99, 99, 12 }, // 11 狀態
      { 99, 99, 99, 99, 99, 99, 13, 99 }, // 12 狀態
      { 99, 99, 99, 99, 14, 99, 99, 99 }, // 13 狀態
      { 99, 99, 99,  0, 99, 99, 99, 99 }, // 14 狀態
    };

    static void Main()
    {
      for (int c, q = 0, n = int.Parse(Console.ReadLine()); n > 0; n--, q = 0)
      {
        while ((c = Console.Read()) != '\n') if (q < 99 && c != '\r') q = delta[q, a[c - 'a']];
        Console.WriteLine((q < 4) ? "YES" : "NO");
      }
    }
  }
}

由於模式中只包括 e、i、n、o、p、t 和 u 這七個字母,數組 a (該數組包括二十六個元素,依次對應二十六個小寫拉丁字母)將輸入字母表映射爲從 0 到 7 的整數,1 到 7 依次對應前面的七個字母,0 對應其它字母。然後,二維數組 delta 表示轉移函數 δ,直接由上面的狀態轉換圖得到。剩下的事情就很簡單了,Main 方法在 for 循環中依次讀入各輸入行,然後在 while 循環中執行這個自動機,之後根據自動機的狀態輸出是否匹配。

進一步的討論

這道題目的輸入規模不超過 107 個字符,時間限制是 1.0 秒,內存限制是 16 MB。我提交的幾個程序的運行時間和內存使用如下表所示:

ID Date Author Problem Language Judgement
result
Execution
time
Memory
used
2612947 19:52:41 20 May 2009 skyivben 1102 C++ Accepted 0.062 121 KB
2612930 19:44:58 20 May 2009 skyivben 1102 C# Accepted 0.125 10 561 KB
2612807 17:17:31 20 May 2009 skyivben 1102 C# Accepted 0.718 857 KB

上表中第三行就是前面的 C# 程序提交的結果。可以看出,這個 C# 程序的運行時間達到了 0.718 秒,已經接近題目的時間限制了。

如果這個題目的時間限制改爲 0.2 秒,那麼我們怎麼辦呢?尋找更高效的字符串匹配算法?

實際上前面的 C# 程序中使用的字符串匹配算法已經非常高效了,幾乎沒有什麼改進的餘地了。這個 C# 程序的瓶頸不在於字符串匹配算法,而在於 I/O,即 Console.Read 方法不夠高效,而該方法需要在內層循環中被調用大約 107 次。只要將 Main 方法用以下程序片段代替:

static void Main()
{
  var s = new byte[10000000 + 100];
  int i = 0, n = Console.OpenStandardInput().Read(s, 0, s.Length);
  while (s[i++] != '\n') ;
  for (int c, q = 0; i < n; q = 0)
  {
    while ((c = s[i++]) != '\n') if (q < 99 && c != '\r') q = delta[q, a[c - 'a']];
    Console.WriteLine((q < 4) ? "YES" : "NO");
  }
}

就可以將運行時間縮短到 0.125 秒,如上表中第二行所示。在這個 C# 程序中,我們調用一次 Stream 類的 Read 方法將所有的輸入讀到的字節數組 s 中,避免了多次調用 Console.Read 方法。因爲輸入僅包含小寫拉丁字母,並不包含漢字等需要兩個字節編碼的字符,所以可以使用字節數組 byte[],而不需要使用字符數組 char[]。但是,內存使用就從原來的 857 KB 上升到 10,561 KB 了。

如果時間限制爲 0.2 秒,內存限制爲 1 MB,那麼又要怎麼辦呢?

只需要將第一個 C# 程序簡單地翻譯爲 C 或者 C++ 程序就行了,用 C/C++ 的 getchar 代替 C# 的 Console.Read 方法。運行時間縮短到 0.062 秒,內存使用降低到 121 KB,如上表中的第一行所示。可見,C/C++ 的 getchar 是非常高效的。實際上,在絕大多數的 C/C++ 實現中,getchar 應該是一個宏,而不是一個函數。

有關這道題目的更多信息,請參見 CSDN 論壇上的一篇貼子:超級鬱悶,4個方法都“Memory limit exceeded”

提到字符串匹配,大多數人都會想起經典的 Knuth-Morris-Pratt 算法。這個由 Donald Knuth (經典名著 The Art of Computer Programming 的作者,著名的電子排版系統 TeX 的研製者)等三人設計的算法是使用有限自動機的單模式匹配算法。它不用計算轉移函數 δ,匹配時間爲 Θ(n),只用到輔助數組 π[1, m],它是在 Θ(m) 的時間內,根據模式預先計算出來的。數組 π 使得我們可以按需要,“現場”有效地計算(在平攤意義上來說)轉移函數 δ。

思考題

上面的第二個 C# 程序中有一個 bug,但是這個 bug 在絕大多數情況下都不會表現出來。所以這個程序能夠 Accepted

親愛的讀者,你能夠找出這個 bug 嗎?

提示,這個 bug 和字符串匹配算法無關,並且第一個 C# 程序中不存在這個 bug 。


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