最近班裏開始玩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這種簡單的遊戲。這句話暗示得很清楚了吧,咱們中篇再見。