园友Master_Chivu在文章[Drops 十滴水] 强大的搜索(中)提起了十滴水这个游戏的搜索问题,给人启发颇深。Master_Chivu提到启发式策略可能会提高搜索效率,因为他本人也是用“裸搜”的。刚好我是学智能计算的,接着园友的启发,心想,何不试试智能算法呢?
经过权衡,最后决定用带精英保留策略的遗传算法求解。别的语言用不熟,还是最熟悉的Matlab上。开始写程序,却发现头疼的事来了。算法本身的步骤不难,但是写程序模拟棋盘上的动态可对我这种开发菜鸟是个问题。仔细分析了很久,最后决定用一个6×6矩阵A存储棋盘状态,然后用一个3×?的矩阵W存储所有飞行的水滴状态。
W第一行是水滴位置横坐标,第二是纵坐标,第三行是速度,用1,2,3,4表示上,右,下,左。(原谅我无知,用OOP可能只要实例化一个飞行水滴类就好了,但咱也就Matlab用的熟)。
当用户操作后,将放置水滴的位置,也就是矩阵A中相应的元素加一,如果大于4了(表示爆了),就在四周产生四个水滴,如果没爆,就直接把改变了状态的矩阵A返回。由于貌似一个值为4的大水滴,在不同方向同时来多个水滴情况下,也只会爆出四个小水滴,所以采用每一回合全部刷新飞行水滴位置后,如果该位置有水滴,则棋盘上相应位置处水滴数加一,并销毁W矩阵中相应的一列。待全部刷新完后,再根据棋盘上水滴的状态生成新的水滴(只要大于4就爆,不论是5,6还是7)。水滴碰撞解决了,可是到达边缘的碰撞还没处理。想了一下,最后用一个简单的办法:假设棋盘是8×8的(注意不是6×6),即在原来棋盘基础上,四周各加一条,让水滴飞吧,位置更新全部结束后检查棋盘四个边,发现有水滴进入就销毁之。
棋盘模拟代码如下:
% Ten Drops
function
[A]
=
Ten_Drops(
A,x,y)
[
N1
N2
]
=
size(
A);
AL
=
zeros(
N1
+
2
,
N1
+
2);
for
i
=
1
:
N1
for
j
=
1
:
N1
AL(
i
+
1
,
j
+
1)
=
A(
i
,
j);
end
end
AL(
x
+
1
,
y
+
1)
=
AL(
x
+
1
,
y
+
1)
+
1;
W
=
[];
numDrops
=
0;
flag
=
0;
while
flag
==
0
for
i
=
2
:
N1
+
1
for
j
=
2
:
N1
+
1
if
AL(
i
,
j)
>
4
W
=
[
W
[
i
-
1;
j;
1
]];
numDrops
=
numDrops
+
1;
% One drop added
W
=
[
W
[
i;
j
+
1;
2
]];
numDrops
=
numDrops
+
1;
W
=
[
W
[
i
+
1;
j;
3
]];
numDrops
=
numDrops
+
1;
W
=
[
W
[
i;
j
-
1;
4
]];
numDrops
=
numDrops
+
1;
AL(
i
,
j)
=
0;
end
end
end
if
numDrops
==
0
break
end
k
=
1;
while
k
<
=
numDrops
% Every drop operates just once
if
AL(
W(
1
,
k
),
W(
2
,
k))
~=
0
AL(
W(
1
,
k
),
W(
2
,
k))
=
AL(
W(
1
,
k
),
W(
2
,
k))
+
1;
W
(:,
k)
=
[];
numDrops
=
numDrops
-
1;
% One drop depolyed
else
switch
W(
3
,
k)
% Update water drop velocity
case
1
W(
1
,
k)
=
W(
1
,
k)
-
1;
case
2
W(
2
,
k)
=
W(
2
,
k)
+
1;
case
3
W(
1
,
k)
=
W(
1
,
k)
+
1;
case
4
W(
2
,
k)
=
W(
2
,
k)
-
1;
end
k
=
k
+
1;
end
end
if
numDrops
~=
0
k
=
1;
while
k
<
=
numDrops
if
W(
1
,
k)
<
=
1 ||
W(
1
,
k)
>
=
N1
+
2 ||
W(
2
,
k)
<
=
1 ||
W(
2
,
k)
>
=
N1
+
2
W
(:,
k)
=
[];
numDrops
=
numDrops
-
1;
else
k
=
k
+
1;
end
end
end
if
numDrops
==
0
flag
=
1;
else
flag
=
0;
end
for
i
=
2
:
N1
+
1
for
j
=
2
:
N1
+
1
if
AL(
i
,
j)
>
4
flag
=
0;
end
end
end
end
for
i
=
1
:
N1
for
j
=
1
:
N1
A(
i
,
j)
=
AL(
i
+
1
,
j
+
1);
end
end
return
乎,没有Matlab高亮插件啊,我非主流了。。。空格敲的我好累。
下面,用遗传算法求解最优放水序列。。。关于这个问题的遗传算法求解,更详细的讨论在这里:使劲点我!
首先AA用来存储初始棋盘状态(就是你要求解的问题),N最大序列长度,D种群规模,Ps选择概率,Pc交叉概率,Pm变异概率,iter迭代序号。
1)生成初始种群(随机),这个种群就是备选序列的集合,每两行代表一个序列,奇数为横坐标,偶数为纵坐标。
2)把每个序列都放到Ten_Drops里跑一下,求出每个序列需要多少步才能把棋盘清空(多余出的序列不起作用)。
3)按照序列的清零步长对候选的序列排序,保留最佳序列,剩下的用轮盘赌方法按一定概率选出(当然步长短的序列被选出的概率大)。选择完以后,没被选中的序列,很不幸,被淘汰了,取而代之的是随机初始化的新序列(但是总的种群规模不变,还是D)。
4)按照交叉概率从经过选择的种群中随机选出偶数对备选序列,并且在序列中任意一点为限,交换相邻的序列(继承优良的基因)。
5)按照变异概率随机选择备选序列,将其中一个坐标组合初始化。
6)如果达到最大迭代次数,则到下一步,否则返回第2步.
7)将备选序列按顺序放到Ten_Drops里跑一边,选取最短的输出。
代码如下(还是敲了很多遍空格)
% GA for Water Drops
% Searching for optimum drop sequence
clear
all
N
=
25;
D
=
20;
Ps
=
0
.
6;
Pc
=
0
.
4;
Pm
=
0
.
2;
AA
=
[
4 2 1 0 3 1;
0 0 2 2 4 1;
2 0 0 2 0 3;
0 1 3 0 4 3;
2 4 3 2 0 1;
0 1 2 1 1 2
];
%--------------Generate Population--------------
XX
=
ceil(
6
*
rand(
2
*
D
,N));
%--------------Evaluate Fittness-----------------
for
iter
=
1
:
1000
if
rem(
iter
,
50)
==
0
disp
([
'iteration: '
,
num2Str(
iter
)]);
end
que
=
zeros(
1
,
D);
for
i
=
1
:
D
X
=
XX(
2
*
i
-
1
:
2
*
i
,:);
A
=
AA;
for
j
=
1
:N
if
sum(
sum(
A))
~=
0
que(
i)
=
que(
i)
+
1;
A
=
Ten_Drops(
A
,
X(
1
,
j
),
X(
2
,
j));
end
end
end
%-------------Sellection------------------------
[
ord
ind
]
=
sort(
que);
roller
=
D
./
que
/(
sum(
D
./
que));
for
i
=
2
:
D
roller(
i)
=
roller(
i)
+
roller(
i
-
1);
end
XXnew
=
[];
for
i
=
1
:
1
% Preserve the best
XXnew
=
[
XXnew;
XX(
2
*
ind(
i)
-
1
:
2
*
ind(
i
),:)];
end
for
i
=
2
:
round(
D
*Ps)
r
=
rand;
j
=
1;
while
roller(
j)
<
r
j
=
j
+
1;
end
XXnew
=
[
XXnew;
XX(
2
*
j
-
1
:
2
*
j
,:)];
end
XXnew
=
[
XXnew;
ceil(
6
*
rand(
2
*(
D
-
round(
D
*Ps
)),N
))];
XX
=
XXnew;
%---------------Crossover-----------------------
XXnew
=
[];
for
i
=
1
:
1
% Preserve the best indivadual
XXnew
=
[
XXnew;
XX(
2
*
ind(
i)
-
1
:
2
*
ind(
i
),:)];
XX(
2
*
ind(
i)
-
1
:
2
*
ind(
i
),:)
=
[];
end
K
=
round(
D
*
Pc);
for
i
=
2
:
round(
D
*
Pc)
r
=
ceil(
K
*
rand);
XXnew
=
[
XXnew;
XX(
2
*
r
-
1
:
2
*
r
,:)];
XX(
2
*
r
-
1
:
2
*
r
,:)
=
[];
K
=
K
-
1;
end
for
i
=
2
:
round(
D
*
Pc)
/
2
r
=
ceil(N
*
rand);
temp
=
XXnew(
4
*
i
-
5
:
4
*
i
-
4
,
r
:N);
XXnew(
4
*
i
-
5
:
4
*
i
-
4
,
r
:N)
=
XXnew(
4
*
i
-
3
:
4
*
i
-
2
,
r
:N);
XXnew(
4
*
i
-
3
:
4
*
i
-
2
,
r
:N)
=
temp;
clear
temp;
end
XXnew
=
[
XXnew;
XX
];
XX
=
XXnew;
%-------------------Mutation---------------------
for
i
=
2
:
round(
D
*
Pm)
r1
=
ceil((
D
-
1)
*
rand)
+
1;
r2
=
ceil(N
*
rand);
XX(
2
*
r1
-
1
:
2
*
r1
,
r2)
=
ceil(
6
*
rand(
2
,
1));
end
end
% End of iteration
%----------------Reorder-------------------------
que
=
zeros(
1
,
D);
for
i
=
1
:
D
X
=
XX(
2
*
i
-
1
:
2
*
i
,:);
A
=
AA;
for
j
=
1
:N
if
sum(
sum(
A))
~=
0
que(
i)
=
que(
i)
+
1;
A
=
Ten_Drops(
A
,
X(
1
,
j
),
X(
2
,
j));
end
end
end
[
ord
ind
]
=
sort(
que);
Xopt
=
XX(
2
*
ind(
1)
-
1
:
2
*
ind(
1
),:);
minQue
=
ord(
1);
当然最后的两个值就是我们想要的,Xopt是最优序列,minQue是清零步长。实际上就是每次按照Xopt中一列所指示的坐标给棋盘加上一滴水,当到达minQue显示的步骤时,一定会把棋盘清空的。
这个程序算出来的序列有时候很奇特,可能前面10步一个水滴都消不掉,最后一滴加上去,Wala,满屏幕都是水。。。汗一个。
16级的时候
我的机器迅驰1.73,2G进化1000代大概需要30秒,生成的解基本优良。level10一下基本都是6,7步,level10~level20就需要十多步了(也不稳定,有时候算出来11,12,有时候30,那是无解的情况),level20以上更不好讲,但总的来说还是可以用。
这个游戏感觉难度不一定,有时候碰到比较难的局,搜几次都是最大步长,但是这局坚持过去了,下一句可能10步就完成了。但过了20还是挺费事。有时候出不了解就自己在棋盘上先想办法爆几个水滴,然后将这个状态作为初始值再求解一下,很多时候可以绕过那些让解求不出来的为止的因素。
总之,遗传算法并不能保证每次都能给出最优解,甚至都不保证能给出解(数学上说是概率收敛),但是有很大的几率给出一个相对满意的解,用来玩游戏还是可以应付了。
另外,适应度函数我直接用了清零步长的倒数,但是这个没有考虑清零过程中产生的奖励(连续爆三个加一滴水),如果算上奖励,那就应该用奖励水滴-步长=剩余水滴,并以剩余水滴最大化为目标进行搜索,这样才符合游戏的初衷。但是我对游戏本身设置的奖励机制不太清楚,是每隔三个爆就奖励呢,还是必须连续起来。要是连续的话除了第三个有奖,其余的呢。由于真正飞起来水滴太多,光听见加分的声音,也不知道是第几个爆了加的分,这个就确实不好掌握了。
算法本身也不完美,各种改进策略很多的,懒得用了。话说目前DE是最快的启发搜索算法,但是向量差分在这个序列中怎么实现还不可知,期待牛人实现一下。
达到25级了,貌似水滴不太够了,先不玩了
晚上继续,死在这一关了,这个算法还不是无敌的。。。