第三部分答案:正如您所看到的,遗传算法
通常不是难点。再次强调,它只是一段简单的代码,真正的目的是为了测试另一段代码——演员。在这里,演员由一个Shooter
类实现。这些演员通常以图灵机的方式建模,也就是说,演员对于一组输入有一组定义好的输出。遗传算法帮助您确定状态表
的最佳配置。在此问题的先前答案中,Shooter
实现了一个概率矩阵,就像@SalvadorDali在他的答案中描述的那样。
经过充分测试之前的Shooter
,我们发现它能做到的最好成绩大约是:
BEST Ave=5, Min=3, Max=9
Best=Shooter:5:[(1,0), (0,0), (2,0), (-1,0), (-2,0), (0,2), (0,1), (0,-1), (0,-2), (0,1)]
这表明平均需要5次射击,最少3次,最多9次才能击沉一艘3X3的战舰。9次射击的位置显示为X/Y坐标对。问题“是否可以做得更好?”取决于人类的机智。遗传算法不能为我们编写新的演员。我想知道一个决策树
是否比概率矩阵更好,所以我实现了一个来尝试一下:
public class Branch {
private static final int MAX_DEPTH = 10;
private static final int MUTATE_PERCENT = 20;
private Branch hit;
private Branch miss;
private Position shot;
public Branch() {
shot = new Position(
(int)((Math.random()*6.0)-3),
(int)((Math.random()*6.0)-3)
);
}
public Branch(Position shot, Branch hit, Branch miss) {
this.shot = new Position(shot.x, shot.y);
this.hit = null; this.miss = null;
if ( hit != null ) this.hit = hit.clone();
if ( miss != null ) this.miss = miss.clone();
}
public Branch clone() {
return new Branch(shot, hit, miss);
}
public void buildTree(Counter c) {
if ( c.incI1() > MAX_DEPTH ) {
hit = null;
miss = null;
c.decI1();
return;
} else {
hit = new Branch();
hit.buildTree(c);
miss = new Branch();
miss.buildTree(c);
}
c.decI1();
}
public void shoot(Ship ship, Counter c) {
c.incI1();
if ( ship.madeHit(shot)) {
if ( c.incI2() == ship.getSize() ) return;
if ( hit != null ) hit.shoot(ship, c);
}
else {
if ( miss != null ) miss.shoot(ship, c);
}
}
public void mutate() {
if ( (int)(Math.random() * 100.0) < MUTATE_PERCENT) {
shot.x = (int)((Math.random()*6.0)-3);
shot.y = (int)((Math.random()*6.0)-3);
}
if ( hit != null ) hit.mutate();
if ( miss != null ) miss.mutate();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(shot.toString());
if ( hit != null ) sb.append("h:"+hit.toString());
if ( miss != null ) sb.append("m:"+miss.toString());
return sb.toString();
}
}
Branch
类是一个决策树
中的节点
(好吧,可能命名不太好)。在每次射击时,下一个选择的分支取决于该射击是否命中。
射手被修改以使用新的decisionTree
。
public class Shooter implements Comparable<Shooter> {
private Branch decisionTree;
private int aveScore;
public Shooter newShots() {
decisionTree = new Branch();
Counter c = new Counter();
decisionTree.buildTree(c);
return this;
}
public int testShooter(Ship ship) {
Counter c = new Counter();
decisionTree.shoot(ship, c);
return c.i1;
}
@Override
public int compareTo(Shooter o) {
return o.aveScore - aveScore;
}
public void mutate(Branch pDecisionTree) {
decisionTree = pDecisionTree.clone();
decisionTree.mutate();
}
public int getAveScore() {
return aveScore;
}
public void setAveScore(int aveScore) {
this.aveScore = aveScore;
}
public Branch getDecisionTree() {
return decisionTree;
}
@Override
public String toString() {
StringBuilder ret = new StringBuilder("Shooter:"+aveScore+": [");
ret.append(decisionTree.toString());
return ret.append(']').toString();
}
}
细心的读者会注意到,虽然方法本身发生了变化,但是一个
Shooter
需要实现哪些方法与之前的
Shooters
并没有区别。这意味着主要的
GA
模拟除了与突变相关的一行代码外,并没有发生变化,而且这可能还可以改进:
Shooter child = shooters.get(l);
child.mutate( shooters.get(NUM_SHOOTERS - l - 1).getDecisionTree());
一个典型模拟运行的图像现在看起来像这样:
![Ave. Score vs Generation](https://istack.dev59.com/iTIy6.webp)
正如您所看到的,使用决策树
进化出的最终最佳平均分数比使用概率矩阵
进化出的最佳平均分数少了一次射击。同时请注意,这组射手
训练到最佳状态需要约800代,大约是简单概率矩阵射手
所需时间的两倍。最佳决策树射手
的结果如下:
BEST Ave=4, Min=3, Max=6
Best=Shooter:4: [(0,-1)h:(0,1)h:(0,0) ... ]
在这里,平均数不仅少了一杆,而且最大杆数比概率矩阵射手
低1/3。
此时需要一些非常聪明的人来确定这个演员是否已经达到了问题域的理论最优解,也就是说,这是您在尝试击沉3X3船时能做到的最好的吗?考虑到这个问题的答案在真正的战舰游戏中会变得更加复杂,因为它有几种不同大小的船只。您将如何构建一个演员,将已经被击沉的船只的知识纳入到随机选择和动态修改
的行动中?这就是理解图灵机(也称为CPU)的重要性所在。
附注> 您还需要这个类:
public class Counter {
int i1;
int i2;
public Counter() {i1=0;i2=0;}
public int incI1() { return ++i1; }
public int incI2() { return ++i2; }
public int decI1() { return --i1; }
public int decI2() { return --i2; }
}