对抗搜索和博弈 - 查表和缓存
查表
我们的AI算法在开局会有搜索空间过大的情况,所以不能及时去发现比较好的解。这个时候我们可以用人类的经验去辅助它。因为开局的格式比较小,我们可以很容易枚举完开局的前3步的可能性。这样可以很好的提高它在开局时的速度。
我们为了可以减少枚举的状态,可以做一个对旋转,翻转,平移的映射。这样可以使得本来需要枚举的情况可以极大的减少。比如说开局下斜着的4个角是等价。开局下水平和竖直也是等价。因为他们都可以通过旋转,平移,和翻转操作变化得到。
public enum PositionTranslator {
CLOCKWISE_0{
@Override
Position translate(int y, int x) {
return new Position(y, x);
}
@Override
Position deTranslate(int y, int x) {
return CLOCKWISE_0.translate(y, x);
}
}, CLOCKWISE_90{
@Override
Position translate(int y, int x) {
return new Position(-x, y);
}
@Override
Position deTranslate(int y, int x) {
return CLOCKWISE_270.translate(y, x);
}
}, CLOCKWISE_180{
@Override
Position translate(int y, int x) {
return new Position(-y,-x);
}
@Override
Position deTranslate(int y, int x) {
return CLOCKWISE_180.translate(y, x);
}
}, CLOCKWISE_270{
@Override
Position translate(int y, int x) {
return new Position(x, -y);
}
@Override
Position deTranslate(int y, int x) {
return CLOCKWISE_90.translate(y, x);
}
};
abstract Position translate(int y, int x);
abstract Position deTranslate(int y, int x);
public static int toDeltaX(int x) {
return x - 7;
}
public static int toDeltaY(int y) {
return 7 - y;
}
public static int toOriginX(int dx) {
return dx + 7;
}
public static int toOriginY(int dy) {
return 7 - dy;
}
public Position originDeTranslateThenOrigin(Position p) {
Position res = deTranslate(toDeltaY(p.y), toDeltaX(p.x));
return new Position(toOriginY(res.y), toOriginX(res.x), p.winning);
}
public Position originTranslateThenOrigin(int y, int x) {
Position res = translate(toDeltaY(y), toDeltaX(x));
return new Position(toOriginY(res.y), toOriginX(res.x));
}
public Piece originTranslateThenOrigin(Piece p) {
int y = p.getY(), x = p.getX();
Position res = originTranslateThenOrigin(y, x);
return new Piece(res.x, res.y, p.getPlayer());
}
public String translateToString(int dy, int dx) {
Position pos = translate(dy, dx);
return toString(toOriginY(pos.y) , toOriginX(pos.x));
}
public static String toString(int y, int x) {
assert y >= 0 && y < 15;
assert x >= 0 && x < 15;
String ys = (y + 1) + "";
char xs = (char) ('a' + x);
return xs + ys;
}
public static PositionTranslator select(int dy, int dx) {
if (dx > 0 && dy <= 0) return CLOCKWISE_0;
else if (dx >= 0 && dy > 0) return CLOCKWISE_90;
else if (dx < 0 && dy >= 0) return CLOCKWISE_180;
else if (dx <= 0 && dy < 0) return CLOCKWISE_270;
return CLOCKWISE_0;
}
public static PositionTranslator selectByOrigin(int y, int x) {
return select(toDeltaY(y), toDeltaX(x));
}
}
下面我们可以在开局的时候避免大量的搜索。这里我们可以去查一些基本棋谱,找到比较好的开局下法。然后这需要用一个坐标象限的枚举,然后算法会根据旋转,平移,翻转等操作找到正确的落子位置。
这个时候我们必须要维护2套棋盘,一个是内部计算用的棋盘,我们统一翻转到右下角的象限。另一个是展现给用户看的棋盘。是回归到用户下棋位置对应的AI落子。
举个例子,如果用户开始下在,中心点的左上。我们会在实际棋盘里,下在右下,然后去查棋谱,应该怎么下。如果查到应该下在正下,在实际棋盘里下正下。但是我们呈现给用户看的就是正上。
public class ChessboardByteArrayAlgoConsistent extends ChessboardByteArrayAlgo implements IConsistentAlgo {
@Getter
private PositionTranslator positionTranslator;
private Deque<Position> originAllSteps = new ArrayDeque<>();
public ChessboardByteArrayAlgoConsistent(int size) {
super(size);
}
@Override
public void setPiece(Piece piece) {
originAllSteps.offerLast(new Position(piece.getY(), piece.getX()));
if (allSteps.isEmpty()) {
if (piece.getY() != 7 || piece.getX() != 7) {
positionTranslator = PositionTranslator.selectByOrigin(piece.getY(), piece.getX());
} else {
super.setPiece(piece);
return;
}
}
if (positionTranslator == null && allSteps.size() == 1) {
positionTranslator = PositionTranslator.selectByOrigin(piece.getY(), piece.getX());
}
Piece np = positionTranslator.originTranslateThenOrigin(piece);
allSteps.offerLast(new Position(np.getY(), np.getX()));
location[getIdx(np.getY(),np.getX())] = (byte) piece.getPlayer().getId();
}
@Override
public boolean isTerminal(Piece piece) {
if (positionTranslator == null) return false;
Piece np = positionTranslator.originTranslateThenOrigin(piece);
return isTerminal(np.getX(), np.getY());
}
@Override
public boolean isLegalMove(Piece piece) {
if (positionTranslator == null) return true;
Piece np = positionTranslator.originTranslateThenOrigin(piece);
return isLegalMove(np.getX(), np.getY());
}
@Override
public ChessboardByteArrayAlgoConsistent clone() {
ChessboardByteArrayAlgoConsistent cloned = new ChessboardByteArrayAlgoConsistent(size);
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
cloned.setPiece(j, i, getValInBoard(j, i));
}
}
cloned.allSteps = new ArrayDeque<>(allSteps);
cloned.originAllSteps = new ArrayDeque<>(originAllSteps);
cloned.positionTranslator = positionTranslator;
return cloned;
}
@Override
public String generateStepsCode() {
return BoardToString.serialize(originAllSteps);
}
}
同时我们写一个算法,要求做到,只要满足表里的棋子样式(就是经过平移,翻转或旋转都是一个图案的情况下),找到对应的落子。
举个例子,比如表里的落子为:
1695821880828.png
当用户下成下图,我们应该可以查表找到对应红圈的位置
1695821942914.png
下面是算法实现
public class ConsistentPattern {
public static Position findBestMove(List<Position> shape, Map<String, Position> patchedKey) {
return findBestMoveInternal(shape.stream().map(i -> new int[]{i.x, i.y}).collect(Collectors.toList()), patchedKey);
}
private static Position findBestMoveInternal(List<int[]> shape, Map<String, Position> patchedKey) {
int[] blackXs = new int[(shape.size() + 1) >> 1];
int[] blackYs = new int[(shape.size() + 1) >> 1];
int[] whiteXs = new int[shape.size() >> 1];
int[] whiteYs = new int[shape.size() >> 1];
int[] blackOuts = new int[blackXs.length];
int[] whiteOuts = new int[whiteXs.length];
for (int rotateType = 0; rotateType < 8; ++rotateType) {
// 先旋转
fulfillXYs(shape, rotateType, blackXs, blackYs, whiteXs, whiteYs);
// 再位移
int[] myx = fulfillOuts(blackXs, blackYs, whiteXs, whiteYs, blackOuts, whiteOuts);
String candidate = getString(blackOuts, whiteOuts);
if (!patchedKey.containsKey(candidate)) continue;
Position bestRawMovePos = patchedKey.get(candidate);
int[] bestRawMove = new int[]{bestRawMovePos.x, bestRawMovePos.y};
// 先位移
bestRawMove[0] += myx[0];
bestRawMove[1] += myx[1];
// 再旋转
deRotate(rotateType, bestRawMove);
return new Position(bestRawMove[1], bestRawMove[0]);
}
return null;
}
private static void rotate(int rotateType, int[] bestRawMove) {
//x y, x -y, -x y, -x -y, y x, y -x, -y x, -y -x
int x = bestRawMove[0], y = bestRawMove[1];
int nx = rotateType <=1 ? x : rotateType <=3 ? -x : rotateType <=5 ? y : -y;
int ny = rotateType <=3 ? (rotateType %2==0 ? y : -y) : (rotateType %2==0 ? x : -x);
bestRawMove[0] = nx;
bestRawMove[1] = ny;
}
private static void deRotate(int rotateType, int[] bestRawMove) {
//after: x y, x -y, -x y, -x -y, y x, y -x, -y x, -y -x
//apply: x y, x -y, -x y, -x -y, y x, -y x, y -x, -y -x
//res : x y, x y, x y, x y, x y, x y, x y, x y
int x = bestRawMove[0], y = bestRawMove[1];
int nx = rotateType <=1 ? x : rotateType <=3 ? -x : rotateType %2 == 0 ? y : -y;
int ny = rotateType <=3 ? (rotateType %2==0 ? y : -y) : (rotateType <= 5 ? x : -x);
bestRawMove[0] = nx;
bestRawMove[1] = ny;
}
public static String canonical(List<Position> shape) {
return canonicalInternal(shape.stream().map(i -> new int[]{i.x, i.y}).collect(Collectors.toList()), new int[2]);
}
public static String canonical(List<Position> shape, int[] bestXandY) {
return canonicalInternal(shape.stream().map(i -> new int[]{i.x, i.y}).collect(Collectors.toList()), bestXandY);
}
private static String canonicalInternal(List<int[]> shape, int[] bestXandY) {
String ans = "";
int[] blackXs = new int[(shape.size() + 1) >> 1];
int[] blackYs = new int[(shape.size() + 1) >> 1];
int[] whiteXs = new int[shape.size() >> 1];
int[] whiteYs = new int[shape.size() >> 1];
int[] blackOuts = new int[blackXs.length];
int[] whiteOuts = new int[whiteXs.length];
int[] tmpBest = bestXandY.clone();
for (int rotateType = 0; rotateType < 8; ++rotateType) {
fulfillXYs(shape, rotateType, blackXs, blackYs, whiteXs, whiteYs);
int[] myx = fulfillOuts(blackXs, blackYs, whiteXs, whiteYs, blackOuts, whiteOuts);
String candidate = getString(blackOuts, whiteOuts);
if (ans.compareTo(candidate) < 0) {
ans = candidate;
int[] tmp = bestXandY.clone();
rotate(rotateType, tmp);
tmp[0] -= myx[0];
tmp[1] -= myx[1];
tmpBest = tmp;
}
}
bestXandY[0] = tmpBest[0];
bestXandY[1] = tmpBest[1];
return ans;
}
private static String getString(int[] blackOuts, int[] whiteOuts) {
String candidate = new StringBuilder(Arrays.toString(blackOuts)).append(":").append(Arrays.toString(whiteOuts)).toString() ;
return candidate;
}
private static int[] fulfillOuts(int[] blackXs, int[] blackYs, int[] whiteXs, int[] whiteYs, int[] blackOuts, int[] whiteOuts) {
int mx = blackXs[0], my = blackYs[0];
for (int x: blackXs) mx = Math.min(mx, x);
for (int x: whiteXs) mx = Math.min(mx, x);
for (int y: blackYs) my = Math.min(my, y);
for (int y: whiteYs) my = Math.min(my, y);
for (int j = 0; j < blackOuts.length; j++) {
blackOuts[j] = (blackYs[j] - my) * 15 + (blackXs[j] - mx);
}
for (int j = 0; j < whiteOuts.length; j++) {
whiteOuts[j] = (whiteYs[j] - my) * 15 + (whiteXs[j] - mx);
}
Arrays.sort(blackOuts);
Arrays.sort(whiteOuts);
return new int[]{mx, my};
}
private static void fulfillXYs(List<int[]> shape, int c, int[] blackXs, int[] blackYs, int[] whiteXs, int[] whiteYs) {
int t = 0;
for (int i = 0; i < shape.size(); i += 2) {
int[] z = shape.get(i);
int x = z[0], y = z[1];
//x y, x -y, -x y, -x -y, y x, y -x, -y x, -y -x
blackXs[t] = c <=1 ? x : c <=3 ? -x : c <=5 ? y : -y;
blackYs[t++] = c <=3 ? (c %2==0 ? y : -y) : (c %2==0 ? x : -x);
}
t = 0;
for (int i = 1; i < shape.size(); i += 2) {
int[] z = shape.get(i);
int x = z[0], y = z[1];
//x y, x -y, -x y, -x -y, y x, y -x, -y x, -y -x
whiteXs[t] = c <=1 ? x : c <=3 ? -x : c <=5 ? y : -y;
whiteYs[t++] = c <=3 ? (c %2==0 ? y : -y) : (c %2==0 ? x : -x);
}
}
}
有了这个我们可以快速的把一些开局棋谱,运用到各种维度上了。
Zobrist缓存
最后说一下缓存的优化。棋盘类里有个非常有名的缓存叫zobrist
Zobrist缓存(Zobrist Hashing)是一种在计算机博弈和搜索算法中常用的技术,用于高效地存储和检索游戏局面的信息,以提高搜索算法的性能。它的名称来自于其发明者Albert L. Zobrist,他是计算机博弈领域的先驱之一。
Zobrist缓存的主要思想是使用随机生成的哈希键(称为Zobrist键)来表示棋盘上每个局部位置的状态。这个哈希键通常是一个固定位数的二进制数或整数,它可以唯一地标识棋盘上的每种可能局面。Zobrist键的生成是随机的,但在程序的每次运行中都是固定的,以确保相同的局面生成相同的键。
Zobrist缓存的工作流程如下:
初始化:在程序启动时,为每个可能的棋盘局面生成随机的Zobrist键,通常是一个固定位数的二进制数。这些键被存储在一个称为Zobrist表的数据结构中。
计算局面哈希:对于给定的棋盘局面,通过将每个局部位置的Zobrist键按位异或(XOR)在一起来计算整个局面的哈希键。这个哈希键可以用来唯一地标识该局面。
缓存和检索:在搜索算法中,每次评估一个新的局面时,可以使用局面的哈希键来检查Zobrist表,看是否已经计算过这个局面的评估值。如果已经计算过,就可以直接从缓存中获取评估值,而不必重新计算。这大大提高了搜索算法的效率,特别是在深度搜索中。
更新缓存:如果在搜索过程中发现了一个新的局面,可以计算其评估值并将局面的哈希键与评估值存储在Zobrist表中,以供后续检索使用。
Zobrist缓存在博弈树搜索算法(如博弈树搜索、Alpha-Beta剪枝、Minimax等)中广泛应用,因为它能够有效地减少了冗余计算,提高了搜索的速度。然而,需要注意的是,Zobrist缓存可能存在哈希冲突,即不同的局面生成相同的哈希键,因此需要一种方法来处理冲突,通常是通过使用开放寻址法或链表来解决。
下面是代码实现:
public class Zobrist {
public static int DISABLE_CACHE_MASK = 100_000_000;
private final long[] zobristTable;
@Getter
private Map<Long, ResultAndDepth> transpositionTable;
@Getter
private long hash = 0;
@Getter
private int cacheMatch = 0;
private int size;
public Zobrist(IChessboardAIAlgo chessBoardAlgo) {
size = chessBoardAlgo.getSize();
zobristTable = new long[size * size * 2];
transpositionTable = new HashMap<>();
SecureRandom secureRandom = new SecureRandom();
for (int i = 0; i < zobristTable.length; i++) {
zobristTable[i] = secureRandom.nextLong();
}
calculateInitialHash(chessBoardAlgo);
}
public void updateHash(int y, int x, int role) {
if (role < 1 || role > 2) {
throw new IllegalArgumentException("Invalid move");
}
hash ^= zobristTable[(y * size + x) * 2 + (role - 1)];
}
private void calculateInitialHash(IChessboardAIAlgo chessBoardAlgo) {
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
int i = y * size + x;
int val = chessBoardAlgo.getValInBoard(x, y);
if (val != 0) {
hash ^= zobristTable[(i << 1) + (val - 1)];
}
}
}
}
public Optional<Integer> tryGet(int depth) {
if (!transpositionTable.containsKey(hash)) return Optional.empty();
ResultAndDepth scoreAndDepth = transpositionTable.get(hash);
if (scoreAndDepth.depth >= depth) {
cacheMatch++;
return Optional.of(scoreAndDepth.result);
}
return Optional.empty();
}
public int setAndReturnScore(int result, int depth) {
if (Math.abs(result) != DISABLE_CACHE_MASK)
transpositionTable.put(hash, new ResultAndDepth(result, depth));
return result;
}
private class ResultAndDepth {
int result;
int depth;
public ResultAndDepth(int result, int depth) {
this.result = result;
this.depth = depth;
}
}
}
优化算杀深度
要做出五子棋强力AI,算杀模块真的非常重要;如果他能在规定时间算出杀解,那么基本就很早可以胜券在握了。五子棋无禁手黑棋有着很大优势。所以我们要利用好查表和算杀,就可以基本做出一个黑棋先手必胜的AI了。
那么我们上一版设计的算杀AI会有2个缺陷,他可以算出杀解,但是我们指定深度,对手可以通过不断冲四,去消耗掉我们的深度,最终会让我们的算法无法判断是否可以杀棋,而返回不能算杀的结论。
那么要优化这一个缺陷的技术,叫单步拓展。也就是如果对手步数是冲四,我们就不消耗递归的层数。但是这样就会造成时间上的极大退化。
原因是因为,假设对手有2步冲四,算杀过程中,这2步冲四,可以插进其他任意的算杀步子里。这样排列组合的结果非常大。假设算杀需要15步棋,那么2个冲四,可以任意使用,大概会多17 * 18 的复杂度的提高。让原来1秒的计算,变成5分钟。当然冲四更多,则计算让费更大。
这时ZOBRIST缓存 就发挥了很好的效果。因为他只记录棋盘状态,不管你算杀是发生在哪一步,如果这个局面之前计算过,他就可以提早剪枝。
下面我们来看下带ZOBRIST缓存的算杀代码。
public class VCX extends RecursiveBaseAIAlgo implements IWinningAlgo {
private final int TIME_LIMIT_MS = 55_000;
private int nextPoint = -1;
private final long[] zobristTable;
private long hash = 0;
private long startTime = 0;
private Map<Long, Boolean> zobristCache;
private int firstDepth;
private int timeFactor;
private VCXCachedScoreManager vcxCachedScoreManager;
@Setter
private DebugContext debugContext = DebugContext.DISABLE;
public VCX(IChessboardAIAlgo chessBoardAlgo, int humanColor) {
this(chessBoardAlgo, humanColor, 23, VCXOptimization.FAST);
}
public VCX(IChessboardAIAlgo chessBoardAlgo, int humanColor, int firstDepth) {
this(chessBoardAlgo, humanColor, firstDepth, VCXOptimization.FAST);
}
public VCX(IChessboardAIAlgo chessBoardAlgo, int humanColor, int firstDepth, VCXOptimization killOptimization) {
super(chessBoardAlgo, new VCXCachedScoreManager(chessBoardAlgo, humanColor, killOptimization), humanColor);
this.vcxCachedScoreManager = (VCXCachedScoreManager) scoreManager;
this.firstDepth = firstDepth;
this.timeFactor = killOptimization.factor;
zobristTable = new long[size * size * 2];
SecureRandom secureRandom = new SecureRandom();
for (int i = 0; i < zobristTable.length; i++)
zobristTable[i] = secureRandom.nextLong();
}
@Override
public boolean isAbsoluteForcedWin() {
return true;
}
@Override
public Position aiFindPos() {
nextPoint = -1;
int oriFirstDepth = firstDepth;
startTime = System.currentTimeMillis();
// 迭代加深
for (int i = Math.max(oriFirstDepth - 16, 5); i <= oriFirstDepth; i += 4) {
firstDepth = i;
zobristCache = new HashMap<>();
if (aiWin(aiColor, i, -1)) break;
if ((System.currentTimeMillis() - startTime) * timeFactor > TIME_LIMIT_MS) break;
}
firstDepth = oriFirstDepth;
if (nextPoint == -1)
return Position.EMPTY;
return new Position(getY(nextPoint), getX(nextPoint), true);
}
// lastMaxPoint, 为了优化性能, 方便剪枝
boolean aiWin(int role, int depth, int lastMaxPoint) {
// 因为会根据lastMaxPoint 进行进攻剪枝,所以CACHE里的输不一定是输
Boolean cached = zobristCache.get(hash);
if (cached != null) return cached;
if (depth <= 0) return setAndReturn(false);
List<Long> candidates = vcxCachedScoreManager.findAIKillSteps(lastMaxPoint);
if (!candidates.isEmpty() && score(candidates.get(0)) >= Score.FOUR.value) {
if (depth == firstDepth)
nextPoint = pos(candidates.get(0));
return setAndReturn(true);
}
if (candidates.isEmpty()) return setAndReturn(false);
debugContext.debugStartInfo(depth, firstDepth);
int maxPoint = -1;
for (long p : candidates) {
if (Thread.currentThread().isInterrupted()) return false;
if (System.currentTimeMillis() - startTime > TIME_LIMIT_MS) return false;
int pos = pos(p), score = score(p);
if (!debugContext.isInDebugStep(depth, firstDepth, pos)) continue;
int y = getY(pos), x = getX(pos);
addPiece(y, x, pos,true);
if (score > -Score.FIVE.value) {
maxPoint = pos;
}
boolean humanLose = humanLose(Player.enemyColor(role), depth - 1, maxPoint);
debugContext.debugResultInfo(depth, y, x, firstDepth, String.format("human lose: %s", humanLose));
removePiece(y, x, pos, true);
if (humanLose) {
if (depth == firstDepth)
nextPoint = pos;
return setAndReturn(true);
}
}
return setAndReturn(false);
}
private boolean humanLose(int role, int depth, int lastMaxPoint) {
Boolean cached = zobristCache.get(hash);
if (cached != null) return cached;
// 超过回合数,代表防守成功
if (depth <= 0) return setAndReturn(false);
List<Long> candidates = vcxCachedScoreManager.findHumanDefendSteps();
// 如果发现对面没有进攻手段(活三,冲四,活四),则代表防守成功
if (candidates.isEmpty()) return setAndReturn(false);
// 如果对面能成五,发现自己有成五;
// 如果对面不能成五,发现自己有活四;
if (-1 * score(candidates.get(0)) >= Score.FOUR.value)
return setAndReturn(false);
debugContext.debugStartInfo(depth, firstDepth);
for (long p : candidates) {
int pos = pos(p);
if (!debugContext.isInDebugStep(depth, firstDepth, pos)) continue;
int y = getY(pos), x = getX(pos);
addPiece(y, x, pos, false);
boolean aiWin = aiWin(Player.enemyColor(role), depth - 1, lastMaxPoint);
debugContext.debugResultInfo(depth, y, x, firstDepth, String.format("ai Win:%s",aiWin));
removePiece(y, x, pos, false);
if (!aiWin) return setAndReturn(false);
}
return setAndReturn(true);
}
protected void addPiece(int y, int x, int nextStep, boolean isAI) {
int color = isAI ? aiColor : humanColor;
hash ^= zobristTable[(nextStep << 1) | (color - 1)];
super.addPiece(y, x, isAI);
}
protected void removePiece(int y, int x, int nextStep, boolean isAI) {
int color = isAI ? aiColor : humanColor;
hash ^= zobristTable[(nextStep << 1) | (color - 1)];
super.removePiece(y, x, isAI);
}
private boolean setAndReturn(boolean res) {
zobristCache.put(hash, res);
return res;
}
}
迭代加深
上面算杀过程中还运用了一个提速的技巧,如果我们7步可以算出杀解,要是一开始走错了路子。他会搜的非常深,其实那个正确的路子只要7步。这时可以用迭代加深的搜索来解决这个问题。
迭代加深算法(Iterative Deepening Search,简称IDS)是一种用于解决搜索问题的深度优先搜索(Depth-First Search,DFS)算法的改进版本。它的主要思想是通过不断增加搜索深度来逐渐扩展搜索范围,直到找到解决方案或达到最大搜索深度为止。IDS结合了DFS的简单性和广度优先搜索(Breadth-First Search,BFS)的逐层扩展特性,因此通常用于在有限搜索空间中找到解决方案。
IDS的基本工作流程如下:
从初始节点开始,设置初始搜索深度为1。
使用深度优先搜索(DFS)来探索搜索树,但限制搜索深度不超过当前设定的深度。
如果在当前深度下找到了解决方案,就停止搜索并返回解决方案。
如果在当前深度下没有找到解决方案,增加搜索深度,并回到步骤2,继续搜索。
重复步骤2到步骤4,直到找到解决方案或达到最大搜索深度为止。
IDS的优点包括:
完备性:IDS保证会找到解决方案,如果解存在于搜索空间中。这是因为它逐渐增加搜索深度,最终会探索整个搜索树。
空间效率:与BFS不同,IDS不需要在内存中存储整个搜索树,只需要存储当前深度的节点,因此对内存的要求较低。
可控制的搜索深度:IDS允许你在搜索过程中控制搜索的深度,这在处理搜索空间大小未知或不确定的情况下很有用。
然而,IDS的主要缺点是其多次重复搜索相同的节点,因为每次增加深度时都会重新搜索之前的部分。这可能会导致性能问题,特别是在搜索空间庞大的情况下。
但是这个额外性能开销不算高。原因是因为对于许多状态空间,大多数节点位于底层。所以上层的重复其实并不重要。经过我自己测试,如果搜不到解,使用了迭代加深的时间开销只多15%,但是在有解的情况下,他能极快的返回结果。