致佳音: 推箱子游戲自動求解演算法設計(五)
說了這麼多,這一節是本文最後一節啦,就是程式的進一步優化。
這一節呢,還分那麼幾個小意思,- -!
1.程式邏輯和機制的優化
2.原始碼級程式碼的優化
3.針對CPU和作業系統的編譯優化
問:大俠,我是過來人,排序雜湊我漸漸習慣了,不痛了,還有哪些地方可以更刺激的
答:前面我們提到檢測局面重複,不要讓後面的局面有跟走過的局面一樣,導致無限的墮落,無法自拔,還有一樣是可以算作重複的
那就是失敗的局面,即沒有一個箱子可以有效推的局面,再出現這個局面就不要分析了,直接刪掉吧,那麼我們就要再建立一個失敗
局面佇列,把失敗過的局面箱子座標和雜湊值以及搬運工的位置儲存下來,雜湊值是為了雜湊計算,而座標則是為了萬一的萬一的萬
一的萬一有重複,局面解不出來,那麼就原汁原味的使用座標比較,當然使用純雜湊比較求解的時候座標既不儲存也不分配記憶體。
問:那麼就只有這個嗎?
答:同樣,失敗的局面向父級遞迴,tagStage.Slaves遞減,如果降到零,那麼父級也是失敗局面,跟著連坐,也加入失敗局面佇列。
問:爽!我想馬上看原始碼,我要看,我要看……
答:
// 記錄失敗場景快照(暫時未完成測試) int fnStageSnap(PQUEUE pQueue, PSTAGE pStage) { union { PSNAP pSnap; // 快照指標 BYTE *pDummy; }; PSHOT pShot; int dwRet; pShot = pQueue->Shots; if(pShot->Count) { pSnap = (PSNAP)realloc(pShot->Snaps, pShot->Size * (pShot->Count + 1)); }else { pSnap = (PSNAP)malloc(pShot->Size); } if(pSnap) { pShot->Snaps = pSnap; // 更新指標 //pSnap = &pSnap[pNext->Shots->Count]; // 結構體不定大小 pDummy += pShot->Size * pShot->Count; pSnap->Position = pStage->Position; pSnap->Hash = pStage->Hash; // 佈局指紋 if((pStage->Flags & SGF_CRC32) == 0) { // 排序儲存座標詳情(只要不歸位, 都會撤銷回到最初, 不必排序) //fnStageSort(pNext->Stars, NULL, pSnap->Stars, pNext->Count); V32Copy(pSnap->Stars, pStage->Series, sizeof(STAR) * pStage->Count); } pShot->Count++; // 遞增失敗局面數量 // 向上遞迴父級場景, 所有待分析子場景數量為零的都將記錄到失敗列表並彈出佇列 pStage = pStage->Host; if(pStage != NULL) { #ifdef _DEBUG fnPrint("父級場景=0x%08X, 剩餘子級場景數量=%d.\r\n", pStage, pStage->Slaves); dwRet = fnQueueRange(pQueue, pStage); if(dwRet <= 0) { fnPrint("[警告]場景0x%08X不在有效佇列中!\r\n", pStage); return 1; } if(pStage->Slaves == 0) { fnPrint("[警告]場景0x%08X已經沒有子場景!\r\n", pStage); return 1; } #endif pStage->Slaves--; // 除錯完成可以確保計數為正 if(pStage->Slaves == 0) { #ifdef _DEBUG fnPrint("記錄父級場景=0x%08X到失敗集合.\r\n", pStage); #endif dwRet = fnStageSnap(pQueue, pStage); if(dwRet <= 0) return dwRet - 1; #ifdef _DEBUG fnPrint("移除父級場景=0x%08X.\r\n", pStage); #endif fnQueueRemove(pQueue, pStage); return dwRet + 1; // 返回級數 } } return 1; } fnStageCode(SEC_CACHE_NULL); return 0; // 記憶體不足, 資料不變 }
問:那麼,可有執行效果比較?
答:
沒有失敗佇列,在前面已有,這裡再貼一次:
加入失敗佇列後,注意佇列使用峰值,剩餘值和步數比較:
問:太TM神了,還有沒有?我還要,我還要!
答:當我們針對一個箱子進行分析的時候,無論上下左右,總有個固定次序,比如就是上下左右,有可能最好的走法是後面的方向
得到走法列表後,按照歸位數量從大到小依次生成場景,如圖,如果不優化這個機制,上下左三個方向會先被生成分析,而加入這
個機制,向右的歸位數量最大,先被分析,其實就一步完成了,也就是立刻到達最優解。
問:暴走了。。還有嗎?
答:留點空間給後來者發揮吧, 下面是原始碼級別的優化,比如內部指標
tagStage有幾個內部指標,在申請記憶體時一併計算分配,而不是後面再分配,動態分配在32位Windows中以4K為
單位,你申請一個位元組也是4K,浪費記憶體,反覆申請釋放,過程複雜容易寫錯,同時容易產生碎片手榴彈。。。
另外,是一維陣列代替若干個一維或多維陣列,比如Stars,簡化記憶體管理邏輯,詳見資源包程式碼
再者,使用內聯彙編的裸函式代替庫函式,可以大幅提升效率,比如記憶體複製,在V32.dll中:
// 複製兩個記憶體塊, 不檢測指標
void __declspec(naked) __stdcall fnCopy(void *dst, const void *src, int len)
{
// 除錯時mov esi, esp, 呼叫完成cmp esi, esp檢查堆疊
//__asm{
// mov edi, dst ; [esp + 4]
// mov esi, src ; [esp + 8]
// mov ecx, len ; [esp + c]
// rep movsb
//}
__asm{
push esi ; 保護暫存器esi和edi, 第一個變數由[esp+4h]推移至[esp+0ch], 第二個為+10h
push edi
mov esi, [esp+10h] ; 串操作原始地址
mov edi, [esp+0ch] ; 串操作目標地址
mov ecx, [esp+14h] ; 串操作計數
and ecx, 3 ; 先複製餘數
rep movsb ; 逐位元組複製
mov ecx, [esp+14h]
shr ecx, 2 ; 求四的商(雙字個數)
rep movsd ; 逐雙字複製
pop edi
pop esi
ret 0ch ; 裸函式返回(三個引數)
}
}
當你賦值兩個結構體時,編譯出來其實差不多就是類似的指令,而不會呼叫memcpy之類的函式。
就像把比較放在迴圈外,有程式來控制指標,我們幹嘛每次都去檢測這個指標呢?
最後針對CPU和作業系統的優化,時間差不多了,就不說了,Intel有專門的編譯優化工具。
其實,我只是寫個小禮物給蓮花妹妹開心一下,寫到這裡,也算盡心盡力了,但願程式設計之美,能帶給世人更多的歡笑,釋放更多的壓力和痛苦
我的計算機說,反反覆覆的事情就TM交給我吧!!!
致佳茵 全文畢
虎膽遊俠
2015-03-15 00:34:56