玩玩24點(上)

最近班裏開始玩24點了。原由是一個在計算器上兩人比賽24點的程序,但計算器判斷一組數據是否有解須要15秒,因而這個程序就沒有斷定有解這一功能。ios

這麼慢的速度我固然看不下去,但去優化那個BASIC程序是不可能的,我就開始寫本身的24點程序。正好以前的算法課中遞歸一章提到過24點,我就理所固然地用開始寫遞歸求解算法。算法

 

第一個版本是一個運行在PC上的很是複雜的C++程序,用上了十多個頭文件。因爲它太爛了,我把它註釋掉之後又刪掉了。這個程序最後算出1820組數據中有1362組有解數據,與網上查到的數字是一致的,不過算得很慢,要二十幾秒。編程

這個時候個人想法仍是單片機上的程序經過標準庫隨機數函數產生數據而後跑一遍求解算法。因而我就把這個程序修改了一下,標準庫容器換成數組替換掉了,排序就隨便寫了個冒泡。PC上全部數據遍歷一遍須要十幾秒。數組

  1 #include <iostream>
  2 #include <cstdint>
  3 
  4 using Integer = std::uint16_t;
  5 
  6 template <typename T>
  7 inline void swap(T& lhs, T& rhs)
  8 {
  9     auto temp = lhs;
 10     lhs = rhs;
 11     rhs = temp;
 12 }
 13 
 14 template <typename I>
 15 inline void sort(I begin, I end)
 16 {
 17     for (auto pass_end = end - 1; pass_end != begin; --pass_end)
 18     {
 19         bool changed = false;
 20         for (auto iter = begin; iter != pass_end; ++iter)
 21             if (*(iter + 1) < *iter)
 22             {
 23                 swap(*iter, *(iter + 1));
 24                 changed = true;
 25             }
 26         if (!changed)
 27             break;
 28     }
 29 }
 30 
 31 int divide_count = 0, modulo_count = 0;
 32 
 33 class Rational
 34 {
 35 public:
 36     Integer num, den;
 37     Rational(Integer num = 0, Integer den = 1)
 38         : num(num), den(den)
 39     {
 40         // make every object reduced
 41         reduce();
 42     }
 43     Rational& operator=(Integer i)
 44     {
 45         num = i;
 46         den = 1;
 47         return *this;
 48     }
 49     Rational operator+(const Rational& rhs) const
 50     {
 51         // assume it won't overflow
 52         return Rational(num * rhs.den + rhs.num * den, den * rhs.den);
 53     }
 54     Rational operator-(const Rational& rhs) const
 55     {
 56         // assume *this >= rhs
 57         return Rational(num * rhs.den - rhs.num * den, den * rhs.den);
 58     }
 59     Rational operator*(const Rational& rhs) const
 60     {
 61         return Rational(num * rhs.num, den * rhs.den);
 62     }
 63     Rational operator/(const Rational& rhs) const
 64     {
 65         // assume rhs != 0
 66         return Rational(num * rhs.den, den * rhs.num);
 67     }
 68     bool operator==(const Rational& rhs) const
 69     {
 70         return num == rhs.num && den == rhs.den;
 71     }
 72     bool operator==(Integer rhs) const
 73     {
 74         return num == rhs && den == 1;
 75     }
 76     bool operator<(const Rational& rhs) const
 77     {
 78         return num * rhs.den < rhs.num * den;
 79     }
 80     explicit operator bool()
 81     {
 82         return num;
 83     }
 84 private:
 85     void reduce()
 86     {
 87         if (num == 1 || den == 1)
 88             return;
 89         if (num == 0)
 90         {
 91             den = 1;
 92             return;
 93         }
 94         Integer gcd = 1;
 95         auto a = num, b = den;
 96         while (1)
 97         {
 98             if (a == 0 || a == b)
 99             {
100                 gcd = b;
101                 break;
102             }
103             if (b == 0)
104             {
105                 gcd = a;
106                 break;
107             }
108             if (a > b)
109             {
110                 ++modulo_count;
111                 a %= b;
112             }
113             else
114             {
115                 ++modulo_count;
116                 b %= a;
117             }
118         }
119         if (gcd > 1)
120         {
121             divide_count += 2;
122             num /= gcd;
123             den /= gcd;
124         }
125     }
126 };
127 
128 template <typename S>
129 S& operator<<(S& lhs, const Rational& rhs)
130 {
131     lhs << rhs.num;
132     if (rhs.den > 1)
133         lhs << '/' << rhs.den;
134     return lhs;
135 }
136 
137 struct Expression
138 {
139     Expression() = default;
140     Expression(const Rational& lhs, char op, const Rational& rhs,
141         const Rational& res)
142         : lhs(lhs), rhs(rhs), res(res), op(op) { }
143     char op = ' ';
144     Rational lhs, rhs, res;
145 };
146 
147 template <typename S>
148 S& operator<<(S& lhs, const Expression& rhs)
149 {
150     lhs << rhs.lhs << ' ' << rhs.op << ' ' << rhs.rhs << " = " << rhs.res;
151     return lhs;
152 }
153 
154 constexpr Integer target = 24;
155 constexpr Integer max_count = 4;
156 
157 bool solve(Integer count, const Rational* data, Expression* expr)
158 {
159     // assume data is ordered
160     if (count == 1)
161         return *data == target;
162     auto end = data + count;
163     auto before_end = end - 1;
164     --count;
165     Rational new_data[max_count - 1];
166     auto new_end = new_data + count;
167     for (auto lhs = data; lhs != before_end; ++lhs)
168         for (auto rhs = lhs + 1; rhs != end; ++rhs)
169         {
170             auto dst = new_data;
171             for (auto src = data; src != end; ++src)
172                 if (src != lhs && src != rhs)
173                     *dst++ = *src;
174             *dst = *lhs + *rhs;
175             Expression temp(*lhs, '+', *rhs, *dst);
176             sort(new_data, new_end);
177             if (solve(count, new_data, expr + 1))
178             {
179                 *expr = temp;
180                 return true;
181             }
182         }
183     for (auto lhs = data + 1; lhs != end; ++lhs)
184         for (auto rhs = data; rhs != lhs; ++rhs)
185         {
186             auto dst = new_data;
187             for (auto src = data; src != end; ++src)
188                 if (src != lhs && src != rhs)
189                     *dst++ = *src;
190             *dst = *lhs - *rhs;
191             Expression temp(*lhs, '-', *rhs, *dst);
192             sort(new_data, new_end);
193             if (solve(count, new_data, expr + 1))
194             {
195                 *expr = temp;
196                 return true;
197             }
198         }
199     for (auto lhs = data; lhs != before_end; ++lhs)
200         for (auto rhs = lhs + 1; rhs != end; ++rhs)
201         {
202             auto dst = new_data;
203             for (auto src = data; src != end; ++src)
204                 if (src != lhs && src != rhs)
205                     *dst++ = *src;
206             *dst = *lhs * *rhs;
207             Expression temp(*lhs, '*', *rhs, *dst);
208             sort(new_data, new_end);
209             if (solve(count, new_data, expr + 1))
210             {
211                 *expr = temp;
212                 return true;
213             }
214         }
215     for (auto lhs = data; lhs != end; ++lhs)
216         for (auto rhs = data; rhs != end; ++rhs)
217         {
218             if (lhs == rhs || *rhs == Rational(0))
219                 continue;
220             auto dst = new_data;
221             for (auto src = data; src != end; ++src)
222                 if (src != lhs && src != rhs)
223                     *dst++ = *src;
224             *dst = *lhs / *rhs;
225             Expression temp(*lhs, '/', *rhs, *dst);
226             sort(new_data, new_end);
227             if (solve(count, new_data, expr + 1))
228             {
229                 *expr = temp;
230                 return true;
231             }
232         }
233     return false;
234 }
235 
236 bool test(Integer a, Integer b, Integer c, Integer d)
237 {
238     Rational data[6];
239     Expression expr[3];
240     data[0] = a;
241     data[1] = b;
242     data[2] = c;
243     data[3] = d;
244     std::cout << a << ", " << b << ", " << c << ", " << d
245         << ':' << std::endl;
246     bool solved = solve(4, data, expr);
247     if (solved)
248         for (const auto& e : expr)
249             std::cout << '\t' << e << std::endl;
250     else
251         std::cout << "\tno solution" << std::endl;
252     return solved;
253 }
254 
255 int main()
256 {
257     int count = 0;
258     constexpr Integer max_num = 13;
259     for (int a = 1; a <= max_num; ++a)
260         for (int b = a; b <= max_num; ++b)
261             for (int c = b; c <= max_num; ++c)
262                 for (int d = c; d <= max_num; ++d)
263                     if (test(a, b, c, d))
264                         ++count;
265     std::cout << count << ' ' << divide_count << ' '
266         << modulo_count << std::endl;
267 
268     test(1, 3, 4, 6);
269     test(1, 4, 5, 6);
270     test(1, 5, 5, 5);
271     test(1, 6, 11, 13);
272     test(2, 2, 11, 11);
273     test(2, 2, 13, 13);
274     test(2, 7, 7, 10);
275     test(3, 3, 7, 7);
276     test(3, 3, 8, 8);
277     test(3, 7, 9, 13);
278     test(4, 4, 7, 7);
279     test(5, 5, 7, 11);
280 }

