A*(Astar)算法详解与应用

算法背景

A*(A-Star)算法是一种在图形平面上,有多个节点的路径中,求出最低通过成本的算法。其历史可以追溯到早期的图搜索算法,如Dijkstra算法和贪心最佳优先搜索(Greedy Best-First Search)。是人工智能、游戏开发、机器人路径规划等领域中最经典、最有效的寻路算法之一。

算法解释

A*算法的核心思想是结合了Dijkstra算法和Best-First-Search(BFS)算法的特点。他给每个格子赋予了一个权重 f(n) = g(n) + h(n) ,通过权重来决定遍历的顺序。

详细解释

首先得理解广度优先搜索(BFS),在方格中,BFS通过遍历起始格子上下左右的四个格子,将其压入一个队列 ,通过循环 出队-搜索-入队 来寻找最短路。

而A*算法则是在BFS的基础上使用了贪心策略,给每个格子赋予一个权重f(n),其中f(n) = g(n) + h(n) 。

  • g(n)为起点到当前点的最短距离

  • h(n)为当前点到终点的估计距离

A*算法则使用了优先队列 ,通过寻找路径代价最小的节点来寻找目标点,它保证了如果h (n )的估计值是准确的,那么找到的路径也是最短的。同时,A*算法比BFS减少了遍历点的数量,加快了路线寻找的速度。

【很好理解,当你要从一个地方到另一个地方,并且你已经知道终点的方位,及时没有导航,你很自然会优先朝着终点方向前进,即使和道路方向并不相同】

下图是每个格子的 g(n) 也就是当前距离起点的步数,这个很好理解。

下图是每个格子的 h(n) ,也就是每个格子距离终点的估计距离 ,下图使用了曼哈顿距离

你会发现,在上图没有障碍的时候,每个格子的权重是一样的,无法体现路线的优化。如果使用欧几里得距离 ,就能体现出权重的作用,如下图:

距离的估计

有很多计算方式,比如:

1.曼哈顿距离

  • 定义:只能沿着网格的横纵方向移动,不能斜向移动。
  • 适用场景:城市街区网格、只能走直线(上下左右)的环境,比如A*算法中常用的启发函数

  • 特点:简单、计算快,但可能高估实际距离。

2.欧几里得距离:

  • 定义:两点之间的"直线距离",也就是我们日常生活中说的"最短距离"。
  • 适用场景:连续空间、物理世界中的距离计算,比如机器人导航、图像识别。

  • 特点:真实距离,但计算稍慢,且可能包含浮点运算。

3. 切比雪夫距离:

  • 定义:允许八方向移动(上下左右+对角线),取各坐标差值的最大值。
  • 适用场景:可以斜向移动的游戏地图(如象棋中的国王移动),或某些特殊路径规划。

  • 特点:比曼哈顿更灵活,但可能低估实际步数。

4. 八方向距离:

  • 定义:允许八方向移动,但斜向移动的代价更高(比如√2倍),更接近真实情况。

近似写法:

  • 适用场景:允许斜向移动但代价更高的地图,比如某些策略游戏或真实地形路径规划。

  • 特点:比切比雪夫更精确,适合八方向移动的A*启发函数。

最短路证明

所以,A*算法得到的路径一定是最短路吗?答案是否定的。

我们已知BFS得到的路径一定是最短路,但是BFS的时间效率过低,我们添加了贪心的策略,权衡速度与最优性。

但是,如果我们处理好启发式函数的大小,可以保证A*路径的最优性,即启发项小于等于最优的那条路径

在之前的方格示例中,A*得到的一定是最优路径,因为每个点的估计价值是通过当前点与终点的相对位置计算得出的,其有绝对性,即不同点的估计价值在同个距离算法下是绝对的。

但是在实际应用中,直接使用相对坐标来计算估计价值是不合理的,比如:

其中蓝色的为河流,我们预估通过河流所需时间需要10个单位,假设通过一个格子所需时间1个单位。

从起点开始,到河流的权重f(n)为16,而走上面路线权重为14,则走上面路线到达终点。但是事实上河流上有桥,可以直接通过,而河流路径明显比上面更短,丧失了最优性

发现当h<=4的时候,其最短路径都是正确的,这就是启发项大小要小于等于最优路径。

在实际情况中,要合理设置初始权值,才能避免某些不必要的麻烦。

当然,比如你想要某个敌人不能走某条路,直接将初始权值设为无限大,就可以实现该功能。

