概述
用一个大小为20的数组表示地图。
地图中有两个空格,空格可以上下左右四个方向。每个地图有八种移动情况。
初始化一个队列,放进去初始的地图。
从队列取出第一个地图,然后尝试这八种情况,把可行的放到队列里。依此类推,直到成功。
代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>华容道练习20201228</title>
</head>
<style>
td{
padding: 0.4em;
}
table{
float: left;
margin: 4px;
}
</style>
<body>
<div id="msgDiv">华容道</div>
<br/>
<button id="btn1" onclick="getBestPath()">求解</button>
<span id="info" style="display: none;">用时:<span id="useTime"></span>ms, 步骤:<span id="stepNum"></span>步 <span style="opacity: 0.6;float: right;"> (尝试了:<span id="count"></span>次)</span></span>
<br/>
<div style="clear: both;"></div>
<script type="text/javascript">
/*
地图宽4,高5; 用连续的20个数表示地图;
0000 代表当前位置的点与相邻的点没有关系,
1000 代表与上边的点连接
0100 代表与右边的点连接
.... 依次类推,上右下左
表示点的数字
[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].map(v=>v.toString(2))
[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].map(v=>""+((v>>3)&1)+((v>>2)&1)+((v>>1)&1)+((v>>0)&1))
{ 0: "0000", 1: "0001", 2: "0010", 3: "0011",
4: "0100", 5: "0101", 6: "0110", 7: "0111",
8: "1000", 9: "1001", 10: "1010",11: "1011",
12: "1100",13: "1101",14: "1110",15: "1111" }
偏移量:
[0,1,2,3].map(v=>{ return {x:v&1,y:(v&2)>>1} }) //
表示点的数字 与 它对应的 相邻的点的偏移量们
JSON.stringify([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].map(v=>{
var arr=[];
if((v>>3)&1) arr.push([0,-1]);
if((v>>2)&1) arr.push([1,0]);
if((v>>1)&1) arr.push([0,1]);
if((v>>0)&1) arr.push([-1,0]);
return arr;
})) // const NEAR_SKEWS = ...
JSON.stringify([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15].map(v=>{
var arr=[];
if((v>>3)&1) arr.push([0,-1]);
if((v>>2)&1) arr.push([1,0]);
if((v>>1)&1) arr.push([0,1]);
if((v>>0)&1) arr.push([-1,0]);
if(arr.length>2) return null; // 不处理复杂的
if(arr.length===2) arr.push([arr[0][0]+arr[0][1],arr[1][0]+arr[1][1]]);
return arr;
})) // const NEAR_POINT_SKEWS
*/
console.log("华容道练习");
/**
* 定义一些常量,也可以定义到 Cmp 中
*/
const EMPS_LEN = 2, EMPS_LEN_HALF = 1; // 常量,两个空格, 2的一半是1
const WIDTH = 4, HEIGHT = 5; // 地图大小
const CHAR_NUM_MP = {"0":0,"1":1,"2":2,"3":3,"4":4,"5":5,"6":6,"7":7,"8":8,"9":9,"a":10,"b":11,"c":12,"d":13,"e":14,"f":15}; // 字符 : 数字
const NUM_CHAR_ARR = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];
// 根据点的数字,确定相邻的点基于当前的点的坐标的偏移量
const NEAR_SKEWS = [[],[[-1,0]],[[0,1]],[[0,1],[-1,0]],[[1,0]],[[1,0],[-1,0]],[[1,0],[0,1]],[[1,0],[0,1],[-1,0]],[[0,-1]],[[0,-1],[-1,0]],[[0,-1],[0,1]],[[0,-1],[0,1],[-1,0]],[[0,-1],[1,0]],[[0,-1],[1,0],[-1,0]],[[0,-1],[1,0],[0,1]],[[0,-1],[1,0],[0,1],[-1,0]]];
// 针对当前这个地图做的一个特殊的数组,存放了点的数字 对应着的物体中的所有点
const NEAR_POINT_SKEWS = [[],[[-1,0]],[[0,1]],[[0,1],[-1,0],[1,-1]],[[1,0]],[[1,0],[-1,0],[1,-1]],[[1,0],[0,1],[1,1]],[null],[[0,-1]],[[0,-1],[-1,0],[-1,-1]],[[0,-1],[0,1],[-1,1]],[null],[[0,-1],[1,0],[-1,1]],[null],[null],[null]];
const EMP_CHAR = 'f', EMP_NUM = 15; // 表示空白的字符,常用。 1111 。
const TOP_NUM = 8, RIGHT_NUM = 4, BOTTOM_NUM = 2, LEFT_NUM = 1; // 上右下左 // 1000, 0100, 0010, 0001
// 下面四个是一组 ,(为了遍历空格的四周而服务)
const SKEWS_XY = [[0,-1],[1,0],[0,1],[-1,0]]; // 偏移量,上右下左
const SKEWS_NUM = [TOP_NUM, RIGHT_NUM, BOTTOM_NUM, LEFT_NUM]; // 标记物体方向,上右下左
const MOVE_XY = [[0,1],[-1,0],[0,-1],[1,0]]; // 偏移量,下左上右
// const MOVE_NUM = [BOTTOM_NUM, LEFT_NUM , TOP_NUM, RIGHT_NUM]; // 标记移动方向,下左上右
var numToChar = function(num){return NUM_CHAR_ARR[num];};
// 一个地图状态
function Cmp(mpNums,emps,parentMp,count){ // 地图数组,空格的位置,父Cmp,计数
this.mpStr = mpNums.map(numToChar).join("");
this.mpNums = mpNums;
// [(x1,y1),(x2,y2)] 表示空格的位置 // 注意:不能修改内部的数组里面的值
this.emps = emps;
// 最小的步数
this.count = count;
// count最小的父状态
this.parentMp = parentMp;
// this.childMps = null; // 记录子mps,如果可行的话 [[子mps],] 子mps按照count从小到大排列
}
// 是否符合条件
Cmp.prototype.isOk = function(){
// return this.mpStr[1+4*3]=="0110" && this.mpStr[2+4*3]=="0011" && this.mpStr[1+4*4]=="1100" && this.mpStr[2+4*4]=="1001";
return this.mpStr[13]=="6";//只用判断这一个就行; && this.mpStr[14]=="3" && this.mpStr[17]=="c" && this.mpStr[18]=="9";
}
// 是否对称
Cmp.prototype.is对称 = (function(){
var relMp = {
"6":"3", "3":"6",
"c":"9", "9":"c",
"4":"1", "1":"4"
};
return function (){
return this.mpStr[0]===this.mpStr[3] &&
this.mpStr[4]===this.mpStr[7] &&
this.mpStr[8]===this.mpStr[11] &&
this.mpStr[12]===this.mpStr[15] &&
this.mpStr[16]===this.mpStr[19] &&
(this.mpStr[1]===this.mpStr[2] || this.mpStr[1]===relMp[this.mpStr[2]]) &&
(this.mpStr[5]===this.mpStr[6] || this.mpStr[5]===relMp[this.mpStr[6]]) &&
(this.mpStr[9]===this.mpStr[10] || this.mpStr[9]===relMp[this.mpStr[10]]) &&
(this.mpStr[13]===this.mpStr[14] || this.mpStr[13]===relMp[this.mpStr[14]]) &&
(this.mpStr[17]===this.mpStr[18] || this.mpStr[17]===relMp[this.mpStr[18]]);
}
})();
// 获取地图中的点信息
Cmp.prototype.getCharAt = function(x,y){
if(x===-1||x===WIDTH||y===-1||y===HEIGHT)return undefined;
return this.mpStr[ x + y*WIDTH ];
};
Cmp.prototype.getIntAt = function(x,y){
if(x===-1||x===WIDTH||y===-1||y===HEIGHT)return undefined; // 必须要加判断,不然后出错
return this.mpNums[ x + y*WIDTH ];
};
// 拷贝数组
Cmp.prototype.copyNums = function(){
return this.mpNums.slice();
}
Cmp.prototype.copyEmps = function(){
return this.emps.slice();
}
// 将地图转化为表格
Cmp.prototype.toDomTable = function(){
var table = document.createElement("table"), tr, td;
table.border=1; table.cellSpacing=1;
var x,y,tNum;
var pmp=this.parentMp;
for(y=0;y<HEIGHT;y++){
tr = document.createElement("tr");
for(x=0;x<WIDTH;x++){
td = document.createElement("td");
tNum = this.getIntAt(x,y);
if(pmp!==null){
if(pmp.getIntAt(x,y) != tNum) td.style.backgroundColor="gold";
}
if(tNum === EMP_NUM){
td.style.border = "1px solid white";
} else {
td.style.borderTop = (tNum & TOP_NUM) ? "1px solid white" : "1px solid black";
td.style.borderRight = (tNum & RIGHT_NUM) ? "1px solid white" : "1px solid black";
td.style.borderBottom = (tNum & BOTTOM_NUM) ? "1px solid white" : "1px solid black";
td.style.borderLeft = (tNum & LEFT_NUM) ? "1px solid white" : "1px solid black";
}
tr.appendChild(td);
}
table.appendChild(tr);
}
return table;
}
// 获取下一步的地图;第i个空格,空格的移动方向为 j
Cmp.prototype.next = function(i,j){ // 如果无法移动,返回null
var cmp2 = null; // 下一步地图
var x = this.emps[i][0],
y = this.emps[i][1]; // 空格的坐标
var sx = SKEWS_XY[j][0],
sy = SKEWS_XY[j][1]; // 相对于空格的位置
var sNum = SKEWS_NUM[j]; // 相对于空格的位置
var px = x + sx,
py = y + sy; // 要移动的点的坐标
var pNum = this.getIntAt(px,py); // 要移动的点的num
if( pNum === undefined ) return cmp2; // 越界了
var aNum,bNum,cNum,dNum; // 临时变量
// 判断能否移动成功
if(pNum !== EMP_NUM){ // 不能挨着空格,因为空格不能移动
var tNums = this.copyNums(); // 地图Nums的拷贝
var tnps = NEAR_POINT_SKEWS[pNum]; // 点相邻的点们
var tEmps = this.copyEmps(); // 点坐标的浅拷贝
if(pNum === 0){ // 挨着小兵
tNums[x+y*WIDTH]=pNum; // 直接交换
tNums[px+py*WIDTH]=EMP_NUM;
tEmps[i] = [px,py];// 更新空格的位置
cmp2 = new Cmp(tNums,tEmps,this,this.count+1);
} else if(pNum === sNum){ // 挨着条状的,且跟移动方向一条直线的,可以移动
aNum = x+y*WIDTH; // 空格
bNum = px+py*WIDTH; // 点
cNum = (px+sx) + (py+sy) * WIDTH; // 另一个点
tNums[aNum]=pNum;
tNums[bNum]=tNums[cNum];
tNums[cNum]=EMP_NUM;
tEmps[i] = [(px+sx),(py+sy)];// 更新空格的位置 // 这儿其实不用new一个数组
cmp2 = new Cmp(tNums,tEmps,this,this.count+1);
} else if(tnps.length===1){ // 挨着条状的,且跟移动方向不在一条直线,
if(this.getIntAt(x+tnps[0][0],y+tnps[0][1]) === EMP_NUM){ // 判断空格旁边是否也是空格
// 可以移动
aNum = x+y*WIDTH; // 当前空格
bNum = px+py*WIDTH; // 挨着的点
cNum = (px+tnps[0][0]) + (py+tnps[0][1]) * WIDTH; // 另一个点
dNum = (x+tnps[0][0]) + (y+tnps[0][1]) * WIDTH; // 另一个空格
tNums[aNum]=pNum;
tNums[bNum]=EMP_NUM;
tNums[dNum]=tNums[cNum];
tNums[cNum]=EMP_NUM;
tEmps[i] = [px,py]; // 更新空格的位置
tEmps[1-i] = [(px+tnps[0][0]),(py+tnps[0][1])]; // 另一个空格
cmp2 = new Cmp(tNums,tEmps,this,this.count+1);
}
}else{ // 曹操 //这儿的 tnps.length === 3
var x2 = this.emps[1-i][0]; // 另一个空格挨着的点
var y2 = this.emps[1-i][1];
if((aNum=this.getIntAt(x2+sx,y2+sy))!==undefined && NEAR_POINT_SKEWS[aNum].length===3){ // 判断另一个空格是否也挨着曹操
aNum = x+y*WIDTH; // 空格
bNum = px+py*WIDTH; // 点
cNum = (px+sx) + (py+sy) * WIDTH; // 另一个点
tNums[aNum]=tNums[bNum];
tNums[bNum]=tNums[cNum];
tNums[cNum]=EMP_NUM;
tEmps[i] = [(px+sx),(py+sy)];// 更新空格的位置 // 这儿其实不用new一个数组
aNum = x2+y2*WIDTH; // 空格2
bNum = (x2+sx)+(y2+sy)*WIDTH; // 点2
cNum = (x2+sx+sx) + (y2+sy+sy) * WIDTH; // 另一个点2
tNums[aNum]=tNums[bNum];
tNums[bNum]=tNums[cNum];
tNums[cNum]=EMP_NUM;
tEmps[1-i] = [(x2+sx+sx),(y2+sy+sy)];// 更新空格的位置 // 这儿其实不用new一个数组
cmp2 = new Cmp(tNums,tEmps,this,this.count+1);
}
}
}
return cmp2;
}
// 最开始的地图
var mpNums = [
"0010","0110","0011","0010", // 2632
"1000","1100","1001","1000", // 8c98
"0010","0100","0001","0010", // 2412
"1000","0000","0000","1000", // 8008
"0000","1111","1111","0000", // 0ff0
].map(function(v){
return parseInt(v,2);//.toString(16);
}); // 26328c98241280080ff0
var startCmp = new Cmp(mpNums,[[1,4],[2,4]],null,0);
// 清空界面
function clear(){
var tables = document.getElementsByTagName("table");
for(var i=tables.length-1;i>=0;i--){ // 清空table
document.body.removeChild(tables[i]);
}
}
// 打印地图
function print(cmp){
if(cmp) {
var tt = cmp.toDomTable();
document.body.appendChild(tt);
}
}
function getBestPath(){
document.getElementById("btn1").setAttribute("disabled",true);
var time0 = new Date(), count = 0;
var stateMp={},i=j=ij=0,DEATH_MP={},OK_MP={},PATH_MP={},mps=[startCmp],cmp,cmp2;
while(mps.length){
count++;
i = ij>>2;
j = ij&3;
// 从队列里拿出来一个
cmp = mps[0];
cmp2 = cmp.next(i,j);
if(cmp2 !== null){
if(stateMp[cmp2.mpStr] === undefined){
stateMp[cmp2.mpStr] = PATH_MP;
cmp2.parentMp = cmp;
cmp2.count = cmp.count+1;
if(cmp2.isOk())break;
mps.push(cmp2);
}
}
ij++;
if(ij===8){
ij=0;
mps.shift();
}
}
var time1 = new Date();
document.getElementById("useTime").innerText = time1-time0;
cmp = cmp2;
mps.length = 0;
mps.push(cmp);
while(cmp=cmp.parentMp){
mps.push(cmp);
}
document.getElementById("stepNum").innerText = mps.length;
document.getElementById("count").innerText = count;
document.getElementById("info").style.display = "inline";
while(cmp=mps.pop()){
print(cmp);
}
}
</script>
</body>
</html>