稍微解釋一下這個程序。先定義了Integer類型別名,原本應該是uint8_t,意思是單片機是8位字長的,但流輸入輸出中會被當成char處理,而我想要的是整數,就換成了uint16_t。swap和sort用於替換標準庫中同名函數,後者是冒泡排序,反正待排序的元素至多4個。而後是Rational類,表示非負有理數,自動約分,用的是展轉相除。因爲單片機算除法和取模極慢,我把特殊狀況排除掉了,相似於剪枝的思想。另有divide_count和modulo_count兩變量用於統計除法和取模次數。Expression類表示表達式,24點的解法由3個表達式組成。以及Rational類和Expression類的流插入運算符重載。框架

遞歸求解函數參數有3個:要求解的數字個數count、輸入數據,data以及表達式存放位置expr。函數假設輸入數據已經有序。遞歸出口爲count == 1,檢查惟一的數據是否爲24。其餘狀況下,程序會選兩個數相加、相減、相乘、相除(排除除數爲0的狀況),把這兩個數從序列中刪除再加入新的運算結果,而後排序傳給遞歸子程序。若是子程序返回true,就把這一步運算存入*expr。ide

這裏還有一個小插曲。寫完這個程序後,第一次運行的結果是有1320多組有解,然而答案應該是1362。我試着debug,但遞歸函數至關難debug,我只能在遞歸中寫輸出語句,最後發現是排序算法出了問題。那麼簡單的冒泡排序我固然不會寫錯,問題在於*(iter + 1) < *iter這句,一開始寫的是*iter > *(iter + 1),然而Rational類並無重載operator>,實際調用的是兩個operator bool。我把比較方向換了過來,又在operator bool前加了explicit關鍵字以防萬一,結果就正確了。這個故事告訴咱們重載關係運算符要麼乖乖所有寫出來,要麼寫using namespace std::rel_ops。函數

 

