什么是24點游戲
24點游戲,英文叫做24 game,是對給定的4個非負整數進行加減乘除運算,要求每個數都要被用到且僅用到一次,並得到最終的運算結果為24。比如3、8、3、8這四個數,可以找出唯一的一個解是8/(3-8/3)。
本程序的實現特點
1、采用降階法實現,不再局限於輸入4個數,也不局限於求值為24。
2、僅用整數運算,而不用浮點運算。
3、求解去重處理。
關於求解去重
24點游戲的編程求解的基本思路本質上就是遍歷的思路,把每種運算組合都計算出其結果,所有最終結果為24的運算組合就是全體的解。但是單純遍歷出來的解往往會有大量的重復解。比如用1、2、3、4這四個數求24,1*2*3*4是一個解,按乘法交換律和結合律由這個解可以衍生出很多類似的解,如:2*1*3*4,1*2*(3*4),1*(2*3)*4,2*3*4*1,2*3*4/1,等等。還有很多其它類型的重復解。后面再細說。總的來說,對求解做去重處理是為了盡快求解並簡潔地展現出來。本程序相當比重的設計和邏輯實現都是用於應對去重這個任務的。
基礎數據結構設計
SFraction
1 struct SFraction 2 { 3 u64 numerator; 4 u64 denominator; 5 bool bNegative; 6 7 SFraction() 8 { 9 reset(); 10 } 11 void reset() 12 { 13 numerator = 0; 14 denominator = 1; 15 bNegative = false; 16 } 17 void setValue(u32 uiVal) 18 { 19 numerator = (u64)uiVal; 20 denominator = 1; 21 bNegative = false; 22 } 23 bool isZero() const 24 { 25 return (numerator == 0); 26 } 27 bool isOne() const 28 { 29 return (numerator != 0 && !bNegative && numerator == denominator); 30 } 31 bool operator==(const SFraction& oVal) const 32 { 33 SFraction oResult = fractAdd(*this, minusByZero(oVal)); 34 return (oResult.numerator == 0); 35 } 36 bool operator<(const SFraction& oVal) const 37 { 38 SFraction oResult = fractAdd(*this, minusByZero(oVal)); 39 return (oResult.numerator != 0 && oResult.bNegative); 40 } 41 bool operator>(const SFraction& oVal) const 42 { 43 SFraction oResult = fractAdd(*this, minusByZero(oVal)); 44 return (oResult.numerator != 0 && !oResult.bNegative); 45 } 46 };
這里,表示分數的數據結構 SFraction 沿用了 用C++實現的有理數(分數)四則混合運算計算器 里的定義,相應增加了本程序需要的 isZero、isOne、operator==、operator<、operator> 等方法。分數加法函數 fractAdd、分數乘法函數 fractMultiply、求相反數、求倒數以及求最大公約數、最小公倍數等輔助函數全部沿用 用C++實現的有理數(分數)四則混合運算計算器 里的定義和實現。
EnumOp
enum EnumOp{E_OP_NONE, E_OP_ADD, E_OP_R_SUBST, E_OP_MULTIPLE, E_OP_DIVIDE, E_OP_R_DIVIDE};
EnumOp 定義了5種二元運算符:加、反減、乘、除、反除。
對於除法運算,這里設置了兩個運算符,是基於兩個因素的考慮:
(1)a / b 通常並不等於 b / a。
(2)為了方便去重處理,本程序內部表達運算數 a 和 b 的二元運算時,總是把值小的數放在左邊,值大的數放在右邊。即:左數 <= 右數。
假設 a 小於 b,那么有如下對應關系:
(1)a + b 和 b + a 都對應 a 加 b
(2)b - a 對應 a 反減 b
(3)a * b 和 b * a 都對應 a 乘 b
(4)b / a 對應 a 除 b
(5)a / b 對應 a 反除 b
這里的反除就是數學中的稱謂“除以”。另外還需要說明一下:減法運算為什么不設置兩個運算符,即只需考慮大值減小值的情形,而忽略小值減大值出現負數的情形。這是因為參與運算的原始輸入數值限定為非負整數,而最終要求得到的運算結果也是一個非負整數。
用幾個例子說明:
以1、3、26 求 24 為例,1-3+26 是一個解,即 -2 加上 26 得 24,但是這種情形總有對應的大值減小值的解,即 26-(3-1),就是說一個負數加一個大正數等於這個大正數減去負數的相反數。
類似地,考慮由1、3、22 求 24,那么 22-(1-3) 是一個解,但對這個解稍作變形就得到不會出現負數的解,即 22+(3-1)。
另外兩個負數相乘或相除得到一個正數的情形,同樣有對應的避免出現負數的變形,比如 (1-3)*(7-19) 可變形為 (3-1)*(19-7),(7-10)*(1/(30-38)) 可變形為 (10-7)*(1/(38-30)) 。
EnumOp 還定義了用於表示沒有二元運算的符號 E_OP_NONE,其作用隨后就會看到。
SExpress
1 struct SExpress 2 { 3 SFraction oVal; 4 std::string strExpress; 5 EnumOp eOp; 6 SFraction oL, oR; 7 8 SExpress() 9 { 10 eOp = E_OP_NONE; 11 } 12 };
SExpress 是一個表達式的表示結構,且有如下語義:
1、每個參與二元運算的原始輸入數值,都是一個葉子表達式,分量 SFraction oVal 是表達式自身的數值,其 EnumOp eOp 分量取值為 E_OP_NONE,以標定該表達式節點為葉子節點
2、對兩個葉子表達式(記為 a 和 b)施予前述的某一個二元運算符(記為 ~)可以得到一個新的表達式(記為 N),這個新的表達式的 SFraction oL, oR 分量分別記錄兩個參與運算的表達式的數值,且滿足 oL <= oR;新表達式的 EnumOp eOp 分量則記錄對應的二元運算符。可以概述為 a 和 b 按 ~ 聚合得到 N,對應的純符號表示式為:a~b=N
3、對任意兩個表達式施予前述的某一個二元運算符可以得到一個新的表達式,這個過程稱作一次聚合(0 作除數的情形除外)
4、為區別於葉子表達式,由聚合得到的新表達式統稱為枝表達式
5、所有給定的葉子表達式都參與且僅參與一次聚合,以及中間產生的枝表達式也都參與且僅參與一次聚合,最終總會得到一個根表達式
6、分量 std::string strExpress 以字符串形式記錄對應的表達式是如何由葉子表達式聚合而成的
本程序的核心任務可以表達為:對所有給定的葉子表達式,遍歷出各種聚合序列,對每種聚合序列得到的根表達式求值,判斷是否和指定的值相等。
主函數實現
1 int main(int argc, char* argv[]) 2 { 3 printf(" 24 game pro version 1.0 by Read Alps\n\n"); 4 printf(" Please input the target non-negative integer to make [default 24]:"); 5 std::string strInput; 6 getline(std::cin, strInput); 7 u32 uiVal = 0; 8 if (!shiftToU32(strInput, uiVal)) 9 { 10 printf(" Invalid input; use 24 instead.\n"); 11 uiVal = 24; 12 } 13 CBackwardCalcor::sm_oAnswer.setValue(uiVal); 14 15 CLog::instance(); 16 printf(" Do you want to log details in workflow.log? [y/n, default n]:"); 17 getline(std::cin, strInput); 18 if (strInput == "y") 19 { 20 CLog::instance()->setLogPathAndName(getMainPath().c_str(), "workflow.log"); 21 CLog::instance()->setLogLevel(LOG_DBG); 22 } 23 24 while (true) 25 { 26 CBackwardCalcor::sm_setResult.clear(); 27 printf("\n Please input several (at most 6) non-negative integers separated by comma to make %u or q to quit:", uiVal); 28 getline(std::cin, strInput); 29 trimString(strInput); 30 if (strInput == "q") 31 break; 32 std::vector<std::string> vecStr; 33 if (std::string::npos != strInput.find(",")) 34 divideString2Vec(strInput, ",", vecStr); 35 if (isIntStrVec(vecStr) == 0) 36 { 37 printf(" Invalid input.\n\n"); 38 continue; 39 } 40 if (vecStr.size() > 6) 41 { 42 printf("At most 6 integers.\n"); 43 continue; 44 } 45 u32 uiStart = (u32)GetTickCount(); 46 CBackwardCalcor oCalc(vecStr); 47 oCalc.exec(); 48 if (CBackwardCalcor::sm_setResult.empty()) 49 { 50 printf(" From %s, there is no solution to make %u by using +-*/ and ().\n", strInput.c_str(), uiVal); 51 printf(" Used time(ms): %u.\n", GetTickCount() - uiStart); 52 continue; 53 } 54 int nCount = 1; 55 for (std::set<std::string>::const_iterator it = CBackwardCalcor::sm_setResult.begin(); it != CBackwardCalcor::sm_setResult.end(); ++it) 56 printf(" %d. %s\n", nCount++, it->c_str()); 57 printf(" Used time(ms): %u.\n", GetTickCount() - uiStart); 58 } // end of main loop
59 printf("\n");
60 return 0;
61 }
main 函數實現一個交互式console程序,進入主循環之前有兩次交互輸入,指定要計算的目標數值以及是否記錄詳細日志。
進入到主循環里,讓輸入一組逗號隔開的非負整數,隨后的如下語句是把輸入的字符串分解到一個數字字符串動態數組里:
32 std::vector<std::string> vecStr; 33 if (std::string::npos != strInput.find(",")) 34 divideString2Vec(strInput, ",", vecStr);
然后以這個動態數組為參數實例化一個 CBackwardCalcor 對象,並調用該對象的 exec 方法進行求解,最后輸出求解結果,重新開始循環。
Linux下運行示例
[root@localhost bin]# ./24GamePro
24 game pro version 1.0 by Read Alps
Please input the target non-negative integer to make [default 24]:
Do you want to log details in workflow.log? [y/n, default n]:
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:3,4,5,6
1. (3+5-4)*6
Used time(ms): 2.
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:1,2,3,4
1. (1+3)*(2+4)
2. 1*2*3*4
3. 4*(1+2+3)
Used time(ms): 1.
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:8,3,8,3
1. 8/(3-8/3)
Used time(ms): 0.
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:56,67,78,89
From 56,67,78,89, there is no solution to make 24 by using +-*/ and ().
Used time(ms): 2.
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:23,45,67,89,34
1. (23+45)/34+89-67
Used time(ms): 62.
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:23,45,67,89,34,66
1. (23+45-66)*(34+67-89)
2. (89-67)*45/(34-23)-66
3. 23+34-66*67/(45+89)
4. 23+45*66/(67-34)-89
5. 66/(45+89)+34/67+23
6. 67+89-66*(23+45)/34
Used time(ms): 3018.
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:q
[root@localhost bin]#
CBackwardCalcor 類接口與成員
1 class CBackwardCalcor 2 { 3 public: 4 CBackwardCalcor(const std::vector<std::string>& vecStr); 5 CBackwardCalcor(const std::vector<SExpress>& vecExp); 6 void exec(); 7 8 private: 9 void workOnLevel(size_t seq1, size_t seq2); 10 void stepOver(size_t& seq1, size_t& seq2); 11 void reduct(EnumOp eOp, size_t seq1, size_t seq2, const std::vector<SExpress>& vecLeft); 12 void doBiOp(EnumOp eOp, const SExpress& oExp1, const SExpress& oExp2, SExpress& oReduct, bool bNoLeft); 13 bool isRedundant(EnumOp eOpL, EnumOp eOpR, EnumOp eOp); 14 bool isRedundantEx(const SExpress& oExp1, const SExpress& oExp2, EnumOp eOp); 15 16 void setAscendingOrder(); 17 bool higherLevel(size_t seq1, size_t seq2); 18 bool topLevel(size_t seq1, size_t seq2); 19 20 std::vector<SExpress> m_vecExpress; 21 22 /// 頭兩個參與運算的數作為當前水位 23 SFraction m_oCurValL; // 左數 24 SFraction m_oCurValR; // 右數 25 26 public: 27 static SFraction sm_oAnswer; 28 static std::set<std::string> sm_setResult; 29 30 private: 31 CBackwardCalcor(){}; 32 };
CBackwardCalcor 類里有兩個靜態分量:sm_oAnswer 和 sm_setResult,前者存放交互指定的要計算的目標數值,后者存放求解的集合。
CBackwardCalcor 類的兩個構造函數
1 CBackwardCalcor::CBackwardCalcor(const std::vector<std::string>& vecStr) 2 { 3 SExpress oExp; 4 for (size_t idx = 0; idx < vecStr.size(); idx++) 5 { 6 oExp.oVal.setValue(strtoul(vecStr[idx].c_str(), 0, 10)); 7 oExp.strExpress = vecStr[idx]; 8 m_vecExpress.push_back(oExp); 9 } 10 } 11 12 CBackwardCalcor::CBackwardCalcor(const std::vector<SExpress>& vecExp) 13 { 14 m_vecExpress = vecExp; 15 }
main 函數里的 CBackwardCalcor oCalc(vecStr) 語句實例化一個 CBackwardCalcor 對象,並調用第一個構造函數把輸入的一組字符串數值轉化為一組葉子表達式,存放到成員 m_vecExpress 里。
第二個構造函數后面會用到。
CBackwardCalcor::exec 接口實現
1 void CBackwardCalcor::exec() 2 { 3 setAscendingOrder(); 4 5 if (CLog::instance()->meetLogCond(LOG_DBG)) 6 { 7 std::string str = "Dealing:"; 8 for (size_t idx = 0; idx < m_vecExpress.size(); idx++) 9 str += (" " + m_vecExpress[idx].strExpress); 10 CLog::LogMsgS(LOG_DBG, str.c_str()); 11 } 12 13 if (m_vecExpress.size() == 1) 14 { 15 if (m_vecExpress[0].oVal == sm_oAnswer) 16 sm_setResult.insert(m_vecExpress[0].strExpress); 17 return; 18 } 19 if (sm_oAnswer.isZero() && m_vecExpress[m_vecExpress.size() - 1].oVal.isZero()) 20 { 21 std::string str = "0"; 22 for (size_t idx = 1; idx < m_vecExpress.size(); idx++) 23 str += "+0"; 24 sm_setResult.insert(str); 25 return; 26 } 27 28 size_t seq1 = 0, seq2 = 1; 29 while (seq1 + 1 < m_vecExpress.size()) 30 { 31 if (higherLevel(seq1, seq2)) 32 { 33 m_oCurValL = m_vecExpress[seq1].oVal; 34 m_oCurValR = m_vecExpress[seq2].oVal; 35 workOnLevel(seq1, seq2); 36 if (topLevel(seq1, seq2)) 37 break; 38 } 39 stepOver(seq1, seq2); 40 } 41 }
CBackwardCalcor::exec 的實現代碼分為四個部分。第一部分是調用 setAscendingOrder 接口對 m_vecExpress 里的表達式按數值從小到大進行排序。
setAscendingOrder 接口的實現代碼如下:
1 void CBackwardCalcor::setAscendingOrder() 2 { 3 for (size_t idx = 1; idx < m_vecExpress.size(); idx++){ 4 for (size_t preidx = 0; preidx < idx; preidx++){ 5 if (m_vecExpress[idx].oVal < m_vecExpress[preidx].oVal){ 6 SExpress oLow = m_vecExpress[idx]; 7 for (size_t mvidx = idx; mvidx > preidx; --mvidx) 8 m_vecExpress[mvidx] = m_vecExpress[mvidx - 1]; 9 m_vecExpress[preidx] = oLow; 10 } 11 } 12 } 13 }
CBackwardCalcor::exec 實現代碼的第二部分是記錄日志。
第三部分是對兩種特殊情形的處理:
1、輸入參與運算的數值只有一個的情形,直接判斷該數值和指定要計算的目標數值是否相等即可完成求解。
2、由若干個0求0的情形,只取連加形式的一種解。
其實第二種情形有很多種解,比如等式 0+0+0+0=0,其中任意一個加號可以換成減號或乘號,依然會保持等式成立。但鑒於數值 0 的特殊性,本程序只取連加形式的一種解,其它解都當作重復解而不予展示。
第四部分代碼是 exec 接口的主體邏輯實現,以下結合一個示例進行闡述。
示例:由 2、2、2、3四個數求 24。
(1)取第一和第二兩個數,即2和2
a)做加法運算,得到4,接下來降階成由4、2、3求24
b)做減法運算,得到0,接下來降階成由0、2、3求24
c)做乘法運算,得到4,接下來降階成由4、2、3求24
d)做除法運算,得到1,接下來降階成由1、2、3求24
e)做反除法運算,得到1,接下來降階成由1、2、3求24
(2)取第一和第三兩個數,即2和2
a)做加法運算,得到4,接下來降階成由4、2、3求24
b)做減法運算,得到0,接下來降階成由0、2、3求24
c)做乘法運算,得到4,接下來降階成由4、2、3求24
d)做除法運算,得到1,接下來降階成由1、2、3求24
e)做反除法運算,得到1,接下來降階成由1、2、3求24
(3)取第一和第四兩個數,即2和3
a)做加法運算,得到5,接下來降階成由5、2、3求24
b)做減法運算,得到1,接下來降階成由1、2、2求24
c)做乘法運算,得到6,接下來降階成由6、2、2求24
d)做除法運算,得到3/2,接下來降階成由3/2、2、2求24
e)做反除法運算,得到2/3,接下來降階成由2/3、2、2求24
再往后,還有:
(4)取第二和第三兩個數,即2和2
(5)取第二和第四兩個數,即2和3
(6)取第三和第四兩個數,即2和3
照這樣遍歷下來,確實可以確保把所有的解求出來。但是,從過程看,很多步驟完全是多余的,比如(2)完全和(1)重復;(4)和(1)重復;(5)、(6)和(3)重復。
現在來看 CBackwardCalcor::exec 的第四部分代碼是如何實現上述求解邏輯並避免上述重復步驟的:
28 size_t seq1 = 0, seq2 = 1;
29 while (seq1 + 1 < m_vecExpress.size()) 30 { 31 if (higherLevel(seq1, seq2)) 32 { 33 m_oCurValL = m_vecExpress[seq1].oVal; 34 m_oCurValR = m_vecExpress[seq2].oVal; 35 workOnLevel(seq1, seq2); 36 if (topLevel(seq1, seq2)) 37 break; 38 } 39 stepOver(seq1, seq2); 40 }
兩個局部變量 seq1 和 seq2 分別記錄首先參與聚合運算的左右兩個表達式在表達式數組里所對應的下標,初始值分別為 0 和 1,即考慮讓數值最小的兩個表達式先做聚合運算的情形;while 循環體內有對這兩個下標變量做步進處理的接口,該接口實現如下:
1 void CBackwardCalcor::stepOver(size_t& seq1, size_t& seq2) 2 { 3 if (seq2 + 1 < m_vecExpress.size()) 4 { 5 ++seq2; 6 return; 7 } 8 ++seq1; 9 seq2 = seq1 + 1; 10 }
但正如上述示例所示,單是對這兩個下標變量做步進遍歷可能會碰到很多完全多余的運算。為解決這個問題,在 CBackwardCalcor 里引入了兩個水位成員:
/// 頭兩個參與運算的數作為當前水位 SFraction m_oCurValL; // 左數 SFraction m_oCurValR; // 右數
水位就是一個有序值對,而求解的過程則按水位由低到高的總體趨勢被分解成若干個子過程。上述示例里,子過程(1)的水位是(2,2);子過程(2)的水位還是(2,2);子過程(3)的水位是(2,3);子過程(4)的水位是(2,2);子過程(5)和(6)的水位都是(2,3)。由這個示例可以看到,在步進過程中,水位有可能由高變低,但總體趨勢是由低到高的。
CBackwardCalcor 實例化一個對象時,其分量m_oCurValL 和 m_oCurValR 都被初始化為數值0,就是說初始設定的水位是(0,0)。higherLevel 接口的實現如下:
1 bool CBackwardCalcor::higherLevel(size_t seq1, size_t seq2) 2 { 3 return (m_vecExpress[seq1].oVal > m_oCurValL ? true : m_vecExpress[seq2].oVal > m_oCurValR); 4 }
if (higherLevel(seq1, seq2))條件體內的那幾行代碼的邏輯是:更新當前水位;調用 workOnLevel 接口在當前水位進行求解;判斷當前水位是否為最高水位,是則退出循環,求解完成。概言之:只有水位升高時 workOnLevel 接口才會被調用。上述示例中的6個子過程,只有(1)和(3)會被執行,其它的都被當成重復過程直接跳過。
topLevel 接口的實現如下:
1 bool CBackwardCalcor::topLevel(size_t seq1, size_t seq2) 2 { 3 size_t total = m_vecExpress.size(); 4 return (m_vecExpress[seq1].oVal == m_vecExpress[total - 2].oVal && m_vecExpress[seq2].oVal == m_vecExpress[total - 1].oVal); 5 }
所謂最高水位,就是輸入的一組數值經由小到大排序后排在最后的兩個數值構成的有序值對。
這里還有一個疑問需要消除:因為限定了只能輸入非負整數,所能輸入的一組數值的最低水位是(0,0),而一個CBackwardCalcor 對象實例化的初始水位就是(0,0),而且只有水位升高時 workOnLevel 接口才會被調用,那么由 0、0、3、8求24,水位(0,0)對應的子過程會被當成重復過程跳過,這樣是否合理?
為說明這么處理是合理的,考察一般性的情形,即由 0、0、k1、k2、……、kn 這 n+2 個非負整數求非負整數 k 的情形。0 和 0 做加減乘(除非法)的二元聚合運算,只能得到 0,說明所考察的情形等價於由 0、k1、k2、……、kn 這 n+1 個非負整數求非負整數 k 的情形。任取后者的一個解,記為 [...] = k,[...] 為 一個復合表達式,從結構上而言是以 0、k1、k2、……、kn 這 n+1 個數為葉子節點的一棵二叉樹。對這個二叉樹的根和新添的 葉子節點 0 做加法運算,就得到一棵新的二叉樹,它以0、0、k1、k2、……、kn 這 n+2 個數為葉子節點。就是說,[...] + 0 = k 是所考察情形的一個解。
上述結論說明,當水位為 (0,0) 時,跳過這個水位對應的子過程是合理的,因為 每個帶有0與0聚合的解都會有對應的不帶0與0聚合的解。例如:0+0+3*8 = 0+(0+3*8)。
當然有一種情況是例外的,即一般情形中 k1=k2=...=kn=k=0,即由 n+2 個 0 求 0 的情形,其最低水位和最高水位都是 (0,0),就是說求求解過程只有一個子過程,直接跳過這個子過程,就會誤判為無解。所以在 exec 接口的代碼實現的第二部分提前應對了這種例外情況。
CBackwardCalcor::workOnLevel 接口實現
workOnLevel 的實現分為三部分,具體如下:
1 void CBackwardCalcor::workOnLevel(size_t seq1, size_t seq2) 2 { 3 std::vector<SExpress> vecLeft; 4 for (size_t idx = 0; idx < m_vecExpress.size(); ++idx) 5 { 6 if (idx != seq1 && idx != seq2) 7 vecLeft.push_back(m_vecExpress[idx]); 8 } 9 10 if (CLog::instance()->meetLogCond(LOG_DBG)) 11 { 12 std::string str = "no pending nodes"; 13 if (!vecLeft.empty()) 14 { 15 str = u32ToStr(vecLeft.size()) + " pending node(s):"; 16 for (size_t idx = 0; idx < vecLeft.size(); idx++) 17 str += (" " + vecLeft[idx].strExpress); 18 } 19 CLog::LogMsg(LOG_DBG, "cur two: L=%s, R=%s with %s", m_vecExpress[seq1].strExpress.c_str(), m_vecExpress[seq2].strExpress.c_str(), str.c_str()); 20 } 21 22 reduct(E_OP_ADD, seq1, seq2, vecLeft); 23 reduct(E_OP_MULTIPLE, seq1, seq2, vecLeft); 24 reduct(E_OP_R_SUBST, seq1, seq2, vecLeft); 25 reduct( E_OP_DIVIDE, seq1, seq2, vecLeft); 26 reduct(E_OP_R_DIVIDE, seq1, seq2, vecLeft); 27 }
第一部分是由 m_vecExpress組建一個新的動態數組 vecLeft,存放當前水位下首先參與聚合運算的兩個表達式之外的其余表達式,這個數組稱作剩余表達式數組。
第二部分是記日志。
第三部分是按二元運算可能的運算符逐個去調用聚合接口 reduct。這里的5個 reduct 調用正好對應前述示例中的5個子子過程:a)、b)、c)、d)、e)。
為清晰起見,把對整體求解過程中按水位划分出的一級子過程簡稱為水位子過程,用(1)、(2)、……等標記;而把在一個水位子進程內部按當前水位的左數和右數的不同聚合方式又划分出的5個二級子過程簡稱為聚合子過程,用a)、b)、……等標記。
運算去重策略細則
本程序實現的去重策略主要包括兩大類:一類是水位去重策略,上面已有詳細闡述;另一類是運算去重策略,這一類又可概括為如下兩個子類(以下說明均假設 a、b、c、d、p、q、r 為非負整數,且滿足 p <= q),各有若干細則。
單次運算去重策略:
【單1】舍 q+p,因為有等價的 p+q
【單2】舍 q*p,因為有等價的 p*q
前文說過約束參與運算的左數不大於右數。因而對於加法運算,按這個約束,就可以只取 p+q,而丟棄等價的 q+p。乘法運算的情形類似。
【單3】舍 b-0,因為有等價的 0+b
【單4】舍 0/b,因為有等價的 0*b
【單5】舍 b/1,因為有等價的 1*b
這三個細則可概括為:能用加則不用減,能用乘則不用除。
關聯運算去重策略:
對多於一次的運算,即關聯運算,定義若干約束細則,以達到所需的去重效果。具體有:
【關1】舍 p-q
減法運算丟棄出現負數的情形,即只取 q-p,而丟棄 p-q。當然這不是因為 p-q 等價於 q-p,而是因為所能涉及到的三種關聯運算情形(參見前文部分的說明)都有等價的避免出現負值的等價關聯運算,如 r-(p-q) 等價於 r+q-p,而后者不會出現負數。
【關2】舍 a-b-c,因為有等價的 a-(b+c)
【關3】舍 a-(b-c),因為有等價的 a+c-b
【關4】舍 a+(b-c),因為有等價的 a+b-c
【關5】舍 a-b+c,因為有等價的 a+c-b
【關6】舍 a/b/c,因為有等價的 a/(b*c)
【關7】舍 a/(b/c),因為有等價的 a*c/b
【關8】舍 a*(b/c),因為有等價的 a*b/c
【關9】舍a/b*c,因為有等價的 a*c/b
這六個細則可概括為:多用加少用減,多用乘少用除;先加后減,先乘后除。
【關10】舍 (a+b)+(c+d),因為有等價的 a+(b+(c+d))
【關11】舍 (a*b)*(c*d),因為有等價的 a*(b*(c*d))
這兩個細則可概括為:去除分組加和分組乘。
CBackwardCalcor::reduct 接口實現
reduct 的實現分為兩個部分:去重處理和聚合處理。
1 void CBackwardCalcor::reduct(EnumOp eOp, size_t seq1, size_t seq2, const std::vector<SExpress>& vecLeft) 2 { 3 SExpress& oExp1 = m_vecExpress[seq1]; 4 SExpress& oExp2 = m_vecExpress[seq2]; 5 if (eOp == E_OP_R_SUBST && oExp1.oVal.isZero()) // curb b-0, only use 0+b 6 return; 7 if ((eOp == E_OP_DIVIDE || eOp == E_OP_R_DIVIDE) && oExp1.oVal.isZero()) 8 return; 9 if (eOp == E_OP_DIVIDE && oExp1.oVal.isOne()) // curb b/1, only use 1*b 10 return; 11 if (eOp == E_OP_R_DIVIDE && oExp1.oVal == oExp2.oVal) 12 return; 13 if (isRedundant(oExp1.eOp, oExp2.eOp, eOp)) 14 { 15 CLog::LogMsg(LOG_DBG, "base curbed: L=%s, R=%s, Op=%d.", oExp1.strExpress.c_str(), oExp2.strExpress.c_str(), (int)eOp); 16 return; 17 } 18 if (isRedundantEx(oExp1, oExp2, eOp)) 19 { 20 CLog::LogMsg(LOG_DBG, "ex curbed: L=%s, R=%s, Op=%d.", oExp1.strExpress.c_str(), oExp2.strExpress.c_str(), (int)eOp); 21 return; 22 } 23 24 SExpress oReduce; 25 doBiOp(eOp, oExp1, oExp2, oReduce, vecLeft.empty()); 26 if (vecLeft.empty()) 27 { 28 if (CBackwardCalcor::sm_oAnswer == oReduce.oVal) 29 { 30 if (CLog::instance()->meetLogCond(LOG_DBG)) 31 { 32 CLog::LogMsg(LOG_DBG, "solution: L=%s,R=%s,op=%d => %s", oExp1.strExpress.c_str(), oExp2.strExpress.c_str(), eOp, oReduce.strExpress.c_str()); 33 if (CBackwardCalcor::sm_setResult.find(oReduce.strExpress) != CBackwardCalcor::sm_setResult.end()) 34 { 35 CLog::LogMsg(LOG_DBG, "solution: met a repeated solution %s", oReduce.strExpress.c_str()); 36 } 37 } 38 CBackwardCalcor::sm_setResult.insert(oReduce.strExpress); 39 } 40 return; 41 } 42 std::vector<SExpress> vecNew = vecLeft; 43 vecNew.push_back(oReduce); 44 CBackwardCalcor oNew(vecNew); 45 oNew.exec(); 46 }
exec 接口里是對一級子過程做去重處理,而這里是對二級子過程做去重處理。逐個說明如下:
5 if (eOp == E_OP_R_SUBST && oExp1.oVal.isZero()) // curb b-0, only use 0+b
6 return;
0 和數值 b 做反減運算,即 b-0,由細則【單3】,被當作重復的聚合子過程而跳過。
7 if ((eOp == E_OP_DIVIDE || eOp == E_OP_R_DIVIDE) && oExp1.oVal.isZero())
8 return;
0 做除數(b/0)以及 0做被除數(0/b)時,對應的聚合子過程都跳過。前者是非法情形,后者與 0*b重復,對應細則【單4】。
9 if (eOp == E_OP_DIVIDE && oExp1.oVal.isOne()) // curb b/1, only use 1*b
10 return;
b/1 的情形當成重復被跳過,因為與 1*b 等價,對應細則【單5】。
11 if (eOp == E_OP_R_DIVIDE && oExp1.oVal == oExp2.oVal)
12 return;
a、b 兩數不等時,需要分別嘗試 a/b 和 b/a 兩種聚合子過程。a=b 時,只需考慮其中之一,另一個當成重復被跳過。
13 if (isRedundant(oExp1.eOp, oExp2.eOp, eOp))
... 18 if (isRedundantEx(oExp1, oExp2, eOp)) ...
isRedundant 和 isRedundantEx 這兩個接口應對前后聚合關聯相關的去重處理,放到后面再說。
現在來看 reduct 接口的聚合處理邏輯:
24 SExpress oReduce;
25 doBiOp(eOp, oExp1, oExp2, oReduce, vecLeft.empty());
這兩條語句實施左表達式 oExp1 和右表達式 oExp2 的二元聚合運算(由 eOp 指定),聚合后的結果放到局部變量 oReduce 中。doBiOp 接口的實現后面再說。
26 if (vecLeft.empty())
27 { 28 if (CBackwardCalcor::sm_oAnswer == oReduce.oVal) 29 { 30 if (CLog::instance()->meetLogCond(LOG_DBG)) 31 { 32 CLog::LogMsg(LOG_DBG, "solution: L=%s,R=%s,op=%d => %s", oExp1.strExpress.c_str(), oExp2.strExpress.c_str(), eOp, oReduce.strExpress.c_str()); 33 if (CBackwardCalcor::sm_setResult.find(oReduce.strExpress) != CBackwardCalcor::sm_setResult.end()) 34 { 35 CLog::LogMsg(LOG_DBG, "solution: met a repeated solution %s", oReduce.strExpress.c_str()); 36 } 37 } 38 CBackwardCalcor::sm_setResult.insert(oReduce.strExpress); 39 } 40 return; 41 }
這幾行代碼的邏輯是如果剩余表達式數組 vecLeft 為空,就說明這時的 oReduce 是根表達式,其取值如果等於指定要計算的目標數值,就找到了一個解,記日志並把這個解加入到解集中。
42 std::vector<SExpress> vecNew = vecLeft;
43 vecNew.push_back(oReduce); 44 CBackwardCalcor oNew(vecNew); 45 oNew.exec();
最后這幾行代碼是應對 vecLeft 不為空的情形,用 oReduce 和 vecLeft 拼成 vecNew,以 vecNew 為參數實例化一個 CBackwardCalcor 對象(使用第二個構造函數),並調用該對象的 exec 接口對降階的情形繼續求解。
isRedundant 和 isRedundantEx
isRedundant 接口實現前述的【關2】至【關11】等細則,具體如下:
1 bool CBackwardCalcor::isRedundant(EnumOp eOpL, EnumOp eOpR, EnumOp eOp) 2 { 3 /// curb a-b-c, a-(b-c), a+(b-c) and a-b+c 4 if (eOp == E_OP_R_SUBST || eOp == E_OP_ADD) 5 { 6 if (eOpL == E_OP_R_SUBST || eOpR == E_OP_R_SUBST) 7 return true; 8 } 9 /// curb a/b/c, a/(b/c), a*(b/c) and a/b*c 10 if (eOp == E_OP_MULTIPLE || IS_DIVIDE(eOp)) 11 { 12 if (IS_DIVIDE(eOpL) || IS_DIVIDE(eOpR)) 13 return true; 14 } 15 /// curb (a+b)+(c+d) 16 if (eOp == E_OP_ADD && eOpL == E_OP_ADD && eOpR == E_OP_ADD) 17 return true; 18 /// curb (a*b)*(c*d) 19 if (eOp == E_OP_MULTIPLE && eOpL == E_OP_MULTIPLE && eOpR == E_OP_MULTIPLE) 20 return true; 21 22 return false; 23 }
isRedundantEx 接口針對連乘和連加的情形做去重處理。實現代碼如下:
1 bool CBackwardCalcor::isRedundantEx(const SExpress& oExp1, const SExpress& oExp2, EnumOp eOp) 2 { 3 if (eOp == E_OP_MULTIPLE) // consider a*b*c 4 { 5 if (oExp1.eOp != E_OP_MULTIPLE && oExp2.eOp != E_OP_MULTIPLE) 6 return false; 7 return (oExp2.eOp != E_OP_MULTIPLE ? true : oExp1.oVal > oExp2.oL); 8 } 9 if (eOp == E_OP_ADD) // consider a+b+c 10 { 11 if (oExp1.eOp != E_OP_ADD && oExp2.eOp != E_OP_ADD) 12 return false; 13 return (oExp2.eOp != E_OP_ADD ? true : oExp1.oVal > oExp2.oL); 14 } 15 return false; 16 }
連乘的情形,以 2、3、4 求 24 為例,把2、3、4這三個數連乘便得到24,所以2*3*4 是一個解,按乘法的交換律和結合律,可以衍生出很多類似的解,如:
4*4*(2*3),2*4*3,3*(2*4),3*2*4,4*(3*2),3*4*2,2*(3*4),4*2*3,4*3*2,...
本程序的處理策略是只留下 2*3*4 這一個解,而其它衍生解當成重復解全被忽略掉。擴展到更一般的情形,就是:如果 k1*k2*...*kn=k,且有 k1 <= k2 <= ... <= kn,則 k1*k2*...*kn 是由 k1、k2、...、kn 求 k 的一個解,而由 k1*k2*...*kn 憑據乘法交換律和結合律衍生出的其它解一律當成重復解被忽略不計。
isRedundantEx 接口里如下三行代碼便是應對連乘的去重處理的:
5 if (oExp1.eOp != E_OP_MULTIPLE && oExp2.eOp != E_OP_MULTIPLE)
6 return false; 7 return (oExp2.eOp != E_OP_MULTIPLE ? true : oExp1.oVal > oExp2.oL);
走到這段邏輯的情境是當時正要對左表達式 oExp1 和 右表達式 oExp2 做乘法運算,如果 oExp1 與 oExp2 的 eOp 分量都不是 E_OP_MULTIPLE,即不與當前要做的乘法運算構成連乘,這種情形不判為重復;否則就確定碰到了連乘的情形,連乘又分為如下三種情形(oExp1 部分高亮為黃色,oExp2 部分高亮為藍色):
1、p*(q*r)
2、(p*q)*r
3、(p*q)*(r*s)
情形3在這里不會碰到,因為在 isRedundant 接口里由【關11】細則就會提前判為重復。
情形2因滿足 oExp2.eOp != E_OP_MULTIPLE 而直接判為重復。
情形1判為重復的條件是 oExp1.oVal > oExp2.oL(即 p > q)。
概括來說就是:p、q、r 連乘時,只選取其中最大的兩個數先乘的情形,其余的情形全部判為重復。
由 1、 2、3、4 求 24——以這個例子來看一下大值先乘的策略從效果上要好於小值先乘的策略:
大值先乘策略:
1、3*4
2、2*(3*4) // 去除 2、3、4 三數連乘的其它組合形式
3、1*(2*(3*4)) // 去除 1、2、(3*4) 三數連乘的其它組合形式
對第3步得到的解去括號便是1*2*3*4
小值先乘策略:
1、1*2
2、(1*2)*3 // 去除 1、2、3 三數連乘的其它組合形式
3、4*((1*2)*3) // 去除 4、(1*2)、3 三數連乘的其它組合形式
對第3步得到的解去括號是4*1*2*3
對比不難發現,大值先乘策略總能保證去括號后參與連乘的數由小到大排列;而小值先乘策略則不能保證(例子中第2步1、2、3連乘得積為6,大於最后參與乘法運算的4,按前面的【單2】細則,4要放到乘法的左邊)。
連加去重的情形和連乘的情形類似, 采用的是大值先加策略。
CBackwardCalcor::doBiOp
doBiOp 接口實現如下:
1 void CBackwardCalcor::doBiOp(EnumOp eOp, const SExpress& oExp1, const SExpress& oExp2, SExpress& oReduct, bool bNoLeft) 2 { 3 oReduct.eOp = eOp; 4 oReduct.oL = oExp1.oVal; 5 oReduct.oR = oExp2.oVal; 6 std::string strHead = (bNoLeft ? "" : "("); 7 std::string strTail = (bNoLeft ? "" : ")"); 8 switch (eOp) 9 { 10 case E_OP_ADD: 11 case E_OP_MULTIPLE: 12 oReduct.oVal = (eOp == E_OP_ADD ? fractAdd(oExp1.oVal, oExp2.oVal) : fractMultiply(oExp1.oVal, oExp2.oVal)); 13 if (oExp1.oVal == oExp2.oVal && oExp1.strExpress > oExp2.strExpress) 14 oReduct.strExpress = putTogether(oExp2, oExp1, eOp, strHead, strTail); 15 else 16 oReduct.strExpress = putTogether(oExp1, oExp2, eOp, strHead, strTail); 17 break; 18 case E_OP_R_SUBST: 19 case E_OP_DIVIDE: 20 oReduct.oVal = (eOp == E_OP_R_SUBST ? fractAdd(oExp2.oVal, minusByZero(oExp1.oVal)) : 21 fractMultiply(oExp2.oVal, reciprocal(oExp1.oVal))); 22 if (oExp1.oVal == oExp2.oVal && oExp1.strExpress > oExp2.strExpress) 23 oReduct.strExpress = putTogether(oExp1, oExp2, eOp, strHead, strTail); 24 else 25 oReduct.strExpress = putTogether(oExp2, oExp1, eOp, strHead, strTail); 26 break; 27 case E_OP_R_DIVIDE: 28 default: 29 oReduct.oVal = fractMultiply(oExp1.oVal, reciprocal(oExp2.oVal)); 30 oReduct.strExpress = putTogether(oExp1, oExp2, eOp, strHead, strTail); 31 break; 32 } 33 }
該接口實施左表達式 oExp1 和右表達式 oExp2 的二元聚合運算 eOp,聚合結果由輸出參數 oReduct 帶出,其中 putTogether 是個輔助函數,用來拼接聚合表達式的字符串表示式。當 oExp1 和 oExp2 數值相等時,則比較它們的 strExpress 分量的大小來決定拼接聚合表達式字符串時誰是左數誰是右數。當 eOp 為 E_OP_R_DIVIDE 時,在 doBiOp 里不會出現 oExp1.oVal 等於 oExp2.oVal 的情形,因為在前述的 reduct 接口里這種情形被提前判為重復而不會走到 doBiOp 里。
putTogether 函數實現如下:
1 std::string putTogether(const SExpress& oExpL, const SExpress& oExpR, EnumOp eOp, 2 const std::string& strHead, const std::string& strTail) 3 { 4 std::string strL = oExpL.strExpress, strR = oExpR.strExpress; 5 if (eOp == E_OP_ADD) 6 { 7 if (oExpL.strExpress[0] == '(') 8 strL = oExpL.strExpress.substr(1, oExpL.strExpress.length() - 2); 9 if (oExpR.strExpress[0] == '(') 10 strR = oExpR.strExpress.substr(1, oExpR.strExpress.length() - 2); 11 } 12 else if (eOp == E_OP_R_SUBST) 13 { 14 if (oExpL.strExpress[0] == '(') 15 strL = oExpL.strExpress.substr(1, oExpL.strExpress.length() - 2); 16 if (IS_MULTI_OR_DIVE(oExpR.eOp)) 17 strR = oExpR.strExpress.substr(1, oExpR.strExpress.length() - 2); 18 } 19 else if (eOp == E_OP_MULTIPLE) 20 { 21 if (IS_MULTI_OR_DIVE(oExpL.eOp)) 22 strL = oExpL.strExpress.substr(1, oExpL.strExpress.length() - 2); 23 if (IS_MULTI_OR_DIVE(oExpR.eOp)) 24 strR = oExpR.strExpress.substr(1, oExpR.strExpress.length() - 2); 25 } 26 else // divide 27 { 28 if (IS_MULTI_OR_DIVE(oExpL.eOp)) 29 strL = oExpL.strExpress.substr(1, oExpL.strExpress.length() - 2); 30 } 31 32 return strHead + strL + getOpString(eOp) + strR + strTail; 33 }
新拼接出的表達式,如果不是根節點,其 strExpress 分量是帶左右括號的:左括號在頭部,右括號在尾部。在拼接新的表達式時,參與聚合的子節點為非葉子節點,則根據關聯的運算符之間的優先級規則對子節點做去括號處理。比如由 a、b、c、d 求 e,以下是一些可能的聚合例子:
a 和 (b+c) 做加法聚合得到 (a+b+c)
(a+b) 和 (c-d) 做乘法聚合得到 (a+b)*(c-d)
(a*b) 和 c 做除法聚合,分兩種情形:(a*b) 做被除數,c 做除數,則得到 (a*b/c);反過來,c 做被除數的情形,則得到 (c/(a*b))
關於去重處理的補充說明
最早引入去重處理的說法是說去除重復的解。但是上面文字表述過的去重處理策略實際上是廣義的去重處理,即盡可能早地做去重處理,而不是等聚合到了根節點再做去重處理,這樣的好處是可以減少大量無謂的運算,以便更快地求解,即便是無解的情形也能更快地得出結論。
但是狹義的去重處理邏輯也還是要有的,前面貼了對應的代碼實現,只是沒有做文字表述。就是 reduct 接口的如下代碼段:
30 if (CLog::instance()->meetLogCond(LOG_DBG))
31 { 32 CLog::LogMsg(LOG_DBG, "solution: L=%s,R=%s,op=%d => %s", oExp1.strExpress.c_str(), oExp2.strExpress.c_str(), (int)eOp, oReduce.strExpress.c_str()); 33 if (CBackwardCalcor::sm_setResult.find(oReduce.strExpress) != CBackwardCalcor::sm_setResult.end()) 34 { 35 CLog::LogMsg(LOG_DBG, "solution: met a repeated solution %s", oReduce.strExpress.c_str()); 36 } 37 } 38 CBackwardCalcor::sm_setResult.insert(oReduce.strExpress);
更准確地說,就是這個代碼段的最后一條語句(其余語句只是記日志相關邏輯),靜態成員 sm_setResult 的類型定義成 std::set<std::string> 就是借助 std::set 的特性而實現狹義的去重處理邏輯。
下面以一個實例來說明狹義的去重處理邏輯的必要性,下圖是兩次用 2、6、4、8 求 24的實際效果演示,頭一次記日志,第二次不記日志:
兩次得出的求解集是一樣的,但用時差別顯著,記日志的情況下用了 78 毫秒,不記日志時則為 0 毫秒。
日志文件叫做 workflow.log,日志1529行,97KB,記錄了由 2、6、4、8 求 24 的完整求解過程。 大致過一下頭11行:
1 2020-09-30 20:55:07.867 [2] Dealing: 2 4 6 8 2 2020-09-30 20:55:07.867 [2] cur two: L=2, R=4 with 2 pending node(s): 6 8 3 2020-09-30 20:55:07.867 [2] Dealing: 6 (2+4) 8 4 2020-09-30 20:55:07.867 [2] cur two: L=6, R=(2+4) with 1 pending node(s): 8 5 2020-09-30 20:55:07.867 [2] ex curbed: L=6, R=(2+4), Op=1. 6 2020-09-30 20:55:07.867 [2] Dealing: 8 ((2+4)*6) 7 2020-09-30 20:55:07.867 [2] cur two: L=8, R=((2+4)*6) with no pending nodes 8 2020-09-30 20:55:07.867 [2] ex curbed: L=8, R=((2+4)*6), Op=3. 9 2020-09-30 20:55:07.867 [2] Dealing: (6-(2+4)) 8 10 2020-09-30 20:55:07.868 [2] cur two: L=(6-(2+4)), R=8 with no pending nodes 11 2020-09-30 20:55:07.868 [2] base curbed: L=(6-(2+4)), R=8, Op=1.
第 1 行對應第一個 CBackwardCalcor 實例,對應根任務:由 2、4、6、8 求 24;
第 2 行表示根任務的當前水位為 L=2, R=4,即最低水位,要對最小的兩個數 2 和 4 做聚合運算,6 和 8 進剩余表達式數組;
第 3 行,Dealing: 6 (2+4) 8,表示根任務對 2 和 4 做了加法聚合運算,得到 (2+4),然后實例化了第二個 CBackwardCalcor 對象,執行子任務1:由 (2+4) 與剩余的 6 和 8 求 24;
第 4 行,子任務1的當前水位為 L=6, R=(2+4),8 進剩余表達式數組;
第 5 行,ex curbed: L=6, R=(2+4), Op=1,表示子任務1要對 6 和 (2+4) 做加法運算,這是連加的情形,由於不符合前述的大數先加策略,視為重復被跳過;
第 6 行,Dealing: 8 ((2+4)*6),表示子任務1對 (2+4) 和 6 做了乘法運算,得到 ((2+4)*6),然后實例化了第三個 CBackwardCalcor 對象,執行子任務1-1:由 ((2+4)*6) 與剩余的 8 求 24;
第 7 行,子任務1-1的當前水位(也是唯一水位)為 L=8, R=((2+4)*6),剩余表達式數組為空;
第 8 行,ex curbed: L=8, R=((2+4)*6), Op=3,表示子任務1-1要對 8 和 ((2+4)*6) 做乘法運算,這是連乘的情形,由於不符合前述的大數先乘策略,視為重復被跳過;
子任務1-1會對 8 和 ((2+4)*6) 實施其它二元運算,但由於 8+36、36-8、8/36、36/8 都不等於 24,所以子任務1-1里沒有解。
第 9 行,Dealing: (6-(2+4)) 8,表示子任務1對 (2+4) 和 6 做了減法運算,得到 (6-(2+4)),然后實例化了第四個 CBackwardCalcor 對象,執行子任務1-2:由 (6-(2+4)) 與剩余的 8 求 24;
第 10 行,子任務1-2的當前水位(也是唯一水位)為 L=(6-(2+4)), R=8,剩余表達式數組為空;
第 11 行,base curbed: L=(6-(2+4)), R=8, Op=1,表示子任務1-2對 (6-(2+4)) 和 8 做加法運算,不符合先加后減策略,視為重復被跳過。
現在來看日志文件里記錄到的狹義去重的信息,在日志文件里查找關鍵字 repeated,存在如下兩處:
1054 2020-09-30 20:55:07.899 [2] solution: met a repeated solution 4*8-(2+6)
...
1339 2020-09-30 20:55:07.899 [2] solution: met a repeated solution 6*8/(4-2)
具體看一下第一個重復解是如何發生的。在日志文件里查找關鍵字 4*8-(2+6),找到第一次求得這個解的相關日志,如下:
233 2020-09-30 20:55:07.875 [2] cur two: L=2, R=6 with 2 pending node(s): 4 8 234 2020-09-30 20:55:07.875 [2] Dealing: 4 8 (2+6) 235 2020-09-30 20:55:07.875 [2] cur two: L=4, R=8 with 1 pending node(s): (2+6) 236 2020-09-30 20:55:07.875 [2] Dealing: (2+6) (4+8) 237 2020-09-30 20:55:07.875 [2] cur two: L=(2+6), R=(4+8) with no pending nodes 238 2020-09-30 20:55:07.875 [2] base curbed: L=(2+6), R=(4+8), Op=1. 239 2020-09-30 20:55:07.875 [2] Dealing: (2+6) (4*8) 240 2020-09-30 20:55:07.875 [2] cur two: L=(2+6), R=(4*8) with no pending nodes 241 2020-09-30 20:55:07.875 [2] ex curbed: L=(2+6), R=(4*8), Op=1. 242 2020-09-30 20:55:07.875 [2] ex curbed: L=(2+6), R=(4*8), Op=3. 243 2020-09-30 20:55:07.875 [2] solution: L=(2+6),R=(4*8),op=2 => 4*8-(2+6)
由匹配的第243行日志,往前找到根任務當時對應的水位是 L=2, R=6,在這個水位,先對 2 和 6 做加法運算,得到 (2+6);然后對剩余的 4 和 8 做乘法運算,得到 (4*8);最后再對 (2+6) 和 (4*8) 做反減運算,得到 4*8-(2+6) 這個解。
繼續在日志文件里往后查找 4*8-(2+6):
1053 2020-09-30 20:55:07.899 [2] solution: L=(2+6),R=(4*8),op=2 => 4*8-(2+6) 1054 2020-09-30 20:55:07.899 [2] solution: met a repeated solution 4*8-(2+6)
在日志文件里往前查找 with 2 pending:
986 2020-09-30 20:55:07.897 [2] cur two: L=4, R=8 with 2 pending node(s): 2 6
這說明第二次求得 4*8-(2+6) 這個解時,當時根任務所在的水位是 L=4, R=8,在這個水位,先對 4 和 8 做乘法運算,得到 (4*8);然后對剩余的 2 和 6 做加法運算,得到 (2+6);最后再對 (2+6) 和 (4*8) 做反減運算,再一次得到 4*8-(2+6) 這個解。
日志記錄相關代碼實現
頭文件log.h
1 #ifndef LOG_MGR_H 2 #define LOG_MGR_H 3 4 #include "common.h" 5 #include <stdio.h> 6 7 enum EnumLogLevel 8 { 9 LOG_ERR = 0, 10 LOG_WRN = 1, 11 LOG_DBG = 2 12 }; 13 14 class CLog 15 { 16 public: 17 void setLogPathAndName(const char* pPath, const char* pszName); 18 static void LogMsg(u8 ucLevel, char* pszFmt, ...); 19 static void LogMsgS(u8 ucLevel, const char* pszMsg); 20 u8 getLogLevel() 21 { 22 return m_ucLevel; 23 } 24 void setLogLevel(u8 ucLevel) 25 { 26 if (ucLevel >= LOG_ERR && ucLevel <= LOG_DBG) 27 m_ucLevel = ucLevel; 28 } 29 bool meetLogCond(u8 ucLevel) 30 { 31 return (NULL != m_pFile && ucLevel <= m_ucLevel); 32 } 33 34 static CLog* instance() 35 { 36 if (NULL == sm_poInstance) 37 sm_poInstance = new CLog(); 38 return sm_poInstance; 39 } 40 41 private: 42 CLog() 43 { 44 m_pFile = NULL; 45 m_ucLevel = LOG_ERR; 46 } 47 void LogMsgInner(u8 ucLevel, const char* pszMsg); 48 49 private: 50 std::string m_strAbsFile; 51 u8 m_ucLevel; 52 FILE* m_pFile; 53 54 static CLog* sm_poInstance; 55 }; 56 57 #endif
實現文件log.cpp
1 #include "common.h" 2 #include "log.h" 3 #include <stdarg.h> 4 #include <ctime> 5 #include <sys/timeb.h> 6 using namespace std; 7 8 CLog* CLog::sm_poInstance = NULL; 9 10 void CLog::setLogPathAndName(const char* pPath, const char* pszName) 11 { 12 m_strAbsFile = std::string(pPath) + pszName; 13 m_pFile = fopen(m_strAbsFile.c_str(), "a+"); 14 } 15 16 void CLog::LogMsg(u8 ucLevel, char* pszFmt, ...) 17 { 18 if (!CLog::instance()->meetLogCond(ucLevel)) 19 return; 20 21 std::string strMsg; 22 size_t uiLen = 0; 23 { 24 va_list va; 25 va_start(va, pszFmt); 26 #ifdef _WINDOWS 27 uiLen = _vsnprintf(NULL, 0, pszFmt, va); 28 #else 29 uiLen = vsnprintf(NULL, 0, pszFmt, va); 30 #endif 31 va_end(va); 32 } 33 34 if (uiLen > 0) 35 { 36 strMsg.resize(uiLen); 37 38 va_list va; 39 va_start(va, pszFmt); 40 #ifdef _WINDOWS 41 _vsnprintf(((char*)strMsg.c_str()), uiLen + 1, pszFmt, va); 42 #else 43 vsnprintf(((char*)strMsg.c_str()), uiLen + 1, pszFmt, va); 44 #endif 45 va_end(va); 46 } 47 sm_poInstance->LogMsgInner(ucLevel, strMsg.c_str()); 48 } 49 50 void CLog::LogMsgS(u8 ucLevel, const char* pszMsg) 51 { 52 if (CLog::instance()->meetLogCond(ucLevel)) 53 sm_poInstance->LogMsgInner(ucLevel, pszMsg); 54 } 55 56 void getCurrentTimeString(char* pszOut) 57 { 58 tm* pTm; 59 timeb oTimeb; 60 ftime(&oTimeb); 61 pTm = localtime(&oTimeb.time); 62 sprintf(pszOut, "%04d-%02d-%02d %02d:%02d:%02d.%03d", 63 pTm->tm_year + 1900, pTm->tm_mon + 1, pTm->tm_mday, pTm->tm_hour, pTm->tm_min, pTm->tm_sec, oTimeb.millitm); 64 } 65 66 void CLog::LogMsgInner(u8 ucLevel, const char* pszMsg) 67 { 68 char szDateTime[64]; 69 getCurrentTimeString(szDateTime); 70 char szLogLevel[6] = " [2] "; 71 if (ucLevel != 2) 72 szLogLevel[2] = '0' + ucLevel; 73 strcat(szDateTime, szLogLevel); 74 fprintf(m_pFile, szDateTime); 75 fprintf(m_pFile, pszMsg); 76 fprintf(m_pFile, "\n"); 77 fflush(m_pFile); 78 }
公共基礎函數實現
common.h
1 #ifndef CMN_H 2 #define CMN_H 3 4 #include <stdint.h> 5 #include <string> 6 #include <vector> 7 #ifdef _LINUX 8 #include <string.h> 9 #endif 10 using namespace std; 11 12 #ifdef _WINDOWS 13 #define DIR_SLASH '\\' 14 #define DIR_SLASH_STR "\\" 15 #else 16 #define DIR_SLASH '/' 17 #define DIR_SLASH_STR "/" 18 #endif 19 20 typedef uint64_t u64; 21 typedef uint32_t u32; 22 typedef uint8_t u8; 23 24 void trimString(std::string& strTrim); 25 bool separateString(const std::string& strSrc, const char* pszSep, std::string& strHead, std::string& strTail, bool bFromLeft); 26 void divideString2Vec(const std::string& strInput, const char* pszSep, std::vector<std::string>& vecOut); 27 bool shiftToU32(std::string& strVal, u32& uiVal); 28 bool isIntStrVec(std::vector<std::string>& vecVal); 29 std::string u32ToStr(u32 uiVal); 30 #ifdef _LINUX 31 u32 GetTickCount(); 32 #endif 33 34 std::string getMainPath(); 35 36 #endif
common.cpp
1 #include "common.h" 2 #ifdef _WINDOWS 3 #include <direct.h> 4 #include <windows.h> 5 #else 6 #include <stdio.h> 7 #include <stdlib.h> 8 #include <unistd.h> 9 #include <time.h> 10 #include <sys/time.h> 11 u32 GetTickCount() 12 { 13 timespec ts; 14 clock_gettime(CLOCK_MONOTONIC, &ts); 15 return (u32)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000); 16 } 17 #endif 18 19 void trimString(std::string& strTrim) 20 { 21 size_t iHeadPos = strTrim.find_first_not_of(" \t"); 22 if (iHeadPos == std::string::npos) 23 { 24 strTrim = ""; 25 return; 26 } 27 size_t iTailPos = strTrim.find_last_not_of(" \t"); 28 if (strTrim.length() != iTailPos + 1) 29 strTrim.erase(iTailPos + 1); 30 if (0 != iHeadPos) 31 strTrim.erase(0, iHeadPos); 32 } 33 34 bool separateString(const std::string& strSrc, const char* pszSep, std::string& strHead, std::string& strTail, bool bFromLeft) 35 { 36 std::string::size_type iPos = (bFromLeft ? strSrc.find(pszSep) : strSrc.rfind(pszSep)); 37 if (iPos == std::string::npos) 38 { 39 strHead = strSrc; 40 trimString(strHead); 41 strTail = ""; 42 return false; 43 } 44 45 if (0 == iPos) 46 strHead = ""; 47 else 48 { 49 strHead = strSrc.substr(0, iPos); 50 trimString(strHead); 51 } 52 if (strSrc.length() == iPos + strlen(pszSep)) 53 strTail = ""; 54 else 55 { 56 strTail = strSrc.substr(iPos + strlen(pszSep)); 57 trimString(strTail); 58 } 59 return true; 60 } 61 62 void divideString2Vec(const std::string& strInput, const char* pszSep, std::vector<std::string>& vecOut) 63 { 64 std::string strHead, strTail; 65 std::string strRights = strInput; 66 while (!strRights.empty()) 67 { 68 separateString(strRights, pszSep, strHead, strTail, true); 69 trimString(strHead); 70 if (!strHead.empty()) 71 vecOut.push_back(strHead); 72 trimString(strTail); 73 strRights = strTail; 74 } 75 } 76 77 bool isIntStr(const std::string& strVal) 78 { 79 if (strVal.empty()) 80 return false; 81 size_t len = strVal.length(); 82 if (len >= 10) 83 return false; 84 for (size_t idx = 0; idx < len; ++idx) 85 { 86 if (strVal[idx] < '0' || strVal[idx] > '9') 87 return false; 88 } 89 return true; 90 } 91 92 bool shiftToU32(std::string& strVal, u32& uiVal) 93 { 94 trimString(strVal); 95 if (strVal.empty()) 96 { 97 uiVal = 24; 98 return true; 99 } 100 if (!isIntStr(strVal)) 101 return false; 102 uiVal = strtoul(strVal.c_str(), 0, 10); 103 return true; 104 } 105 106 std::string u32ToStr(u32 uiVal) 107 { 108 char szBuf[128]; 109 sprintf(szBuf, "%u", uiVal); 110 return szBuf; 111 } 112 113 bool isIntStrVec(std::vector<std::string>& vecVal) 114 { 115 if (vecVal.empty()) 116 return false; 117 for (size_t idx = 0; idx < vecVal.size(); ++idx) 118 { 119 if (!isIntStr(vecVal[idx])) 120 return false; 121 } 122 return true; 123 } 124 125 static std::string s_strMainPath; 126 std::string getMainPath() 127 { 128 if (!s_strMainPath.empty()) 129 return s_strMainPath; 130 131 char szBuff[512]; 132 #ifdef _WINDOWS 133 GetModuleFileName(NULL, szBuff, 512); 134 std::string strAbsFile = szBuff; 135 size_t iPos = strAbsFile.rfind(DIR_SLASH); 136 s_strMainPath = strAbsFile.substr(0, iPos + 1); 137 #else 138 getcwd(szBuff, 512); 139 s_strMainPath = std::string(szBuff) + "/"; 140 #endif 141 return s_strMainPath; 142 }
關於處理性能
本程序處理由5個以及少於5個非負整數反求 24(或其它非負整數)是非常快的,如:
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:8,3,3,3
1. (3+3-3)*8
2. 3*(3+8-3)
3. 3*3*8/3
4. 3+3*8-3
Used time(ms): 0.
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:8,45,34,23,67
1. 34-((23+67)/45+8)
2. 45+67-8*(34-23)
Used time(ms): 63.
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:65,67,33,245,27
1. 33-(65+245-67)/27
Used time(ms): 78.
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:89,67,34,5
From 89,67,34,5, there is no solution to make 24 by using +-*/ and ().
Used time(ms): 0.
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:89,67,34,5,6
1. (34+5*(89-67))/6
2. 6*(67+89)/(5+34)
Used time(ms): 47.
但是參與運算的非負整數總數增加到6或更高時,由於完全求解所涉及的運算量呈指數級增長,所需的時間也呈指數級增長,6個數的求解時長已經達到了秒級,如:
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:89,67,34,5,6,87
1. (34+67-5)/(6+87-89)
2. (5+67-(89-87)*34)*6
3. 34+67+5*89-6*87
4. 34+89-((5+67)/6+87)
5. 34-((5+6)*87-67)/89
6. 34-(67+87-(5+89))/6
7. 5*34-(89-87)*(6+67)
8. 5+(6+34*87)/(67+89)
9. 6*(34-5)/87+89-67
10. 6*89-(87-(5+67))*34
11. 6+5*34/(89-87)-67
12. 67+89-(5+6+34+87)
Used time(ms): 3110.
Please input several (at most 6) non-negative integers separated by comma to make 24 or q to quit:23,45,67,89,34,66
1. (23+45-66)*(34+67-89)
2. (89-67)*45/(34-23)-66
3. 23+34-66*67/(45+89)
4. 23+45*66/(67-34)-89
5. 66/(45+89)+34/67+23
6. 67+89-66*(23+45)/34
Used time(ms): 3078.
也因為這個原因,本程序設置了最多6個數的限制。
目前是單線程實現的,可以考慮用多線程並發處理,即對根任務按水位分解成多個子任務交給多個處理線程並發處理。但如果參與運算的數太多的話,多線程機制也作用不大,應該還是要從算法上想更好的辦法。
完整代碼文件
完整代碼文件可以從如下位置提取:
https://github.com/readalps/24GamePro
其中 prj 目錄下為 Windows 平台使用的工程文件;makefile 用於 Linux 平台;src 目錄下為代碼文件,7個文件總體代碼約850行。