給定一個由字母組成的等式,其中每一個字母表示一個數字。不同字母表示的數字一定不同。問字母和數字之間是否存在一一對應關係,使得等式成立。若存在多種方案輸出按字母順序排列後字典序最小的解。
比如 SEND+MORE=MONEY 的一個解爲 9567+1085=10652。
解題思路
根據題意我們可以得到下面幾個條件:
- 最多隻會有10個數字,所以解的組合數不超過 10!=3,628,800
- 最多允許存在10個字母,否則一定是
No Solution
- 當單詞長度超過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
的值時,已經可以判定等式最後一位是否能夠滿足要求。但是在上面的算法中,我們並沒有這麼做,而是一直在枚舉後面的字母。
因此我們提出一個改進的方法,我們按照從低位到高位的過程中,字母出現的順序去枚舉。這樣做有一個好處和一個壞處:
好處是,能夠在最短時間內檢查出是否合法,而不需要去對整個等式進行檢查。
壞處是,必須要枚舉出所有可能的組合情況,並從中選取字典序最小的情況。
但實際上,由於該方法剪枝的強度比較大,所以對於大多數情況都能夠很好的解決。除了一種特殊的情況,這個我們會在優化二中講到。
該算法的實現要點:
-
根據筆算公式
+ SEND + MORE - MONEY
從右到左,從上到下,同時計算
w
和s
的值。設最大的位數爲m
位,一共有n
個單詞。我們用(i,j)
來表示當前枚舉到了右起第i
位,上起第j
個字母。比如在上面例子中(1,1)
就是右上角的D
,(2,3)
就是MONEY
中的E
。 - 當枚舉到的字母
(i,j)
尚未賦值時,枚舉它可能出現的值;否則直接使用已經枚舉的值。 - 當
j = n + 1
時,表示該位所有的字母都已經枚舉完畢,此時計算w
和s
並根據結果,決定是否遞歸計算(i+1,1)
。 - 當
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
的值實際上無論等於多少都可以,需要注意的只有H
和I
的值。
舉一個簡單的例子:A+B+C=AB
由於等式左右兩邊都有在個位的B
,所以B
的取值並不會對計算個位的s
產生影響。對於這樣的字母我們稱之爲無用的字母。
對於上面那個會超時的例子,我們可以發現,除了H
和I
以外全部都是無用的字母。枚舉他們的值完全是沒有必要的,我們只在一個字母出現,並且有用時纔去枚舉它的值。
因此我們先做一次預處理,將所有無用的字母都標記出,在枚舉時直接跳過。
當然這樣做還有一個問題:某一種字母每一次出現都是無用的字母,那麼當我們找到合法答案時,該字母並沒有賦值。
此時我們將剩餘的數字按照最小序依次賦值給它們即可。
因此原僞代碼改爲:
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
加上第二種優化之後,就解決特殊情況,也就能夠順利地通過所有的測試點了。