而後移植到了AVR單片機上(沒有C++標準庫,這就是爲何前一個程序刻意避免了那麼好用的標準庫),開發板用的是寫系列教程那塊。輸入是硬編碼,輸出是幾個LED,3組數據各跑100遍,經過LED和秒錶測運行速度。運行結果是300遍有解的數據求解一共用了15秒,其中最後一組大約用了一半時間。我有充足理由估計對無解的數據跑一遍求解須要至少100毫秒。優化

若是我要保證提供給用戶的數據有解,必定會出現200ms以上的延遲,就算不用保證,也至少要100毫秒計算時間,我認爲這是不能接受的。因此這個求解算法太慢了。爲了用戶體驗,須要一種更快的算法,然而我並無辦法把求解算法優化掉一個量級。ui

那惟一的方法就是不求解了。等等,不求解?那數據怎麼來?要手寫數據?仍是要模板元編程把數據在編譯器就存起來?this

想多了。讓PC端程序數據必定格式的數據做爲單片機程序的代碼,而後直接讀取便可。因爲數據量比較大,單片機2KB內存放不下,必須放在flash中(就算數據量不大放在RAM中也是浪費)。數據長成這樣:

 1 #ifndef DATA_H
 2 #define DATA_H
 3 
 4 #include <avr/pgmspace.h>
 5 
 6 const uint8_t valid_data[][5] PROGMEM =
 7 {
 8      17, 129,  32, 130, 117,
 9      17, 177,  32,  98, 180,
10      17, 193,  32, 146, 117,
11      17, 209,  32,  75, 180,
12      17,  98,  32, 130, 117,
13     // ...
14     // 1362 lines in all
15     // ...
16     219, 221,  32, 130, 109,
17     204, 204,  32, 130, 109,
18     204, 220,  32,  75, 149,
19     204, 221,  32, 130, 109,
20     220, 221,  32, 122, 172,
21 };
22 
23 const uint8_t invalid_data[][2] PROGMEM =
24 {
25      17,  17,
26      17,  33,
27      17,  49,
28      17,  65,
29      17,  81,
30     // ...
31     // 458 lines in all
32     // ...
33     186, 187,
34     186, 221,
35     187, 187,
36     187, 221,
37     221, 221,
38 };
39 
40 #endif

