寫棋牌AI經常需要搜索所有非空真子集,舉個例子
假設手牌{1,2,3,4},那么我們可能需要搜索以下集合
{1,2,3,4}
{1} {2} {3} {4} -----C
{1,2} {1,3} {1,4} {2,3} {2,4} {3,4} -----C
{1,2,3}{1,2,4} {1,3,4} {2,3,4} -----C
它有多少個子集呢?
這里根據高中數學,我們會發現每一行都是 n為集合元素個數,m為當前子集元素個數,即為從集合中挑出幾個元素。根據二項式定理:C
+C
+C
+…+C
=2n,我們可以得出所有子集個數為2n,又因為要減去空集和它自身,所以最終結果為2n-2。由於要遍歷所有子集,所以我們的算法單從目前實現上來說最佳時間復雜度最優解為O(2n),因為遍歷完這2n-2個子集中每一個,它的時間復雜度不可能低於這個,這里我們可以把這個子集抽象成一棵樹去理解。
怎么去實現它?最常想到的是DPS:
這里可以用遞歸實現該算法,遞歸通常需要一個結束條件來控制它的遍歷深度,這里很顯然,我們可以用層數來充當停止條件。第二個問題是,如何控制它的廣度,我們清楚二叉樹只有左右子樹,但是非二叉樹的子樹難以確定,這里我們先將手牌排序,然后通過for循環和數組的size來控制它的廣度。
這里說第一個技巧,刪除數組元素通常不夠方便,浪費時間,這里我們用swap將當前要刪除的元素和最后一個元素交換,最后pop_back()即可,這里的時間復雜度為O(1)。
第二個技巧是map問題,減少map的使用,我們寫這個算法時會用到hash原理,習慣於使用STL中的map容器,map容器的數據結構是紅黑樹,紅黑樹有左右指針和顏色。各占4個字節,我們可以采用size0f()來驗證這個內存占用,在VS里加起來確實是12,頂多在加上鍵和值的大小,看起來似乎可以接受,實際上並非如此。因為map中的erase以及clear,不能馬上釋放內存。map有自己的機制回收內存,用erase以及clear之后,如果沒有特殊需求,可以認為那部分內存已經釋放了。map不會馬上釋放刪掉內容的內存,而是會對內存進行預留,如果確實很長時間用不到預留的內存,才會釋放。所以如果你想優化你的內存使用,減少使用map。
第三個技巧是使用map時,通過value查找key,這里可以使用find_if,使用方法,構造pred函數對象,這里關鍵是重載()。如下是find_if代碼
template <class InputIterator, class Predicate>
InputIterator find_if(InputIterator first, InputIterator last,Predicate pred)
{
while (first != last && !pred(*first)) ++first;
return first;
}
最后,說下優化,無法從算法實現上優化,我們可以優化這個策略,AI策略是加權求和,我們可以通過計算,推演出某些類型的打法必定是高於另一些的,采用貪心的局部最優
優化掉這些求解。這里不貼代碼。