function Prior(State:StateType;M1,M2:MoveType):boolean;
var
NewPos:integer;
Inertia1,Inertia2:boolean;
S1,S2:StateType;
H1,H2:integer;
begin
Prior:=false;
if State.MoveCount>0 then
begin
NewPos:=State.Move[State.MoveCount].Position+
DeltaPos[State.Move[State.MoveCount].Direction];
if NewPos=M1.Position then Inertia1:=true else Inertia1:=false;
连续推同一个箱子的动作优先
if NewPos=M2.Position then Inertia2:=true else Inertia2:=false;
if Inertia1 and not Inertia2 then begin Prior:=true; exit; end;
if Inertia2 and not Inertia1 then begin Prior:=false; exit; end;
end;
end;
procedure IDA_Star;
var
Sucess:boolean;
CurrentState:StateType;
H:integer;
f:Text;
procedure IDA_Push(State:StateType);
begin
if IDA.Top=MaxStack then
Exit;
inc(IDA.Top);
IDA.Stack[IDA.Top]:=State;
end;
procedure IDA_Pop(var State:StateType);
begin
State:=IDA.Stack[IDA.Top];
dec(IDA.Top);
end;
function IDA_Empty:boolean;
begin
IDA_Empty:=(IDA.Top=0);
end;
上面的是栈操作
procedure IDA_AddToHashTable(State:StateType);
var
h:integer;
p:PHashTableEntry;
begin
h:=HashFunction(State);
if HashTable^.Count[h]<MaxSubEntry then
begin
new(p);
p^.State:=State;
p^.Next:=HashTable^.FirstEntry[h];
HashTable^.FirstEntry[h]:=p;
inc(HashTable^.Count[h]);
end
else begin
p:=HashTable^.FirstEntry[h];
while p^.Next^.Next<>nil do
p:=p^.Next;
p^.Next^.State:=State;
p^.Next^.Next:=HashTable^.FirstEntry[h];
HashTable^.FirstEntry[h]:=p^.Next;
p^.Next:=nil;
end;
end;
function IDA_InHashTable(State:StateType):boolean;
var
h:integer;
p:PHashTableEntry;
begin
h:=HashFunction(State);
p:=HashTable^.FirstEntry[h];
IDA_InHashTable:=true;
while p<>nil do
begin
if SameState(p^.State,State) then
begin
if p^.State.g>State.g then
begin
p^.State.g:=State.g;
IDA_InHashTable:=false;
如果找到的表项深度要大些,并不代表这一次深度小点的也无解。本来应该动态更新下界
的,这里作为没有找到处理,后面的章节会改进这个地方的。
end;
exit;
end;
p:=p^.Next;
end;
IDA_InHashTable:=false;
end;
这是Hash表的操作。
procedure IDA_AddNode(State:StateType);
begin
IDA_Push(State);
inc(IDA.NodeCount);
if IDA.NodeCount mod DispNode=0 then
Writeln('NodeCount=',IDA.NodeCount);
inc(IDA.TopLevelNodeCount);
IDA_AddToHashTable(State);
end;
procedure IDA_Expand(State:StateType);
var
MoveCount:integer;
MoveList:array[1..Maxx*Maxy*4] of MoveType;
t:MoveType;
i,j,Direction:integer;
NewBoxPos, NewManPos:integer;
NewState:StateType;
begin
MoveCount:=0;
for i:=1 to MaxPosition do
if GetBit(State.Boxes,i)>0 then
for Direction:=0 to 3 do
begin
NewBoxPos:=i+DeltaPos[Direction];
NewManPos:=i+DeltaPos[Opposite[Direction]];
if GetBit(State.Boxes,NewBoxPos)>0 then continue;
if GetBit(SokoMaze.Walls,NewBoxPos)>0 then continue;
if GetBit(State.Boxes,NewManPos)>0 then continue;
if GetBit(SokoMaze.Walls,NewManPos)>0 then continue;
if CanReach(State,NewManPos) then
begin
DoMove(State,i,Direction,NewState);
if CalcHeuristicFunction(NewState)=Infinite then continue;
if CalcHeuristicFunction(NewState)+State.g>=IDA.PathLimit then con
tinue;
IDA*算法的核心:深度限制
if IDA_InHashTable(NewState) then continue;
inc(MoveCount);
MoveList[MoveCount].Position:=i;
MoveList[MoveCount].Direction:=Direction;
end;
end;
for i:=1 to MoveCount do
for j:=i+1 to MoveCount do
if Prior(State,MoveList[i],MoveList[j]) then
调整推法次序
begin
t:=MoveList[j];
MoveList[j]:=MoveList[i];
MoveList[i]:=t;
end;
for i:=1 to MoveCount do
依次考虑所有移动方案
begin
DoMove(State,MoveList[i].Position,MoveList[i].Direction,NewState);
inc(NewState.MoveCount);
NewState.Move[NewState.MoveCount].Position:=MoveList[i].Position;
NewState.Move[NewState.MoveCount].Direction:=MoveList[i].Direction;
NewState.g:=State.g+1;
IDA_AddNode(NewState);
end;
end;
procedure IDA_Answer(State:StateType);
var
i:integer;
x,y:integer;
begin
Writeln(f,'Solution Found in ', State.MoveCount,' Pushes');
for i:=1 to State.Movecount do
begin
x:=State.Move[i].Position div SokoMaze.Y+1;
y:=State.Move[i].Position mod SokoMaze.Y+1;
Writeln(f, x,' ',y,' ',DirectionWords[State.Move[i].Direction]);
end;
end;
begin
Writeln(VerStr);
Writeln(Author);
IDA.PathLimit:=CalcHeuristicFunction(IDA.StartState)-1;
Sucess:=false;
repeat
inc(IDA.PathLimit);
Writeln('Pathlimit=',IDA.PathLimit);
IDA.TopLevelNodeCount:=0;
IDA.Top:=0;
IDA.StartState.g:=0;
IDA_Push(IDA.StartState);
repeat
IDA_Pop(CurrentState);
H:=CalcHeuristicFunction(CurrentState);
if H=Infinite then continue;
if Solution(CurrentState) then
Sucess:=true
else if IDA.PathLimit>=CurrentState.g+H then
IDA_Expand(CurrentState);
until Sucess or IDA_Empty or (IDA.NodeCount>MaxNode);
Writeln('PathLimit ',IDA.PathLimit,' Finished. NodeCount=',IDA.NodeCount);
until Sucess or (IDA.PathLimit>=MaxDepth) or (IDA.NodeCount>MaxNode);
Assign(f,outfile);
ReWrite(f);
Writeln(f,VerStr);
Writeln(f,Author);
Writeln(f);
if not Sucess then
Writeln(f,'Cannot find a solution.')
else
IDA_Answer(CurrentState);
Writeln('Node Count:',IDA.NodeCount);
Writeln;
close(f);
end;
begin
Init;
IDA_Star;
end.
function CanReach(State:StateType; Position:integer):boolean;
用BFS判断在状态State中,搬运工是否可以到达Position
var
Direction:integer;
Pos,NewPos:integer;
Get,Put:integer;
Queue:array[0..Maxx*Maxy] of integer;
Reached:Array[0..Maxx*Maxy] of boolean;
begin
fillchar(Reached,sizeof(Reached),0);
Pos:=State.ManPosition;
Get:=0; Put:=1;
Queue[0]:=Pos;
Reached[Pos]:=true;
CanReach:=true;
while Get<>Put do
begin
Pos:=Queue[Get];
inc(Get);
if Pos=Position then
exit;
for Direction:=0 to 3 do
begin
NewPos:=Pos+DeltaPos[Direction];
if Reached[NewPos] then continue;
if GetBit(State.Boxes,NewPos)>0 then continue;
if GetBit(SokoMaze.Walls,NewPos)>0 then continue;
Reached[NewPos]:=true;
Queue[Put]:=NewPos;
inc(Put);
end;
end;
CanReach:=false;
end;
function MinPush(BoxPosition,GoalPosition:integer):integer;
在没有其他箱子的情况下,从BoxPosition推到GoalPosition至少要多少步。
var
i:integer;
Direction:integer;
Pos,NewPos,ManPos:integer;
Get,Put:integer;
Queue:array[0..Maxx*Maxy] of integer;
Distance:Array[0..Maxx*Maxy] of integer;
begin
for i:=0 to Maxx*Maxy do
Distance[i]:=Infinite;
Pos:=BoxPosition;
Get:=0; Put:=1;
Queue[0]:=Pos;
Distance[Pos]:=0;
while Get<>Put do
begin
Pos:=Queue[Get];
inc(Get);
if Pos=GoalPosition then
begin
MinPush:=Distance[Pos];
exit;
end;
for Direction:=0 to 3 do
begin
NewPos:=Pos+DeltaPos[Direction];
ManPos:=Pos+DeltaPos[Opposite[Direction]];
人应该站在后面
if Distance[NewPos]<Infinite then continue;
if GetBit(SokoMaze.Walls,NewPos)>0 then continue;
推不动
if GetBit(SokoMaze.Walls,ManPos)>0 then continue;
人没有站的地方
Distance[NewPos]:=Distance[Pos]+1;
Queue[Put]:=NewPos;
inc(Put);
end;
end;
MinPush:=Infinite;
end;
procedure DoMove(State:StateType; Position,Direction:integer; var NewState:Sta
teType);
var
NewPos:integer;
begin
NewState:=State;
NewPos:=Position+DeltaPos[Direction];
NewState.ManPosition:=Position;
SetBit(NewState.Boxes,NewPos);
ClearBit(NewState.Boxes,Position);
end;
function MinMatch(BoxCount:integer;Gr:BiGraph):integer;
这个是标准算法,抄的书上的程序,不用看了。
var
VeryBig:integer;
TempGr:BiGraph;
L:array[1..MaxBox*2] of integer;
SetX,SetY,MatchedX,MatchedY:Set of 1..MaxBox;
procedure MaxMatch(n,m:integer);
function Path(x:integer):boolean;
var
i,j:integer;
begin
Path:=false;
for i:=1 to m do
if not (i in SetY)and(Gr[x,i]<>0) then
begin
SetY:=SetY+[i];
if not (i in MatchedY) then
begin
Gr[x,i]:=-Gr[x,i];
MatchedY:=MatchedY+[i];
Path:=true;
exit;
end;
j:=1;
while (j<=m)and not (j in SetX) and (Gr[j,i]>=0) do inc(j);
if j<=m then
begin
SetX:=SetX+[j];
if Path(j) then
begin
Gr[x,i]:=-Gr[x,i];
Gr[j,i]:=-Gr[j,i];
Path:=true;
exit;
end;
end;
end;
end;
var
u,i,j,al:integer;
begin
Fillchar(L,sizeof(L),0);
TempGr:=Gr;
for i:=1 to n do
for j:=1 to m do
if L[i]<Gr[i,j] then
L[i]:=Gr[i,j];
u:=1; MatchedX:=[]; MatchedY:=[];
for i:=1 to n do
for j:=1 to m do
if L[i]+L[n+j]=TempGr[i,j] then
Gr[i,j]:=1
else
Gr[i,j]:=0;
while u<=n do
begin
SetX:=[u]; SetY:=[];
if not (u in MatchedX) then
begin
if not Path(u) then
begin
al:=Infinite;
for i:=1 to n do
for j:=1 to m do
if (i in SetX) and not (j in SetY) and (L[i]+L[n+j]-TempGr[i,j]<al
) then
al:=L[i]+L[n+j]-TempGr[i,j];
for i:=1 to n do if i in SetX then L[i]:=L[i]-al;
for i:=1 to m do if i in SetY then l[n+i]:=l[n+i]+al;
for i:=1 to n do
for j:=1 to m do
if l[i]+l[n+j]=TempGr[i,j] then
Gr[i,j]:=1
else
Gr[i,j]:=0;
MatchedX:=[]; MatchedY:=[];
for i:=1 to n+m do
if l[i]<-1000 then
exit;
end
else
MatchedX:=MatchedX+[u];
u:=0;
end;
inc(u);
end;
end;
var
i,j:integer;
Tot:integer;
begin
VeryBig:=0;
for i:=1 to BoxCount do
for j:=1 to BoxCount do
if (Gr[i,j]<Infinite)and(Gr[i,j]>VeryBig) then
VeryBig:=Gr[i,j];
inc(VeryBig);
for i:=1 to BoxCount do
for j:=1 to BoxCount do
if Gr[i,j]<Infinite then
Gr[i,j]:=VeryBig-Gr[i,j]
else
Gr[i,j]:=0;
这些语句是进行补集转化。
MaxMatch(BoxCount,BoxCount);
Tot:=0;
for i:=1 to BoxCount do
begin
for j:=1 to BoxCount do
if Gr[i,j]<0 then
begin
Tot:=Tot+VeryBig-TempGr[i,j];
break;
end;
if Gr[i,j]>=0 then
begin
MinMatch:=Infinite;
exit;
end;
end;
MinMatch:=Tot;
end;
function CalcHeuristicFunction(State:StateType):integer;
计算启发函数值
var
H,Min:integer;
i,j,p,Count,BoxCount,Cost:integer;
BoxPos:array[1..MaxBox] of integer;
Distance:BiGraph;
begin
p:=0;
for i:=1 to MaxPosition do
if GetBit(State.Boxes,i)>0 then
begin
inc(p);
BoxPos[p]:=i;
end;
for i:=1 to p do
for j:=1 to p do
Distance[i,j]:=MinPush(BoxPos[i],SokoMaze.GoalPosition[j]);
BoxCount:=SokoMaze.BoxCount;
H:=0;
for i:=1 to BoxCount do
begin
Count:=0;
for j:=1 to BoxCount do
if Distance[i,j]<Infinite then
inc(Count);
if Count=0 then
有一个箱子推不到任何目的地
begin
CalcHeuristicFunction:=Infinite;
exit;
end;
end;
H:=MinMatch(BoxCount, Distance);
CalcHeuristicFunction:=H;
end;
function HashFunction(State:StateType):integer;
var
i,h,p:integer;
begin
h:=0;
p:=0;
for i:=1 to MaxPosition do
if GetBit(State.Boxes,i)>0 then
begin
inc(p);
h:=(h+p*i) mod HashMask;
你可以自己换一个
end;
HashFunction:=h;
end;
function SameState(S1,S2:StateType):boolean;
var
i:integer;
begin
SameState:=false;
for i:=1 to MaxPosition do
if GetBit(S1.Boxes,i)<>GetBit(S2.Boxes,i) then
exit;
if not CanReach(S1,S2.ManPosition) then
注意只要两个状态人的位置是相通的就应该算同一个状态
exit;
SameState:=true;
end;
5.结点扩展顺序的优化
在这一节中,我们的最后一个改进是优化结点扩展的顺序,不是想修剪搜索树,而是希望
早一点得到解。具体的改进方法是这样的:
1.优先推刚刚推过的箱子
2.然后试所有的能够减少下界的方案,减少得越多越先试。如果减少得一样多,就先推离
目标最近的。
3.最后试其他的,也象2一样按顺序考虑。
可以预料,这样处理以后,"比较容易"先找到解,但是因为下界估计不准所花费的代价是
无法减小的(也就是说只能减少顶层结点数)。不过作为IDA*的标准改进方法之一,我们
有必要把它加入我们的程序中试试。
(需要注意的是,我们使用的是栈,应该把比较差的方案先压栈)
实际测试结果,1的效果比较好,2和3的效果不佳,甚至产生了更多的结点。可能主要是我
们的下界估计不准确,而2和3用到了下界函数的缘故。这一个版本Baby-4中,我们屏蔽了
第2,3项措施。
好了,写了四个Baby版程序,想不想比较一下呢?不过我只对几个困难一点的数据感兴趣
。
关卡 实际步数 Baby-1 Baby-2 Baby-3 Baby-4
Kid 1 11 186476 60 52 38
Kid 16 7 3947 304 189 149
Kid 18 10 5108 46 41 31
Kid 35 16 11118 6504 704 462
Kid 50 11 14451 98 96 152
Kid 51 13 Too many Too many 629 54
Kid 52 18 Too many Too many 39841 97
Kid 54 16 24270 273 258 140
Kid 55 14 Too many Too many 4886 3390
Kid 56 14 3318 2225 1518 1069
Kid 60 15 Too many Too many 6916 5022
Wqx 4 26 97855 33923 33916 24251
Wqx 9 12 116927 4806 968 350
从上表可以看出,我们的优化总的来说是有效的,而且直观的看,那些改进不明显的很多
是因为下界估计比较差,这一点我们以后会继续讨论。不管怎样,这61关"幼儿关"过了58
关倒是挺不错的,至少可以说明我们程序的Baby版已经具有普通儿童的"智力"了^_^。不过
这只是个开头,好戏还在后头!
6.Baby-4源程序
程序S4BABY4.PAS在附件中,这里只是加了少量的注释。大家可以试试它的效果,但是没有
必要看得太仔细,因为在以后的章节中,我会改动很多东西,甚至连IDA*主程序框架都会
变得不一样。
常量定义:
const
{Version}
VerStr='S4 - SRbGa Super Sokoban Solver (Baby Version 4)';
Author='Written by Liu Rujia(SrbGa), 2001.2, Chongqing, China';
{Files}
InFile='soko.in';
OutFile='soko.out';
{Charactors}
Char_Soko='@';
Char_SokoInTarget='+';
Char_Box='$';
Char_BoxInTarget='*';
Char_Target='.';
Char_Wall='#';
Char_Empty=' ';
{Dimentions}
Maxx=21;
Maxy=21;
MaxBox=50;
{Directions}
Up=0;
Down=1;
Left=2;
Right=3;
DirectionWords:array[0..3] of string=('UP','DOWN','LEFT','RIGHT');
{Movement}
MaxPosition:integer=Maxx*Maxy;
Opposite:array[0..3] of integer=(1,0,3,2);
DeltaPos:array[0..3] of integer=(-Maxy,Maxy,-1,1);
我们把x,y坐标合成一个值position,其中position=(x-1)*maxy+(y-1)。这里用类型常量
是因为以后会根据地图的尺寸改变MaxPosition的值。Opposite就是相反方向例如Opposit
e[UP]:=DOWN;DeltaPos也是会重新设定的。我们在进行移动的时候只需要用:NewPos:=Ol
dPos+DeltaPos[Direction]就可以了,很方便。
{IDA Related}
MaxNode=1000000;
MaxDepth=100;
MaxStack=150;
DispNode=1000;
每生成多少个结点报告一次。
{HashTable}
MaxHashEntry=10000;
HashMask=10000;
MaxSubEntry=100;
{BitString}
BitMask:array[0..7] of byte=(1,2,4,8,16,32,64,128);
Infinite=Maxint;
类型定义:
type
PositionType=integer;
BitString=array[0..Maxx*Maxy div 8-1] of byte;
整个地图就是一个BitString。第position位为1当且仅当position位置有东西(如箱子,
目标,墙)。
MapType=array[1..Maxx] of string[Maxy];
BiGraph=array[1..MaxBox,1..MaxBox] of integer;
MazeType=
record
X,Y:integer;
Map:MapType;
GoalPosition:array[1..MaxBox] of integer;
BoxCount:integer;
Goals:BitString;
Walls:BitString;
end;
尺寸,原始数据(用来显示状态的),目标的BitString,箱子总数,目标位置(BitStri
ng和位置数组都用是为了加快速度)和Walls的BitString。
MoveType=
record
Position:integer;
Direction:0..3;
end;
Direction是箱子被推向的方向。
StateType=
record
Boxes:BitString;
ManPosition:PositionType;
MoveCount:integer;
Move:array[1..MaxDepth] of MoveType;
g,h:integer;
end;
IDAType=
record
TopLevelNodeCount:longint;
NodeCount:longint;
StartState:StateType;
PathLimit:integer;
Top:integer;
Stack:array[1..MaxStack] of StateType;
end;
Top是栈顶指针。
PHashTableEntry=^HashTableEntry;
HashTableEntry=
record
Next:PHashTableEntry;
State:StateType;
end;
PHashTableType=^HashTableType;
HashTableType=
record
FirstEntry:array[0..MaxHashEntry] of PHashTableEntry;
Count:array[0..MaxHashEntry] of byte;
end;
这些是Hash表相关类型。我们采用的是拉链法,这样可以利用指针申请到堆空间,结合保
护模式使用,效果更好。
var
HashTable:PHashTableType;
SokoMaze:MazeType;
IDA:IDAType;
procedure SetBit(var BS:BitString; p:integer);
begin
BS[p div 8]:=BS[p div 8] or BitMask[p mod 8];
end;
procedure ClearBit(var BS:BitString; p:integer);
begin
BS[p div 8]:=BS[p div 8] xor BitMask[p mod 8];
end;
function GetBit(var BS:BitString; p:integer):byte;
begin
if BS[p div 8] and BitMask[p mod 8]>0 then GetBit:=1 else GetBit:=0;
end;
这些是位操作,设置,清除和得到一个BitString的某一项。
procedure Init;
var
Lines:MapType;
procedure ReadInputFile;
var
f:text;
s:string;
begin
SokoMaze.X:=0;
SokoMaze.Y:=0;
SokoMaze.BoxCount:=0;
assign(f,infile);
reset(f);
while not eof(f) do
begin
readln(f,s);
if length(s)>SokoMaze.Y then
SokoMaze.Y:=length(s);
inc(SokoMaze.X);
Lines[SokoMaze.X]:=s;
end;
close(f);
end;
procedure AdjustData;
var
i,j:integer;
begin
for i:=1 to SokoMaze.X do
while length(Lines[i])<SokoMaze.Y do
Lines[i]:=Lines[i]+' ';
SokoMaze.Map:=Lines;
for i:=1 to SokoMaze.X do
for j:=1 to SokoMaze.Y do
if SokoMaze.Map[i,j] in [Char_BoxInTarget,Char_SokoInTarget,Char_Targe
t] then
SokoMaze.Map[i,j]:=Char_Target
else if SokoMaze.Map[i,j]<>Char_Wall then
SokoMaze.Map[i,j]:=Char_Empty;
调整Map数组,把箱子和搬运工去掉。
for i:=1 to SokoMaze.X do
for j:=1 to SokoMaze.Y do
if Lines[i,j] in [Char_Target,Char_BoxInTarget,Char_SokoInTarget] then
begin
inc(SokoMaze.BoxCount);
SokoMaze.GoalPosition[SokoMaze.BoxCount]:=(i-1)*SokoMaze.Y+j-1;
end;
统计Goal的个数和GoalPosition。
DeltaPos[Up]:=-SokoMaze.Y;
DeltaPos[Down]:=SokoMaze.Y;
MaxPosition:=SokoMaze.X*SokoMaze.Y;
根据地图尺寸调整DeltaPos和MaxPosition
end;
procedure ConstructMaze;
var
i,j:integer;
begin
fillchar(SokoMaze.Goals,sizeof(SokoMaze.Goals),0);
fillchar(SokoMaze.Walls,sizeof(SokoMaze.Walls),0);
for i:=1 to SokoMaze.X do
for j:=1 to SokoMaze.Y do
case Lines[i,j] of
Char_SokoInTarget, Char_BoxInTarget, Char_Target:
SetBit(SokoMaze.Goals,(i-1)*SokoMaze.Y+j-1);
Char_Wall:
SetBit(SokoMaze.Walls,(i-1)*SokoMaze.Y+j-1);
end;
end;
procedure InitIDA;
var
i,j:integer;
StartState:StateType;
begin
IDA.NodeCount:=0;
IDA.TopLevelNodeCount:=0;
fillchar(StartState,sizeof(StartState),0);
for i:=1 to SokoMaze.X do
for j:=1 to SokoMaze.Y do
case Lines[i,j] of
Char_Soko, Char_SokoInTarget:
StartState.ManPosition:=(i-1)*SokoMaze.Y+j-1;
Char_Box, Char_BoxInTarget:
SetBit(StartState.Boxes,(i-1)*SokoMaze.Y+j-1);
end;
StartState.g:=0;
IDA.StartState:=StartState;
new(HashTable);
for i:=1 to MaxHashEntry do
begin
HashTable^.FirstEntry[i]:=nil;
HashTable^.Count[i]:=0;
end;
end;
begin
ReadInputFile;
AdjustData;
ConstructMaze;
InitIDA;
end;
procedure PrintState(State:StateType);
var
i,x,y:integer;
Map:MapType;
begin
Map:=SokoMaze.Map;
x:=State.ManPosition div SokoMaze.Y+1;
y:=State.ManPosition mod SokoMaze.Y+1;
if Map[x,y]=Char_Target then
Map[x,y]:=Char_SokoInTarget
else
Map[x,y]:=Char_Soko;
for i:=1 to MaxPosition do
if GetBit(State.Boxes,i)>0 th