《Arithmetic Puzzles》

給定一個由字母組成的等式,其中每一個字母表示一個數字。不同字母表示的數字一定不同。問字母和數字之間是否存在一一對應關係,使得等式成立。若存在多種方案輸出按字母順序排列後字典序最小的解。

比如 SEND+MORE=MONEY 的一個解爲 9567+1085=10652。

解題思路

根據題意我們可以得到下面幾個條件:

  1. 最多隻會有10個數字,所以解的組合數不超過 10!=3,628,800
  2. 最多允許存在10個字母,否則一定是No Solution
  3. 當單詞長度超過1時,其首字母肯定不爲0

一個簡單的解法

首先我們需要將所有字母都找出來,按照字典序排列。再從第一個字母開始按字典序枚舉數字,當給每一個字母分配了數字之後,將其帶入等式,檢查是否相等。

枚舉數字時需要注意有的字母不能等於0,這一步我們可以在預處理中完成,用數組notZero表示該字母是否不能爲0。

那麼可以寫出僞代碼:

dfs(letter):
    startNum = 0
    If (notZero[ letter ]) Then 
        startNum = 1
    End If
    For i = startNum .. 9
        If not useNum[i] Then
            useNum[i] = true
            val[ letter ] = i
            If letter is last letter Then
                If (check(val)) Then
                    Return True
                End If
            Else 
                If (dfs(next letter)) Then
                    Return True
                End If
            End If
            val[ letter ] = -1
            useNum[i] = false
        End If
    Return False
  • val表示每個字母對應的數字,初始化爲-1
  • useNum表示已經使用了的數字,初始爲爲false
  • check(val)表示將現在的val帶入原公式檢查是否合法

由於等式長度最大爲100,有可能出現兩個長度爲49的數字進行比較,顯然無法正常的採用轉化爲整數再進行比較的方法。

對於這樣的情況,我們有如下的解決方案:

首先將等式右邊的單詞全部移到左邊,這樣公式就變成了形如:

word1 + word2 + ... + word4 - word5 - word6 - ... - word8 = 0

舉個例子:SEND+MORE-MONEY=0

將其寫成筆算的形式

+  SEND
+  MORE
- MONEY
-------
      0

接下來我們從最低位開始,一位一位向高位進行計算。假設前一位的進位爲w,則當前位的總和爲當前位所有出現過的字母之和加上w。最低位時,因爲肯定沒有進位,所以w = 0。

每一位的和爲:s = D + E - Y + w

由於我們之前已經枚舉出了所有字母表示的數字,因此我們可以直接得到這個和s

由於我們要使得最後的結果爲0,所以s的末尾一定需要爲0。

因此判定合法的條件爲s % 10 == 0

s滿足末尾爲0,則我們可以繼續向高位計算,新的進位爲 w = s / 10

當計算到最高位時,不能產生進位,還需要額外判斷s = 0

僞代碼爲:

check(val)
    w = 0
    For i = low order .. high order
        s = sigma(digit at order i) + w // val
        If (s % 10 != 0) Then
            Return False
        End If
        If (i == high order && s != 0) Then
            Return False
        End If
        w = s / 10
    End For
    Return True

假設一共出現了m個字母,n種字母。則該算法枚舉組合的時間複雜度爲 O(10! / (10 - n)!) ~=O(10!),檢查是否合法的時間複雜度爲 O(m),總的時間複雜度爲 _O(10!*m)_

對於給定的時間限制肯定是會TLE的,因此我們需要對其進行優化。

優化一

在上面的算法中,我們是按照字母順序以及字典序開始搜索。總是在枚舉完全部的字母后,才進行匹配。然而實際上我們會遇到這樣的情況:

比如DCBA+DCCA-DDDA=0,當我們枚舉出A的值時,已經可以判定等式最後一位是否能夠滿足要求。但是在上面的算法中,我們並沒有這麼做,而是一直在枚舉後面的字母。

因此我們提出一個改進的方法,我們按照從低位到高位的過程中,字母出現的順序去枚舉。這樣做有一個好處和一個壞處:

好處是,能夠在最短時間內檢查出是否合法,而不需要去對整個等式進行檢查。

壞處是,必須要枚舉出所有可能的組合情況,並從中選取字典序最小的情況。

