数据结构与算法分析:专题内容——人工智能中的寻路6之NegMax(代码详解)
一、算法描述
NegMax算法不再使用Minimax的MAX和MINI层级,而是每一级都使用同样的方法。这种思想也形成了另外一种算法,AlphaBeta,我们在稍后会讨论这个算法。在Minimax中,局面状态是一直从玩家的视角来观察,然后选择走法(需要存储棋子的信息以供评价函数使用)。
此算法不再将博弈树划分为多个层级,NegMax对走法采取一致的评价策略,从最坏的角度来评价子节点。从本质上来说,玩家走完之后,对手肯定会试着做出最好的决定,因此,算法指导玩家选择最好的走法,限制对手的可行走法。如果你比较Minimax和NegMax,你将会看到两个几乎相同的博弈树,唯一的区别就是局面状态是如何被评分的。注意,对玩家来说,两个可行走法中的第一个是最好的,因为这个走法对对手的限制最多。
二、复杂度分析
下图是一字棋游戏中,玩家O采取NegMax搜索时的追寻深度为2的轨迹。注意这里会扩展出所有可能的棋面状态,即使确定玩家X可以稳赢时。每个叶子局面状态的得分都是从玩家的角度出发。我们看到初始局面状态的得分是-2,因为这是所有其子节点的最大负分。
NegMax探测的状态数目和Minimax一样,如果追寻深度为d,每个局面状态的可行走法为b个,那么数目近似于bd。
最后我们可以看到NegMax是如何处理博弈树的叶子节点的。你同样可以在下例的代码中看到,这些叶子状态节点都是从玩家的角度出发,也就是说双亲节点从叶子节点选择的MoveEvaluation只是简单的最大化这些叶子节点的得分。
NegMax的操作好似流水线一般,它不需要来回切换MAX或者MIN层级。我们回忆一下Minimax是如何要求评估函数要从将要走棋的玩家的观点来进行评估的。在NegMax中,局面状态是基于走棋之后的玩家的视角。因为算法是选择“负分最大的子节点”。
三、适用情况
适用情况同Minimax算法。
NegMax是非常实用的算法,如果需要,可以扩展成AlphaBeta算法。因为每个棋面的得分都是负数,我们只需要仔细地区分胜利和失败状态。尤其是最小值必须是最大值的负数。注意Integer.MIN_VALUE(在Java中,这个定义为0x80000000或者-2147483648 )并不是Integer.MAX_VALUE(在Java中,这个定义为0x7fffffff或者2 147483647)的负值。因此,我们将最小值定义为Integer.MIN_VALUE+1 , 这 个 值 可 以 调 用 静 态 函 数MoveEvaluation.minimum()获得。从完整性来看,我们也提供了MoveEvaluation.maximum()函数。
四、算法实现
- 输入
算法从博弈树中的一个初始位置开始,假设它能够(a)选择某个局面状态的所有可行走法,以及(b)评价某个局面状态,表示从玩家来看这个状态的质量。得分越少则质量越差。算法会预测固定步数,这个数目叫做追寻深度。 - 输出
返回所有可行走法中最能使得玩家赢得比赛的走法,由评估函数决定。 - 假设
评估局面状态是非常复杂的,程序员需要依靠评估函数来选择最好的局面状态。事实上,在设计智能程序中,开发一个精确的评估函数,例如象棋、跳棋,或者黑白棋,是一件非常困难的事情。我们假设已经有一些这样的函数。
每一个MoveEvaluation的得分是简单地以走棋的玩家角度来评价棋面状态。这样能够简化算法的实现。
#include <iostream>
#include <vector>
#include <memory>
#include <limits>
#include <algorithm>
// Forward declarations
class IGameState;
class IGameMove;
class IPlayer;
class IEvaluation;
// Interface for game state
class IGameState {
public:
virtual std::shared_ptr<IGameState> copy() = 0;
virtual ~IGameState() = default;
};
// Interface for game move
class IGameMove {
public:
virtual void execute(std::shared_ptr<IGameState> state) = 0;
virtual void undo(std::shared_ptr<IGameState> state) = 0;
virtual ~IGameMove() = default;
};
// Interface for player
class IPlayer {
public:
virtual std::vector<std::shared_ptr<IGameMove>> validMoves(std::shared_ptr<IGameState> state) = 0;
virtual int eval(std::shared_ptr<IGameState> state) = 0;
virtual ~IPlayer() = default;
};
// Interface for evaluation
class IEvaluation {
public:
virtual std::shared_ptr<IGameMove> bestMove(std::shared_ptr<IGameState> s,
std::shared_ptr<IPlayer> player,
std::shared_ptr<IPlayer> opponent) = 0;
virtual ~IEvaluation() = default;
};
// Move evaluation class
class MoveEvaluation {
public:
std::shared_ptr<IGameMove> move;
int score;
// Constructor for leaf nodes (just score)
explicit MoveEvaluation(int score) : move(nullptr), score(score) {}
// Constructor for internal nodes (move and score)
MoveEvaluation(std::shared_ptr<IGameMove> m, int s) : move(m), score(s) {}
static int minimum() {
return std::numeric_limits<int>::min();
}
};
// NegMax evaluation class
class NegMaxEvaluation : public IEvaluation {
private:
std::shared_ptr<IGameState> state;
int ply;
MoveEvaluation negmax(int ply, std::shared_ptr<IPlayer> player,
std::shared_ptr<IPlayer> opponent) {
// Get valid moves for current player
auto moves = player->validMoves(state);
// If no moves or leaf node, return evaluation
if (ply == 0 || moves.empty()) {
return MoveEvaluation(player->eval(state));
}
// Initialize best move with minimum possible score
MoveEvaluation best(MoveEvaluation::minimum());
// Try all possible moves
for (const auto& move : moves) {
// Make move
move->execute(state);
// Recursively evaluate position from opponent's perspective
MoveEvaluation me = negmax(ply - 1, opponent, player);
// Undo move
move->undo(state);
// Update best move if we found a better score
if (-me.score > best.score) {
best = MoveEvaluation(move, -me.score);
}
}
return best;
}
public:
explicit NegMaxEvaluation(int searchPly) : ply(searchPly) {}
std::shared_ptr<IGameMove> bestMove(std::shared_ptr<IGameState> s,
std::shared_ptr<IPlayer> player,
std::shared_ptr<IPlayer> opponent) override {
state = s->copy();
MoveEvaluation me = negmax(ply, player, opponent);
return me.move;
}
};
// Example concrete implementations for testing
class SimpleGameState : public IGameState {
public:
std::vector<int> board;
SimpleGameState() : board(9, 0) {} // 3x3 board initialized with zeros
std::shared_ptr<IGameState> copy() override {
auto newState = std::make_shared<SimpleGameState>();
newState->board = this->board;
return newState;
}
};
class SimpleGameMove : public IGameMove {
public:
int position;
int player; // 1 or -1
SimpleGameMove(int pos, int p) : position(pos), player(p) {}
void execute(std::shared_ptr<IGameState> state) override {
auto gameState = std::dynamic_pointer_cast<SimpleGameState>(state);
gameState->board[position] = player;
}
void undo(std::shared_ptr<IGameState> state) override {
auto gameState = std::dynamic_pointer_cast<SimpleGameState>(state);
gameState->board[position] = 0;
}
};
class SimplePlayer : public IPlayer {
private:
int playerMark; // 1 or -1
public:
explicit SimplePlayer(int mark) : playerMark(mark) {}
std::vector<std::shared_ptr<IGameMove>> validMoves(std::shared_ptr<IGameState> state) override {
auto gameState = std::dynamic_pointer_cast<SimpleGameState>(state);
std::vector<std::shared_ptr<IGameMove>> moves;
for (int i = 0; i < 9; i++) {
if (gameState->board[i] == 0) {
moves.push_back(std::make_shared<SimpleGameMove>(i, playerMark));
}
}
return moves;
}
int eval(std::shared_ptr<IGameState> state) override {
// Simple evaluation: count pieces
auto gameState = std::dynamic_pointer_cast<SimpleGameState>(state);
int score = 0;
for (int val : gameState->board) {
if (val == playerMark) score++;
else if (val == -playerMark) score--;
}
return score;
}
};
int main() {
// Create game state
auto gameState = std::make_shared<SimpleGameState>();
// Create players
auto player1 = std::make_shared<SimplePlayer>(1);
auto player2 = std::make_shared<SimplePlayer>(-1);
// Create evaluator with search depth of 3
NegMaxEvaluation evaluator(3);
// Get best move for player1
auto bestMove = evaluator.bestMove(gameState, player1, player2);
if (bestMove) {
auto move = std::dynamic_pointer_cast<SimpleGameMove>(bestMove);
std::cout << "Best move for Player 1: position " << move->position << std::endl;
}
else {
std::cout << "No valid moves available" << std::endl;
}
return 0;
}
五、引用及参考文献
1.《算法设计手册》