算法示例

写了一个html文件,可以直观展现A*算法的过程与结果,可以复制到本地尝试一下:

代码:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>A* 算法可视化演示(优化版)</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            background-color: #f5f5f5;
            margin: 0;
            padding: 20px;
            color: #333;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        h1 {
            color: #2c3e50;
            margin-bottom: 10px;
        }
        .description {
            max-width: 700px;
            text-align: center;
            margin-bottom: 20px;
            color: #555;
        }
        .controls {
            margin-bottom: 20px;
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            justify-content: center;
            align-items: center;
        }
        button {
            padding: 10px 15px;
            border: none;
            border-radius: 5px;
            background-color: #3498db;
            color: white;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s;
        }
        button:hover {
            background-color: #2980b9;
        }
        button:disabled {
            background-color: #bdc3c7;
            cursor: not-allowed;
        }
        .grid-container {
            display: flex;
            flex-direction: column;
            align-items: center;
            margin-bottom: 20px;
        }
        .grid {
            display: grid;
            grid-template-columns: repeat(20, 25px);
            grid-template-rows: repeat(20, 25px);
            gap: 1px;
            border: 1px solid #ccc;
            background-color: #ddd;
            position: relative;
        }
        .cell {
            width: 25px;
            height: 25px;
            background-color: #fff;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            font-size: 10px;
            transition: background-color 0.2s;
            position: relative;
        }
        .cell:hover {
            opacity: 0.8;
        }
        .wall { background-color: #333; }
        .start { background-color: #2ecc71; }
        .end { background-color: #e74c3c; }
        .visited { background-color: #3498db; }
        .path { background-color: #f1c40f; }
        .current { background-color: #9b59b6; }
        .frontier { background-color: #95a5a6; opacity: 0.6; }
        
        .legend {
            display: flex;
            gap: 15px;
            margin-top: 10px;
            font-size: 14px;
            flex-wrap: wrap;
        }
        .legend-item {
            display: flex;
            align-items: center;
            gap: 5px;
        }
        .legend-color {
            width: 15px;
            height: 15px;
            border: 1px solid #ccc;
        }
        
        .info {
            margin-top: 20px;
            max-width: 600px;
            background-color: white;
            padding: 15px;
            border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }
        .info h3 {
            margin-top: 0;
            color: #2c3e50;
        }
        .info p {
            margin: 5px 0;
            font-size: 14px;
        }
        .mode-selector {
            margin-bottom: 10px;
            display: flex;
            gap: 10px;
            align-items: center;
        }
        select, input {
            padding: 5px;
            font-size: 16px;
        }
        .speed-control {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .stats {
            display: flex;
            gap: 20px;
            flex-wrap: wrap;
        }
        .stat-item {
            font-size: 14px;
        }
        .stat-value {
            font-weight: bold;
            color: #3498db;
        }
        .heuristic-selector {
            display: flex;
            align-items: center;
            gap: 10px;
        }
    </style>
</head>
<body>
    <h1>A* 寻路算法可视化(优化版)</h1>
    <div class="description">
        点击网格设置起点(绿色)、终点(红色)和障碍物(黑色)。支持拖拽绘制障碍物,可切换不同启发式函数。
    </div>

    <div class="mode-selector">
        <label for="mode">编辑模式: </label>
        <select id="mode">
            <option value="start">设置起点</option>
            <option value="end">设置终点</option>
            <option value="wall">绘制障碍墙</option>
            <option value="erase">擦除障碍</option>
        </select>
        
        <div class="heuristic-selector">
            <label for="heuristic">启发式函数: </label>
            <select id="heuristic">
                <option value="manhattan">曼哈顿距离</option>
                <option value="euclidean">欧几里得距离</option>
                <option value="chebyshev">切比雪夫距离</option>
                <option value="octile">八方向距离</option>
            </select>
        </div>
    </div>

    <div class="controls">
        <button id="startBtn">开始寻路</button>
        <button id="stepBtn" disabled>单步执行</button>
        <button id="autoBtn" disabled>自动播放</button>
        <button id="resetBtn">重置网格</button>
        <button id="clearPathBtn">清除路径</button>
        <button id="mazeBtn">生成迷宫</button>
        
        <div class="speed-control">
            <label for="speed">速度: </label>
            <input type="range" id="speed" min="10" max="500" value="100" step="10">
            <span id="speedValue">100ms</span>
        </div>
    </div>

    <div class="grid-container">
        <div class="grid" id="grid"></div>
        <div class="legend">
            <div class="legend-item">
                <div class="legend-color start"></div>
                <span>起点</span>
            </div>
            <div class="legend-item">
                <div class="legend-color end"></div>
                <span>终点</span>
            </div>
            <div class="legend-item">
                <div class="legend-color wall"></div>
                <span>障碍</span>
            </div>
            <div class="legend-item">
                <div class="legend-color frontier"></div>
                <span>待探索</span>
            </div>
            <div class="legend-item">
                <div class="legend-color visited"></div>
                <span>已访问</span>
            </div>
            <div class="legend-item">
                <div class="legend-color current"></div>
                <span>当前节点</span>
            </div>
            <div class="legend-item">
                <div class="legend-color path"></div>
                <span>最终路径</span>
            </div>
        </div>
    </div>

    <div class="info">
        <h3>算法信息</h3>
        <p id="currentInfo">设置起点和终点,然后点击"开始寻路"。</p>
        <div class="stats">
            <div class="stat-item">路径长度: <span class="stat-value" id="pathLength">-</span></div>
            <div class="stat-item">访问节点数: <span class="stat-value" id="visitedCount">-</span></div>
            <div class="stat-item">开放列表大小: <span class="stat-value" id="openListSize">-</span></div>
            <div class="stat-item">执行时间: <span class="stat-value" id="executionTime">-</span></div>
        </div>
    </div>

    <script>
        // 网格配置
        const ROWS = 20;
        const COLS = 20;
        let grid = [];
        let startCell = null;
        let endCell = null;
        let isRunning = false;
        let isAutoRunning = false;
        let openList = [];
        let closedSet = new Set(); // 使用Set提高查找效率
        let path = [];
        let currentAlgorithmState = null;
        let autoInterval = null;
        let isMouseDown = false;
        let startTime = 0;
        let animationSpeed = 100;
        
        // 优先队列实现(最小堆)
        class PriorityQueue {
            constructor() {
                this.heap = [];
            }
            
            push(element) {
                this.heap.push(element);
                this.bubbleUp(this.heap.length - 1);
            }
            
            pop() {
                if (this.heap.length === 0) return null;
                const min = this.heap[0];
                const end = this.heap.pop();
                if (this.heap.length > 0) {
                    this.heap[0] = end;
                    this.bubbleDown(0);
                }
                return min;
            }
            
            bubbleUp(index) {
                while (index > 0) {
                    const parentIndex = Math.floor((index - 1) / 2);
                    if (this.heap[index].f < this.heap[parentIndex].f) {
                        [this.heap[index], this.heap[parentIndex]] = [this.heap[parentIndex], this.heap[index]];
                        index = parentIndex;
                    } else {
                        break;
                    }
                }
            }
            
            bubbleDown(index) {
                while (true) {
                    let minIndex = index;
                    const leftChild = 2 * index + 1;
                    const rightChild = 2 * index + 2;
                    
                    if (leftChild < this.heap.length && this.heap[leftChild].f < this.heap[minIndex].f) {
                        minIndex = leftChild;
                    }
                    if (rightChild < this.heap.length && this.heap[rightChild].f < this.heap[minIndex].f) {
                        minIndex = rightChild;
                    }
                    
                    if (minIndex !== index) {
                        [this.heap[index], this.heap[minIndex]] = [this.heap[minIndex], this.heap[index]];
                        index = minIndex;
                    } else {
                        break;
                    }
                }
            }
            
            get length() {
                return this.heap.length;
            }
            
            contains(element) {
                return this.heap.includes(element);
            }
            
            update(element) {
                const index = this.heap.indexOf(element);
                if (index !== -1) {
                    this.bubbleUp(index);
                    this.bubbleDown(index);
                }
            }
        }
        
        // 初始化网格
        function initGrid() {
            const gridElement = document.getElementById('grid');
            gridElement.innerHTML = '';
            grid = [];
            
            for (let row = 0; row < ROWS; row++) {
                const gridRow = [];
                for (let col = 0; col < COLS; col++) {
                    const cell = document.createElement('div');
                    cell.className = 'cell';
                    cell.dataset.row = row;
                    cell.dataset.col = col;
                    
                    cell.addEventListener('mousedown', () => {
                        isMouseDown = true;
                        handleCellClick(row, col);
                    });
                    cell.addEventListener('mouseenter', () => {
                        if (isMouseDown) {
                            handleCellDrag(row, col);
                        }
                    });
                    cell.addEventListener('mouseup', () => {
                        isMouseDown = false;
                    });
                    
                    gridElement.appendChild(cell);
                    gridRow.push({
                        element: cell,
                        row,
                        col,
                        isWall: false,
                        f: Infinity,
                        g: Infinity,
                        h: 0,
                        parent: null,
                        inOpenList: false
                    });
                }
                grid.push(gridRow);
            }
            
            // 全局鼠标释放事件
            document.addEventListener('mouseup', () => {
                isMouseDown = false;
            });
            
            resetAlgorithmState();
        }
        
        function resetAlgorithmState() {
            openList = new PriorityQueue();
            closedSet = new Set();
            path = [];
            isRunning = false;
            isAutoRunning = false;
            currentAlgorithmState = null;
            startTime = 0;
            
            document.getElementById('startBtn').disabled = false;
            document.getElementById('stepBtn').disabled = true;
            document.getElementById('autoBtn').disabled = true;
            
            clearInterval(autoInterval);
            
            updateInfoText('设置起点和终点,然后点击"开始寻路"。');
            updateStats();
            
            // 清除之前的访问和路径标记,但保留墙、起点和终点
            for (let row = 0; row < ROWS; row++) {
                for (let col = 0; col < COLS; col++) {
                    const cell = grid[row][col];
                    cell.element.classList.remove('visited', 'path', 'current', 'frontier');
                    cell.f = Infinity;
                    cell.g = Infinity;
                    cell.h = 0;
                    cell.parent = null;
                    cell.inOpenList = false;
                }
            }
        }
        
        function handleCellClick(row, col) {
            if (isRunning) return;
            
            const cell = grid[row][col];
            const mode = document.getElementById('mode').value;
            
            switch (mode) {
                case 'start':
                    if (startCell) {
                        startCell.element.classList.remove('start');
                    }
                    cell.element.classList.remove('wall', 'end');
                    cell.element.classList.add('start');
                    cell.isWall = false;
                    startCell = cell;
                    break;
                case 'end':
                    if (endCell) {
                        endCell.element.classList.remove('end');
                    }
                    cell.element.classList.remove('wall', 'start');
                    cell.element.classList.add('end');
                    cell.isWall = false;
                    endCell = cell;
                    break;
                case 'wall':
                    if (cell !== startCell && cell !== endCell) {
                        cell.isWall = true;
                        cell.element.classList.add('wall');
                    }
                    break;
                case 'erase':
                    if (cell !== startCell && cell !== endCell) {
                        cell.isWall = false;
                        cell.element.classList.remove('wall');
                    }
                    break;
            }
        }
        
        function handleCellDrag(row, col) {
            const cell = grid[row][col];
            const mode = document.getElementById('mode').value;
            
            if (mode === 'wall' && cell !== startCell && cell !== endCell) {
                cell.isWall = true;
                cell.element.classList.add('wall');
            } else if (mode === 'erase' && cell !== startCell && cell !== endCell) {
                cell.isWall = false;
                cell.element.classList.remove('wall');
            }
        }
        
        // 启发式函数
        function heuristic(a, b) {
            const type = document.getElementById('heuristic').value;
            const dx = Math.abs(a.row - b.row);
            const dy = Math.abs(a.col - b.col);
            
            switch (type) {
                case 'manhattan':
                    return dx + dy;
                case 'euclidean':
                    return Math.sqrt(dx * dx + dy * dy);
                case 'chebyshev':
                    return Math.max(dx, dy);
                case 'octile':
                    return Math.max(dx, dy) + (Math.sqrt(2) - 1) * Math.min(dx, dy);
                default:
                    return dx + dy;
            }
        }
        
        function getNeighbors(cell) {
            const neighbors = [];
            const { row, col } = cell;
            
            // 八个方向(包括对角线)
            const directions = [
                [-1, 0, 1], [0, 1, 1], [1, 0, 1], [0, -1, 1],  // 上右下左
                [-1, -1, 1.414], [-1, 1, 1.414], [1, 1, 1.414], [1, -1, 1.414]  // 对角线
            ];
            
            const allowDiagonal = document.getElementById('heuristic').value !== 'manhattan';
            const limit = allowDiagonal ? 8 : 4;
            
            for (let i = 0; i < limit; i++) {
                const [dr, dc, cost] = directions[i];
                const newRow = row + dr;
                const newCol = col + dc;
                
                if (newRow >= 0 && newRow < ROWS && newCol >= 0 && newCol < COLS) {
                    const neighbor = grid[newRow][newCol];
                    if (!neighbor.isWall) {
                        // 对角线移动时检查两边是否有墙
                        if (i >= 4) {
                            const side1 = grid[row + directions[i-4][0]][col + directions[i-4][1]];
                            const side2 = grid[row + directions[(i-3)%4][0]][col + directions[(i-3)%4][1]];
                            if (side1.isWall || side2.isWall) continue;
                        }
                        neighbors.push({cell: neighbor, cost});
                    }
                }
            }
            
            return neighbors;
        }
        
        function startAlgorithm() {
            if (!startCell || !endCell) {
                alert('请先设置起点和终点!');
                return;
            }
            
            resetAlgorithmState();
            isRunning = true;
            startTime = performance.now();
            
            // 初始化开放列表,加入起点
            openList = new PriorityQueue();
            startCell.g = 0;
            startCell.h = heuristic(startCell, endCell);
            startCell.f = startCell.h;
            startCell.inOpenList = true;
            openList.push(startCell);
            
            document.getElementById('startBtn').disabled = true;
            document.getElementById('stepBtn').disabled = false;
            document.getElementById('autoBtn').disabled = false;
            
            currentAlgorithmState = {
                found: false,
                noPath: false
            };
            
            updateStats();
            updateInfoText('A* 算法已启动,准备开始搜索...');
        }
        
        function stepAlgorithm() {
            if (!isRunning || currentAlgorithmState.found || currentAlgorithmState.noPath) return;
            
            if (openList.length === 0) {
                currentAlgorithmState.noPath = true;
                updateInfoText('搜索完成:没有找到路径。');
                finishAlgorithm();
                return;
            }
            
            // 从优先队列中取出f值最小的节点
            const current = openList.pop();
            current.inOpenList = false;
            
            // 标记当前节点
            if (current !== startCell && current !== endCell) {
                current.element.classList.add('current');
                current.element.classList.remove('frontier');
                if (current.parent && current.parent !== startCell) {
                    current.parent.element.classList.remove('current');
                }
            }
            
            // 如果找到终点
            if (current === endCell) {
                currentAlgorithmState.found = true;
                reconstructPath(current);
                const executionTime = (performance.now() - startTime).toFixed(2);
                updateInfoText(`搜索完成:已找到最短路径!用时 ${executionTime}ms`);
                finishAlgorithm();
                return;
            }
            
            // 将当前节点移到关闭列表
            closedSet.add(current);
            
            if (current !== startCell && current !== endCell) {
                current.element.classList.remove('current');
                current.element.classList.add('visited');
            }
            
            updateInfoText(`正在检查节点 (${current.row}, ${current.col}) - f=${current.f.toFixed(2)}`);
            
            // 检查所有邻居
            const neighbors = getNeighbors(current);
            for (const {cell: neighbor, cost} of neighbors) {
                if (closedSet.has(neighbor)) continue;
                
                const tentativeG = current.g + cost;
                
                if (tentativeG < neighbor.g) {
                    neighbor.parent = current;
                    neighbor.g = tentativeG;
                    neighbor.h = heuristic(neighbor, endCell);
                    neighbor.f = neighbor.g + neighbor.h;
                    
                    if (!neighbor.inOpenList) {
                        openList.push(neighbor);
                        neighbor.inOpenList = true;
                        if (neighbor !== endCell) {
                            neighbor.element.classList.add('frontier');
                        }
                    } else {
                        openList.update(neighbor);
                    }
                }
            }
            
            updateStats();
        }
        
        function reconstructPath(current) {
            path = [];
            let temp = current;
            while (temp) {
                path.push(temp);
                temp = temp.parent;
            }
            path.reverse();
            
            // 动画显示路径
            path.forEach((cell, index) => {
                if (cell !== startCell && cell !== endCell) {
                    setTimeout(() => {
                        cell.element.classList.add('path');
                        cell.element.classList.remove('visited');
                    }, index * 20);
                }
            });
        }
        
        function finishAlgorithm() {
            isRunning = false;
            isAutoRunning = false;
            document.getElementById('stepBtn').disabled = true;
            document.getElementById('autoBtn').disabled = true;
            clearInterval(autoInterval);
            
            // 移除所有当前标记
            document.querySelectorAll('.current').forEach(el => el.classList.remove('current'));
            document.querySelectorAll('.frontier').forEach(el => el.classList.remove('frontier'));
            
            const executionTime = (performance.now() - startTime).toFixed(2);
            document.getElementById('executionTime').textContent = `${executionTime}ms`;
            document.getElementById('pathLength').textContent = path.length > 0 ? path.length - 1 : '-';
        }
        
        function startAutoRun() {
            if (isAutoRunning) {
                stopAutoRun();
                return;
            }
            
            isAutoRunning = true;
            document.getElementById('autoBtn').textContent = '停止自动播放';
            
            autoInterval = setInterval(() => {
                if (currentAlgorithmState.found || currentAlgorithmState.noPath) {
                    stopAutoRun();
                    return;
                }
                stepAlgorithm();
            }, animationSpeed);
        }
        
        function stopAutoRun() {
            isAutoRunning = false;
            clearInterval(autoInterval);
            document.getElementById('autoBtn').textContent = '自动播放';
        }
        
        function updateInfoText(text) {
            document.getElementById('currentInfo').textContent = text;
        }
        
        function updateStats() {
            document.getElementById('visitedCount').textContent = closedSet.size;
            document.getElementById('openListSize').textContent = openList.length;
            document.getElementById('pathLength').textContent = path.length > 0 ? path.length - 1 : '-';
        }
        
        // 生成随机迷宫
        function generateMaze() {
            if (isRunning) return;
            
            // 清除所有墙
            for (let row = 0; row < ROWS; row++) {
                for (let col = 0; col < COLS; col++) {
                    const cell = grid[row][col];
                    if (cell !== startCell && cell !== endCell) {
                        cell.isWall = false;
                        cell.element.classList.remove('wall');
                    }
                }
            }
            
            // 随机生成障碍物(30%概率)
            for (let row = 0; row < ROWS; row++) {
                for (let col = 0; col < COLS; col++) {
                    const cell = grid[row][col];
                    if (cell !== startCell && cell !== endCell && Math.random() < 0.3) {
                        cell.isWall = true;
                        cell.element.classList.add('wall');
                    }
                }
            }
            
            resetAlgorithmState();
        }
        
        // 速度控制
        document.getElementById('speed').addEventListener('input', (e) => {
            animationSpeed = parseInt(e.target.value);
            document.getElementById('speedValue').textContent = `${animationSpeed}ms`;
            if (isAutoRunning) {
                stopAutoRun();
                startAutoRun();
            }
        });
        
        // 事件监听器
        document.getElementById('startBtn').addEventListener('click', startAlgorithm);
        document.getElementById('stepBtn').addEventListener('click', stepAlgorithm);
        document.getElementById('autoBtn').addEventListener('click', startAutoRun);
        document.getElementById('resetBtn').addEventListener('click', initGrid);
        document.getElementById('clearPathBtn').addEventListener('click', resetAlgorithmState);
        document.getElementById('mazeBtn').addEventListener('click', generateMaze);
        
        // 初始化
        initGrid();
        
        // 设置默认起点和终点
        handleCellClick(5, 2);
        document.getElementById('mode').value = 'end';
        handleCellClick(15, 17);
        document.getElementById('mode').value = 'wall';
    </script>
</body>
</html>
相关推荐
assibe7 小时前
cmake基本语法结构
数据库·c++·cmake
李铁柱的7 小时前
数值分析——误差的来源与分类、误差的基本概念(绝对误差、相对误差、有效数字)
人工智能·算法·分类·数据挖掘
君鼎8 小时前
More Effective C++ 条款26:限制某个类所能产生的对象数量
c++
ajassi20008 小时前
开源 C++ QT Widget 开发(十一)进程间通信--Windows 窗口通信
linux·c++·windows·qt·开源
2401_858286118 小时前
CD75.【C++ Dev】异常
开发语言·c++·异常
蒹葭玉树8 小时前
【C++上岸】C++常见面试题目--算法篇(第十八期)
c++·算法·面试
EkihzniY8 小时前
OCR 发票识别与验真接口:助力电子化发票新时代
算法
一支鱼8 小时前
leetcode-5-最长回文子串
算法·leetcode·typescript
茉莉玫瑰花茶9 小时前
算法 --- 分治(快排)
算法