但實際上,由於該方法剪枝的強度比較大,所以對於大多數情況都能夠很好的解決。除了一種特殊的情況,這個我們會在優化二中講到。

該算法的實現要點:

  1. 根據筆算公式

    +  SEND
    +  MORE
    - MONEY
    

    從右到左,從上到下,同時計算ws的值。設最大的位數爲m位,一共有n個單詞。我們用(i,j)來表示當前枚舉到了右起第i位,上起第j個字母。比如在上面例子中(1,1)就是右上角的D(2,3)就是MONEY中的E

  2. 當枚舉到的字母(i,j)尚未賦值時,枚舉它可能出現的值;否則直接使用已經枚舉的值。
  3. j = n + 1時,表示該位所有的字母都已經枚舉完畢,此時計算ws並根據結果,決定是否遞歸計算(i+1,1)
  4. i = m + 1時,表示所有位置都已經枚舉完畢,此時根據進位的w是否等於0,來判定當前解是否合法。

僞代碼實現爲:

dfs(i, j, s):
    If (i == m + 1) Then
        If (s == 0) Then
            UpdateAns()
        End If
        Return 
    End If
    If (j == n + 1) Then
        If (s % 10 == 0) Then
            dfs(i + 1, 1, s / 10) // 直接將w加入s
        Else
            Return 
        End If
    End If
    letter = getLetter(i, j)
    If (val[ letter ] != -1) Then
        dfs(i, j + 1, s + val[ letter ] * op[j])
    Else
        startNum = 0
        If (notZero[ letter ]) Then 
            startNum = 1
        End If
        For i = startNum .. 9
            If not useNum[i] Then
                useNum[i] = true
                val[ letter ] = i
                dfs(i, j + 1, s + val[ letter ] * op[j])
                val[ letter ] = -1
                useNum[i] = false
            End If
        End If
    End If
優化二

上面優化搜索順序之後的代碼,能夠通過絕大部分的測試點,但是仍然有一種情況沒有辦法做到。當碰到下面這種形式時,就會超時:

A+B+C+D+E+F+G+HJJJJJJJJJJJJJJ=A+B+C+D+E+F+G+IJJJJJJJJJJJJJJ

超時的原因是因爲其中8個字母都在最低位出現了,所以全部進行了枚舉。並且只有當計算到最高位時才能確定這兩個數是否相等。

對於這種數據,我們仔細觀察會發現其中ABCDEFGJ的值實際上無論等於多少都可以,需要注意的只有HI的值。

舉一個簡單的例子:A+B+C=AB

由於等式左右兩邊都有在個位的B,所以B的取值並不會對計算個位的s產生影響。對於這樣的字母我們稱之爲無用的字母

對於上面那個會超時的例子,我們可以發現,除了HI以外全部都是無用的字母。枚舉他們的值完全是沒有必要的,我們只在一個字母出現,並且有用時纔去枚舉它的值。

因此我們先做一次預處理,將所有無用的字母都標記出,在枚舉時直接跳過。

當然這樣做還有一個問題:某一種字母每一次出現都是無用的字母,那麼當我們找到合法答案時,該字母並沒有賦值。

此時我們將剩餘的數字按照最小序依次賦值給它們即可。

因此原僞代碼改爲:

dfs(i, j, s):
    If (i == m + 1) Then
        If (s == 0) Then
            fillAns()   // 賦值剩餘的數字
            UpdateAns()
        End If
        Return 
    End If
    If (j == n + 1) Then
        If (s % 10 == 0) Then
            dfs(i + 1, 1, s / 10) // 直接將w加入s
        Else
            Return 
        End If
    End If
    letter = getLetter(i, j) // 若該字母爲無用的字母,則返回-1
    If (letter == -1) Then  // 處理無用的字母
        dfs(i, j + 1, s)
        Return ;
    End
    If (val[ letter ] != -1) Then
        dfs(i, j + 1, s + val[ letter ] * op[j])
    Else
        startNum = 0
        If (notZero[ letter ]) Then 
            startNum = 1
        End If
        For i = startNum .. 9
            If not useNum[i] Then
                useNum[i] = true
                val[ letter ] = i
                dfs(i, j + 1, s + val[ letter ] * op[j])
                val[ letter ] = -1
                useNum[i] = false
            End If
        End If
    End If

加上第二種優化之後,就解決特殊情況,也就能夠順利地通過所有的測試點了。

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