數據格式是我一拍腦殼定的:前兩字節的低4位、高4位分別是從小到大的4個數字;後三字節是3個表達式,最低3位爲LHS下標,中間2位爲運算符,最高3位爲RHS下標。運算符域從0到3分別是加減乘除,LHS和RHS的下標定義爲依次存放輸入數字和中間結果的數組中相等元素的下標;對於無解的數據,只有前兩個字節。

這樣單片機就無需對輸入數據求解,只需根據壓縮成字節碼的表達式復原出原來的表達式,涉及到少許分數運算而已。實驗證實這樣的算法是足夠快的,至少沒有超過16毫秒的顯示屏刷新時間。

 

單片機端的程序框架無非是定時器中斷中更新顯示,其餘如硬件驅動等函數都是現成的。程序用到兩個按鍵,分別用於切換刷新狀態與顯示答案。左邊的按鍵按一下開始刷新數據,再按一下中止刷新,顯示一組數據;對於有解的數據,右邊的按鍵按第一下會顯示最後一行,第二下會顯示完整答案;對於無解的數據,按右邊的按鍵會顯示「no solution」。

我之前玩的24點都是1~10的數字,此次是真實模擬撲克牌環境的A~K。10這個數字若是正常顯示須要2位,不美觀,所以我用畫點和畫線的操做組合出了在一個字符空間內畫10的操做。

爲了增長可玩性,我還加入了無解的數據,機率大約爲1/6。一羣人圍着一道題想了半分鐘後發現是「no solution」是最爽的事。

 

實際上這個24點程序還遠不完美。單片機常常在屏幕上輸出詭異的解法,好比10 * 12 = 120,120 / 5 = 24,這些是不符合人類計算邏輯的,正常人想到的都是10 / 5 = 2,2 * 12 = 24。一個可行的方法是把遞歸搜索的順序換一下,先減再加,先除後乘,在除法中優先用最大的數除以最小的數。但仍是會出現12 / 5 = 12/5,12/5 * 10 = 24這樣的式子,最根本的算法仍是根據表達式創建樹,在樹上調整順序。也許4個數算24點的狀況不須要這麼複雜,但這是萬能的、具備可擴展性的作法(也有多是我想多了)。

 

點一下,玩一年。24點這麼好玩,我確定不能止步於4個1~13的數加減乘除算出1個24這種簡單的遊戲。這句話暗示得很清楚了吧,咱們中篇再見。

相關文章
相關標籤/搜索