算法背景
? ? ? ??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*算法的過程與結果,可以復制到本地嘗試一下:
? 代碼:
<!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>