這一次讓偶仍然用拿手的窮舉法來寫一個自動解推箱子問題的機器人吧。
偶使用了yzwykkldczsh同志寫的狀態機器模板以及該模板的配套演算法“萬用智能超級無窮迭代動態規劃遞迴演算法”(為紀念友人Jornathan Ding,此演算法又稱為H>D演算法)。它的基本原理是:
1.將每一步的人和箱子的資料放到一個記錄裡,此記錄表示目前狀態
2.設定目前狀態為已訪問
3.遍曆下面的每一個子狀態。若所有子狀態都已訪問或無解,則當前步也無解
4.若子狀態有解,則恢複此狀態為未訪問。
對所有子狀態,重複1-4步。
此演算法可解很多智力題,比如倒酒問題、過河問題,等等。
偶的測試程式裡寫死了地圖。實際應用中應當從檔案中載入。下面帖出測試程式碼:
ACIter.h源碼
/*
* CACIter:用於記錄結點訪問狀態的輔助模板類。TNodeKey為唯一確定目前狀態的雜湊實值型別,應當是一個struct。
* 三種狀態是:nsNone:無或不在結點訪問狀態表中;nsVisiting:正在訪問;nsDead:已死亡
*/
#pragma once
#include <map>
enum NodeStatus {
nsNone,
nsVisiting,
nsDead
};
template<typename _Ty>
class CMemLessTempl
: public std::binary_function<_Ty, _Ty, bool>
{
public:
bool operator()(const _Ty& _Left, const _Ty& _Right) const
{
/* 對於兩個struct的比較,用記憶體逐byte比較即可。
要注意的是struct如果不是按位元組對齊的話,需要在初始化時memset為全0,否則可能有幹擾資料 */
unsigned char *p1 = (unsigned char *)&_Left, *p2 = (unsigned char *)&_Right;
return memcmp(p1, p2, sizeof(_Ty)) < 0;
}
};
template<typename TNodeKey>
class CACIter
{
private:
typedef std::map<TNodeKey, enum NodeStatus, CMemLessTempl<TNodeKey> > NODESTATUSMAP;
/* 結點訪問狀態記錄樹 */
NODESTATUSMAP m_NodeStatus;
public:
/* 擷取當前結點訪問狀態。找不到也返回nsNone */
virtual enum NodeStatus GetStatus(TNodeKey key)
{
NODESTATUSMAP::iterator it = m_NodeStatus.find(key);
if ( it == m_NodeStatus.end() ) return nsNone;
return it->second;
}
/* 設定當前結點訪問狀態。如果設為nsNone,則可刪除這個結點 */
virtual void SetStatus(TNodeKey key, enum NodeStatus status)
{
if (status == nsNone)
{
NODESTATUSMAP::iterator it = m_NodeStatus.find(key);
if ( it == m_NodeStatus.end() ) return;
m_NodeStatus.erase(it);
return;
}
m_NodeStatus[key] = status;
}
public:
CACIter(void) {}
~CACIter(void) {}
};
主程式源碼(偶是用VS2003的console app寫的測試程式)
// PushBoxRobot.cpp : 定義控制台應用程式的進入點。
//
#include "stdafx.h"
#include "conio.h"
#include "ACIter.h"
#include <map>
#include <vector>
/* 地圖最大寬度和高度 */
#define MAXMAPWIDTH 40
#define MAXMAPHEIGHT 40
/* 假定最多有6個箱子 */
#define MAXBOXCOUNT 6
/* 地圖上幾種元素:空格表示空地,X表示阻擋物,?表示箱子移動的目的地,O表示箱子的起始位置,*表示人的起始位置 */
#define MAP_EMPTY ' '
#define MAP_BLOCK 'X'
#define MAP_HOLE '?' // target position
#define MAP_BOX 'O' // box start position(internal use)
#define MAP_MAN '*' // man start position(internal use)
/* 目前狀態。x,y為人的位置,boxx[],boxy[]為箱子的位置。假定最多有6個箱子 */
struct _PushStatus
{
// man pos
char x, y;
char boxx[MAXBOXCOUNT], boxy[MAXBOXCOUNT];
};
enum TryResult
{
trFailed,
trUnknown,
trSuccess
};
/* 工作類 */
class CPushBoxAI : public CACIter<struct _PushStatus>
{
public:
/* 記錄當前工作路徑的數組,記錄最短成功路徑的數組 */
std::vector<struct _PushStatus> m_vStepLog, m_vSuccLog;
/* 地圖,其中不包含人和箱子 */
char m_Map[MAXMAPWIDTH][MAXMAPHEIGHT];
/* 地圖寬高、箱子數量、最短成功路徑 */
int m_nWidth, m_nHeight, m_nBoxCount, m_nMinSteps;
/* 初始化地圖和初始狀態 */
void Init(struct _PushStatus& status);
/* 嘗試下一個方向。status是目前狀態,level是下一個方向的深度(步數),dx,dy指出下一個方向 */
enum TryResult TryDir(struct _PushStatus status, int level, int dx, int dy);
/* 求解 */
bool Execute(void);
/* 列印目前狀態下的地圖 */
void DumpMaze(struct _PushStatus status);
};
void CPushBoxAI::Init(struct _PushStatus& status)
{
/* 寫死的一個地圖,實際應用中應替換成從檔案中讀入。略。 */
char *map = {
" XXXXX"
" X *X"
"XXX XOO X"
"X?X X O X"
"X?XXX XX "
"X? XX "
"X X XX"
"XXXX XX"
" XXXXX "
};
char *p = map;
m_nHeight = 9;
m_nWidth = 9;
m_nBoxCount = 0;
for ( int j = 0; j < 9; j++ )
for ( int i = 0; i < 9; i++, p++ )
{
switch(*p)
{
case ' ': m_Map[i][j] = MAP_EMPTY; break;
case 'X': m_Map[i][j] = MAP_BLOCK; break;
case '?': m_Map[i][j] = MAP_HOLE; break;
case '*': status.x = i; status.y = j; m_Map[i][j] = MAP_EMPTY; break;
default: // O
status.boxx[m_nBoxCount] = i;
status.boxy[m_nBoxCount] = j;
m_Map[i][j] = MAP_EMPTY;
m_nBoxCount++;
break;
}
}
}
enum TryResult CPushBoxAI::TryDir(struct _PushStatus status, int level, int dx, int dy)
{
int x = status.x + dx;
int y = status.y + dy;
/* 超出地圖範圍?失敗 */
if ( x < 0 || x >= m_nWidth || y < 0 || y >= m_nHeight ) return trFailed;
/* 碰到阻擋?失敗 */
if ( m_Map[x][y] == MAP_BLOCK ) return trFailed;
int nLeft = m_nBoxCount;
status.x = x;
status.y = y;
for ( int i = 0; i < m_nBoxCount; i++ )
{
if ( x == status.boxx[i] && y == status.boxy[i] )
{
// 碰到了箱子。嘗試推動它
int m = status.boxx[i] + dx;
int n = status.boxy[i] + dy;
// 嘗試推動箱子出地圖?失敗
if ( m < 0 || m >= m_nWidth || n < 0 || n >= m_nHeight ) return trFailed;
// 箱子碰到阻擋?失敗
if ( m_Map[m][n] == MAP_BLOCK ) return trFailed;
// 箱子碰到其它箱子?失敗
for ( int j = 0; j < m_nBoxCount; j++ )
if ( j != i && m == status.boxx[j] && n == status.boxy[j] )
return trFailed;// crash
status.boxx[i] = m;
status.boxy[i] = n;
}
// 箱子到達目的地?
if ( m_Map[status.boxx[i]][status.boxy[i]] == MAP_HOLE ) nLeft--;
}
/* 超出現有最短步數?失敗 */
if (level >= m_nMinSteps) return trUnknown;
// 目前狀態如果已經失敗或者已經訪問,則此步也失敗
if (GetStatus(status) != nsNone) return trFailed;
// 記錄當前路徑
if ( (int)m_vStepLog.size() == level ) m_vStepLog.push_back(status);
else if ( (int)m_vStepLog.size() > level ) m_vStepLog[level] = status;
// 所有箱子都到達了目的地
if (nLeft == 0)
{
m_nMinSteps = level;
// 將當前路徑拷貝到成功路徑中,並且更新最小步數變數
m_vSuccLog.clear();
std::insert_iterator< std::vector<struct _PushStatus> > iter( m_vSuccLog, m_vSuccLog.end() );
copy( m_vStepLog.begin(), m_vStepLog.end(), iter );
// 返回成功
return trSuccess; // success
}
// 設定目前狀態為正在訪問,並且訪問下面的狀態
SetStatus(status, nsVisiting);
int n = 0;
enum TryResult tr;
bool bSucc = false;
tr = TryDir(status, level + 1, -1, 0);
if ( tr != trFailed ) n++;
if ( tr == trSuccess ) bSucc = true;
tr = TryDir(status, level + 1, 1, 0);
if ( tr != trFailed ) n++;
if ( tr == trSuccess ) bSucc = true;
tr = TryDir(status, level + 1, 0, -1);
if ( tr != trFailed ) n++;
if ( tr == trSuccess ) bSucc = true;
tr = TryDir(status, level + 1, 0, 1);
if ( tr != trFailed ) n++;
if ( tr == trSuccess ) bSucc = true;
// 如果後續狀態都無法訪問或不成功,則目前狀態也死亡。否則恢複目前狀態為未訪問。
if ( n == 0 ) SetStatus(status, nsDead);
else SetStatus(status, nsNone);
if ( bSucc ) return trSuccess;
else if ( n > 0 ) return trUnknown;
else return trFailed;
}
bool CPushBoxAI::Execute(void)
{
m_nMinSteps = 1000;
int n = 0;
struct _PushStatus status;
// 一定要初始化為全0
memset(&status, 0, sizeof(struct _PushStatus));
// 初始化地圖和開始時的狀態
Init(status);
// 開始訪問相鄰四個方向
if ( TryDir(status, 0, -1, 0) == trSuccess ) n++;
if ( TryDir(status, 0, 1, 0) == trSuccess ) n++;
if ( TryDir(status, 0, 0, -1) == trSuccess ) n++;
if ( TryDir(status, 0, 0, 1) == trSuccess ) n++;
return n > 0;
}
void CPushBoxAI::DumpMaze(struct _PushStatus status)
{
for ( int j = 0; j < m_nHeight; j++ )
{
for ( int i = 0; i < m_nWidth; i++ )
{
char c = m_Map[i][j];
for ( int k = 0; k < m_nBoxCount; k++ )
if ( i == status.boxx[k] && j == status.boxy[k] )
{
c = 'O';
break;
}
if ( i == status.x && j == status.y ) c = '*';
printf("%c", c);
}
printf("/n");
}
}
int _tmain(int argc, _TCHAR* argv[])
{
CPushBoxAI pbai;
if (pbai.Execute())
{
// 成功,列印成功路徑。
for ( int i = 0; i < pbai.m_nMinSteps + 1; i++ )
pbai.DumpMaze(pbai.m_vSuccLog[i]);
printf("OK");
}
else printf("NO RESULT"); // 無解
getch();
return 0;
}
06/6/30: 發現原演算法有些問題,當超過規定步數時也會把目前狀態置為Dead,會引起誤判,造成不能算出最優解。做了一些改動。每步嘗試將返回3種類型中的一種:失敗、成功或未知。如下一步是未知,則不修改狀態。(遺憾的是演算法又慢鳥大概2秒鐘 -___-||| )
PS: 地盤上很涼快啊~~~