普通的广度优先最短路径搜索算法只能解决无权重的图:
求出的最短路径没问题,但是如果上边的空缺格子的距离为100呢?这种方式搜出的最短路径可能就是错的:
我们可以找一个距离1作为基本距离,然后距离为100的格子在搜索时构筑搜索树结构时可以当成100个虚拟格子来处理,比如该格子本来的code为 xxx,则这100个格子的code分别为code1、code2、code3、...、code99、code100,其中code1~code99的可达格子只能是它的下一个虚拟格子,最尾部的code100的可达格子和正常格子是一样的,不考虑障碍物、超越边界和已经走过的格子的话,分别是其上边、右边、下边、左边的格子( 正常格子或者虚拟格子链表的第一个虚拟格子 ),这样的话这个问题就迎刃而解了,改进后的BFS完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style type="text/css">
#box1 table{
border: 0px;
border-collapse: collapse;
cursor: pointer;
background-color: gray;
}
#box1 th {
border: 0px;
border-collapse: collapse;
cursor: pointer;
}
#box1 td{
border: 1px solid #000;
border-collapse: collapse;
cursor: pointer;
text-align: center;
font-size: 4;
}
#box1{
border: 0px;
}
.wall{
background-color: black;
}
.startPoint{
background-color: blue;
}
.targetPoint{
background-color: blue;
}
.line{
background-color: yellow;
}
</style>
</head>
<body>
单击灰色格子将其设置为障碍( 黑色 ),单击障碍清空障碍,双击格子修改格子的距离
<div id="box1"></div>
<button onclick="startSearch()">开始寻路</button>
<button onclick="initRingWalls()">生成环状障碍</button>
<button onclick="initPointWalls()">生成点状障碍</button>
<button onclick="clearLine()">清空路线</button>
</body>
<script type="text/javascript">
var map_code_td = new Map();
var row_count = 20;
var col_count = 20;
var rowNum_start = 1;
var colNum_start = 1;
var rowNum_target = 10;
var colNum_target = 10;
var map_code_wallYn = new Map();
var map_code_distance = new Map();
var code_start = rowNum_start + "_" + colNum_start;
var code_target = rowNum_target + "_" + colNum_target;
// 辅助帮助显示横竖纹的底色棋盘,没有任何事件和业务逻辑
function initBox1( ) {
var table = document.createElement("table");
table.rules = 'all' ;
for (var rowNum = 1; rowNum <= row_count; rowNum++) {
var tr = document.createElement("tr");
for (var colNum = 1; colNum <= col_count; colNum++) {
var td = document.createElement("td");
td.width = 20;
td.height = 20;
td.className ="road";
tr.appendChild(td);
var code = rowNum + "_" + colNum;
map_code_td.set(code, td);
if( code != code_start && code != code_target ){
map_code_distance.set(code, 1);
td.innerText = "1";
}
}
table.appendChild(tr);
}
document.getElementById( "box1" ).appendChild(table);
table.addEventListener( "click", setOrClearWall );
table.addEventListener( "dblclick", setDistance );
var code_start = rowNum_start + "_" + colNum_start;
var code_target = rowNum_target + "_" + colNum_target;
map_code_td.get( code_start ).className = "startPoint";
map_code_td.get( code_target ).className = "targetPoint";
}
function setDistance(){
var td = event.srcElement;
var rowNum = td.parentElement.rowIndex + 1;
var colNum = td.cellIndex + 1;
var code = rowNum + "_" + colNum;
var distance = prompt("请为格子 " + code + " 设置距离", "1");
if (distance != null && distance != "") {
distance = parseInt(distance);
if (distance > 0) {
map_code_distance.set(code, distance);
td.innerText = distance;
}
} else {
alert("您没有输入距离!");
}
}
function initPointWalls(){
// 清空障碍
clearAllWalls();
for (var rowNum = 1; rowNum <= row_count; rowNum++) {
for (var colNum = 1; colNum <= col_count; colNum++) {
var code = rowNum + "_" + colNum;
if( code == code_start || code == code_target ){
continue;
}
// 右 1/3 的几率成为障碍
var randomNum = randomBetween(1,10000);
if( randomNum < 3333 ){
var td = map_code_td.get( code );
td.className = "wall";
map_code_wallYn.set( code, 'y' );
}
}
}
}
// 鼠标的点击事件监听方法,用来将点击的格子设置为墙
function setOrClearWall(){
var td = event.srcElement;
var rowNum = td.parentElement.rowIndex + 1;
var colNum = td.cellIndex + 1;
var code = rowNum + "_" + colNum;
if( td.className == "wall" ){
td.className = "road";
map_code_wallYn.set( code, "n" );
}else if( td.className =='road' ){
td.className = "wall";
map_code_wallYn.set( code, "y" );
}
}
function randomBetween(minNum,maxNum){
return Math.floor(Math.random() * (maxNum - minNum + 1)) + minNum;
}
function clearAllWalls(){
for (var rowNum = 1; rowNum <= row_count; rowNum++) {
for (var colNum = 1; colNum <= col_count; colNum++) {
var code = rowNum + "_" + colNum;
if( code == code_start || code == code_target){
continue;
}
var wallYn = map_code_wallYn.get( code );
if( wallYn == 'y' ){
map_code_td.get( code ).className = "";
map_code_wallYn.set( code, "n" );
}else{
var td = map_code_td.get( code );
if( td.className == 'passed' ){
td.className = "";
}
}
}
}
}
function initRingWalls(){
// 清空障碍
clearAllWalls();
// 将第三行设置为障碍
var circles = [2,4,6,8,10,12,14,16,18];
for( var i in circles ){
var circle = circles[i];
var randomNum = randomBetween(1,4);
var randomNum_1 = randomBetween(circle,col_count - circle);
var randomNum_2 = randomBetween(circle,col_count - circle);
var randomNum_3 = randomBetween(circle,row_count - circle);
var randomNum_4 = randomBetween(circle,row_count - circle);
// 顶行
for (var colNum = circle; colNum <= ( col_count - circle );colNum++){
if( randomNum == 1 ){
if( colNum == randomNum_1 ){
continue;
}
}
var code = circle + "_" + colNum;
map_code_td.get( code ).className = "wall";
map_code_wallYn.set( code, "y" );
}
// 底行
for (var colNum = circle; colNum <= ( col_count - circle );colNum++){
if( randomNum == 2 ){
if( colNum == randomNum_2 ){
continue;
}
}
var code = ( row_count - circle ) + "_" + colNum;
map_code_td.get( code ).className = "wall";
map_code_wallYn.set( code, "y" );
}
// 左列
for (var rowNum = circle; rowNum <= ( row_count - circle );rowNum++){
if( randomNum == 3 ){
if( rowNum == randomNum_3 ){
continue;
}
}
var code = rowNum + "_" + circle;
map_code_td.get( code ).className = "wall";
map_code_wallYn.set( code, "y" );
}
// 右列
for (var rowNum = circle; rowNum <= ( row_count - circle );rowNum++){
if( randomNum == 4 ){
if( rowNum == randomNum_4 ){
continue;
}
}
var code = rowNum + "_" + ( col_count - circle );
map_code_td.get( code ).className = "wall";
map_code_wallYn.set( code, "y" );
}
}
}
// 检测 code 表示的格子是否可以通行
function canPass( code ){
var arr = code.split("_");
var rowNum = parseInt( arr[0])
var colNum = parseInt( arr[1] );
if( rowNum > row_count || rowNum < 1 || colNum > col_count || colNum < 1 ){
// 该位置已经超过边界了,不能走
return false;
}
var wallYn = map_code_wallYn.get( code );
if( wallYn == 'y' ){
// 该位置是墙,不能走
return false;
}
return true;
}
class Node {
constructor(code,virtualCode,isVirtualCode,isVirtualHead,isVirtualTail,distance,orderNum,parent) {
// 形如:'1_1'
this.code = code;
// 形如: '1_1_1'
this.virtualCode = virtualCode;
// 是否是虚拟节点
this.isVirtualCode = isVirtualCode;
// 是否是该虚拟链上的头节点
this.isVirtualHead = isVirtualHead;
// 是否是该虚拟链上的尾结点
this.isVirtualTail = isVirtualTail;
// 该训链的距离
this.distance = distance;
// 该虚拟节点是该虚拟链中的第几个虚拟节点,1~distance
this.orderNum = orderNum;
this.children = [];
this.parent = parent;
}
addChild(node) {
this.children.push(node);
}
}
initBox1( );
function isNormalCode( code ){
if( code.split.length > 2 ){
// 是虚拟格子,形如:"1_1_2"
return false;
}
// 形如:"1_1"
return true;
}
function startSearch( ){
var node_start = new Node( code_start,null,false,false,false,1,null,null )
var nodes_currLevel = [node_start];
var nodes_nextLevel = [];
var codes_passed = [];
var node_target = null;
while( true ){
// 如果找到了 目标节点或者 nodes_currLevel 为空了,需要退出循环
// 遍历 nodes_currLevel 中的 每个 node,检测其是否是目标节点,如果是目标节点,则找到了最短路径,
if( nodes_currLevel.length == 0 ){
break;
}
for( var i in nodes_currLevel ){
var node = nodes_currLevel[i];
var code = node.code;
var isVirtualCode = node.isVirtualCode;
if( isVirtualCode ){
// 是虚拟节点
handleVirtualNode( node,nodes_nextLevel,codes_passed );
}else{
// 是正常节点
node_target = handleNormalNode( node,nodes_nextLevel,codes_passed );
}
if( node_target != null ){
break;
}
}
if( node_target != null ){
break;
}
// 该层遍历完了,更新 nodes_currLevel 和重置 nodes_nextLevel
nodes_currLevel = nodes_nextLevel;
nodes_nextLevel = [];
}
if( node_target != null ){
// console.log("找到最短路径了,node_target = ",node_target);
// 画出最短路径
printPath( node_target );
}else{
alert( "未找到最短路径" );
}
}
// node 是虚拟节点
function handleVirtualNode( node,nodes_nextLevel,codes_passed ){
var virtualCode = node.virtualCode;
var code = node.code;
var distance = node.distance;
var isVirtualTail = node.isVirtualTail;
var orderNum = node.orderNum;
if( codes_passed.indexOf( virtualCode ) > -1 ){
// 该节点已经遍历过了,不需要重复处理
return;
}
// 找到该节点的所有可达子节点,保存到下一级的集合中,并将当前节点置为已遍历状态
if( isVirtualTail ){
// todo 该虚拟节点是所处虚拟链的尾巴
// 找到该虚拟节点所在的正常节点的上、右、下、左节点( 需要排除超过边界的和是障碍物的,可能下个节点也是距离大于1 的,这样的话,需要让其的虚拟头部加入nodes_nextLevel中 )
var distance = map_code_distance.get( code );
var arr = code.split("_");
var rowNum = parseInt( arr[0] );
var colNum = parseInt( arr[1] );
var code_up = ( rowNum - 1 ) + "_" + colNum;
var code_right = rowNum + "_" + ( colNum + 1 );
var code_down = ( rowNum + 1 ) + "_" + colNum;
var code_left = rowNum + "_" + ( colNum - 1 );
var node_up = buildNodeOrVirtualNodeByCode( code_up );
var node_right = buildNodeOrVirtualNodeByCode( code_right );
var node_down = buildNodeOrVirtualNodeByCode( code_down );
var node_left = buildNodeOrVirtualNodeByCode( code_left );
// 更新搜索树结构
flushNodeTree( node,node_up,nodes_nextLevel );
flushNodeTree( node,node_right,nodes_nextLevel );
flushNodeTree( node,node_down,nodes_nextLevel );
flushNodeTree( node,node_left,nodes_nextLevel );
}else{
// todo 该虚拟节点是所处虚拟链的头部或身体,下一步的可达节点为该虚拟链的下一个虚拟节点,
var arr = code.split("_");
var rowNum = parseInt( arr[0] );
var colNum = parseInt( arr[1] );
var code_up = ( rowNum - 1 ) + "_" + colNum;
var code_right = rowNum + "_" + ( colNum + 1 );
var code_down = ( rowNum + 1 ) + "_" + colNum;
var code_left = rowNum + "_" + ( colNum - 1 );
var orderNum_next = orderNum + 1;
var virtualCode_next = code + "_" + orderNum_next;
var isVirtualTail_next = false;
if( orderNum_next == distance ){
isVirtualTail_next = true;
}
var node_next = new Node( code,virtualCode_next,true,false,isVirtualTail,distance,orderNum_next,node );
// 更新搜索树结构
flushNodeTree( node,node_next,nodes_nextLevel );
}
codes_passed.push( virtualCode );
}
function flushNodeTree( node,node_next,nodes_nextLevel ){
if( node_next == null ){
return;
}
// 更新树结构
node_next.parent = node;
node.addChild( node_next );
nodes_nextLevel.push( node_next );
}
// code 的格式为:'${rowNum}_${colNum}'
function buildNodeOrVirtualNodeByCode( code ){
if( !canPass( code ) ){
return null;
}
var distance = map_code_distance.get( code );
if( distance == 1 ){
// 返回正常节点
return new Node( code,null,false,false,false,1,null,null );
}else{
// 返回虚拟节点( 所处虚拟链的头部 )
return new Node( code,code + "_1",true,true,false,distance,1,null );
}
}
// node 是正常节点
function handleNormalNode( node,nodes_nextLevel,codes_passed ){
var code = node.code;
if( codes_passed.indexOf( code ) > -1 ){
// 该节点已经遍历过了,不需要重复处理
return;
}
if( code == code_target ){
// 找到目标节点了
return node;
}
// 找到该节点的所有可达子节点,保存到下一级的集合中,并将当前节点置为已遍历状态
// 找到该节点的上、右、下、左节点( 需要排除超过边界的和是障碍物的,可能这些几点是距离大于1的,需要用其对应的虚拟链的头部替代 )
var arr = code.split("_");
var rowNum = parseInt( arr[0] );
var colNum = parseInt( arr[1] );
var code_up = ( rowNum - 1 ) + "_" + colNum;
var code_right = rowNum + "_" + ( colNum + 1 );
var code_down = ( rowNum + 1 ) + "_" + colNum;
var code_left = rowNum + "_" + ( colNum - 1 );
var node_up = buildNodeOrVirtualNodeByCode( code_up );
var node_right = buildNodeOrVirtualNodeByCode( code_right );
var node_down = buildNodeOrVirtualNodeByCode( code_down );
var node_left = buildNodeOrVirtualNodeByCode( code_left );
// 更新搜索树结构
flushNodeTree( node, node_up, nodes_nextLevel );
flushNodeTree( node, node_right, nodes_nextLevel );
flushNodeTree( node, node_down, nodes_nextLevel );
flushNodeTree( node, node_left, nodes_nextLevel );
codes_passed.push( code );
}
function printPath( node_target ){
if( node_target == null ){
return;
}
var node_prev = node_target.parent;
while( true ){
if( node_prev == null ){
break;
}
var code = node_prev.code;
if( code == code_start ){
break
}
var td = map_code_td.get( code );
td.className = "line";
node_prev = node_prev.parent;
}
}
function clearLine(){
for( var rowNum=1;rowNum<=row_count;rowNum++ ){
for( var colNum=1;colNum<=col_count;colNum++ ){
var code = rowNum + "_" + colNum;
var td = map_code_td.get( code );
if( td.className == "line" ){
td.className = "";
}
}
}
}
</script>
</html>