1. 程式人生 > >致佳音: 推箱子游戲自動求解演算法設計(五)

致佳音: 推箱子游戲自動求解演算法設計(五)

說了這麼多,這一節是本文最後一節啦,就是程式的進一步優化。

這一節呢,還分那麼幾個小意思,- -!

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