union-find实现网页版的迷宫
前段时间研究了下并查集(union-find)算法。此算法是用来处理不相交集合的。这种数据模型在计算机和网络的世界里它是不可或缺的。在网络中各个网域间形成不相交集合;在社交网络中,社交圈之间形成不相交集合。
union-find的应用于测试无向图节点间的连通性,因此可以用来随机生成一个迷宫模型。迷宫又一个个小格子构成。把它想象成一个一个的房间,我们的目的是使入口房间和出口房间最终可以连通。为了保证路径复杂性,每次随机选择两个相邻的‘房间’判断它们是否相同,如果不通就打破‘墙体’。
下面是网页版迷宫的效果图:

迷宫的绘制采用svg技术实现,整理来说实现此效果要分3个步骤:
第一步:建立坐标系,根据规模(nxn)创建房间对象并对房间进行编号;这里我是以左上角为原点,横向为x轴,纵向为y轴,并逐行进行编号;
第二步:建立nxn个不相交集,确定入口和出口的位置;这里左上角为入口的位置、右下角是出口;
第三步:利用算法模型生成墙体;
整个代码的结构分为3块:
1、迷宫对象(maze.js);
2、房间对象(Cell.js);
3、算法模型(DSets.js)
html的核心代码:
页面的结构很简单,主要就是一个svg元素,并绘制一个矩形区域当作边界;
<div class="mazewrap"> <div class="box"> <svg xmlns="http://www.w3.org/2000/svg" id="mazesvg" width="500" height="500" viewBox="20 20 460 460" onload="maze.init(evt);"> <g id="group"> <rect x="40" y="40" width="420" height="420" style="stroke:black;stroke-width:1;fill:Wheat;"/> </g> </svg> </div> </div>
svg加载完毕即进行迷宫的初始化;
maze.js核心代码:
function Maze(){ this.initParams = { rows: 21, columns: 21, cellsize: 20 } } Maze.prototype.randomMaze = function(){ var r = this.initParams.rows; var c = this.initParams.columns; var dsets = new DSets(r*c); //debugger; //初始化房间对象 var cells = new Array(r*c); for(var i = 0; i < r; ++i) for(var j = 0; j < c; ++j){ var cell = new Cell(i, j, r, c); cells[cell.cellno] = cell; } //初始化集合对象 for(i = 0; i < cells.length; ++i){ dsets.makeSet(cells[i].cellno); } //打通墙体 while(dsets.findSet(0) != dsets.findSet(r*c-1)){ //1 选择任意一个房间 var cellno = GetRandomNum(0, r*c); var cell = cells[cellno]; //2 找到所有相邻的房间 var neighbors = cell.getNeighbors(); //3 随机选择一个相邻的房间,并判断两者的连通性 var ri = GetRandomNum(0, neighbors.length); var ncellno = neighbors[ri]; var ncell = cells[ncellno]; if(dsets.findSet(cellno) != dsets.findSet(ncellno)){ dsets.unionSet(cellno, ncellno); //4-1 拆墙 var group = svgRoot.getElementById('group'); var which = this.getWhichWall(cellno, ncellno),wall; switch(which){ case 1: //右墙 cell.right = 0; ncell.left = 0; try{ wall = svgRoot.getElementById(cell.getRightWall()); if(wall) group.removeChild(wall); }catch(e){ console&&console.log(svgRoot.getElementById(cell.getRightWall()),cell.getRightWall()); } break; case -1: cell.left = 0; ncell.right = 0; try{ wall = svgRoot.getElementById(cell.getLeftWall()); if(wall) group.removeChild(wall); }catch(e){ console&&console.log(svgRoot.getElementById(cell.getLeftWall()), cell.getLeftWall()); } break; case 2: //下墙 cell.bottom = 0; ncell.top = 0; try{ wall = svgRoot.getElementById(cell.getBottomWall()); if(wall) group.removeChild(wall); }catch(e){ console&&console.log(svgRoot.getElementById(cell.getBottomWall()), cell.getBottomWall()); } break; case -2: cell.top = 0; ncell.bottom = 0; try{ wall = svgRoot.getElementById(cell.getTopWall()); if(wall) group.removeChild(wall); }catch(e){ console&&console.log(svgRoot.getElementById(cell.getTopWall()),cell.getTopWall()); } break; } } } } /*根据编号获知是哪一面墙*/ Maze.prototype.getWhichWall = function(mcellno, rcellno){ var mark = 0; if(mcellno == rcellno - 1) //主墙是右墙 mark = 1; else if(mcellno == rcellno + 1) //主墙是右墙 mark = -1; else if(mcellno < rcellno) //主墙是下墙 mark = 2; else mark = -2; return mark; } Maze.prototype.init = function(evt) { svgdoc = evt.target.ownerDocument; svgRoot = svgdoc.getElementById('mazesvg'); var group = svgRoot.getElementById('group'); for(var i = 0; i <= this.initParams.rows; ++i){ for(var j = 0; j < this.initParams.columns; ++j){ var vline = svgdoc.createElementNS('http://www.w3.org/2000/svg','line'); vline.setAttribute('id', 'v_'+i+'_'+j); vline.setAttribute('x1', this.initParams.cellsize * (i)+40); vline.setAttribute('y1', this.initParams.cellsize * (j)+40); vline.setAttribute('x2', this.initParams.cellsize * (i)+40); vline.setAttribute('y2', this.initParams.cellsize * (j+1)+40); vline.setAttribute('style', 'stroke:SlateGray'); group.appendChild(vline); vline.addEventListener('mouseover', function(){console.log(this.getAttribute('id'))}); } } for(i = 0; i < this.initParams.columns; ++i){ for(j = 0; j <= this.initParams.rows; ++j){ var hline = svgdoc.createElementNS('http://www.w3.org/2000/svg','line'); hline.setAttribute('id', 'h_'+i+'_'+j); hline.setAttribute('x1', this.initParams.cellsize * (i)+40); hline.setAttribute('y1', this.initParams.cellsize * (j)+40); hline.setAttribute('x2', this.initParams.cellsize * (i+1)+40); hline.setAttribute('y2', this.initParams.cellsize * (j)+40); hline.setAttribute('style', 'stroke:SlateGray'); group.appendChild(hline); hline.addEventListener('mouseover', function(){console.log(this.getAttribute('id'))}); } } var exitPort = svgRoot.getElementById('h_'+(this.initParams.rows-1)+'_'+this.initParams.columns); var enterPort = svgRoot.getElementById('v_0_0'); enterPort.setAttribute('style', 'stroke:Wheat'); exitPort.setAttribute('style', 'stroke:Wheat'); //初始化迷宫 this.randomMaze(); } //随机获取整数 function GetRandomNum(Min, Max){ var Range = Max - Min; var Rand = Math.random(); return(Min + Math.floor(Rand * Range)); } var maze = new Maze();
接下来是Cell.js的代码:
//定义房间对象 function Cell(rowidx, colidx, rows, cols){ //横坐标 this.x = rowidx; //纵坐标 this.y = colidx; //横坐标阈值 this.xMax = rows - 1; //纵坐标阈值 this.yMax = cols - 1; //房间编号(按行) this.cellno = cols * colidx + rowidx; //房间墙体的标识 this.top = 1; this.right = 1; this.bottom = 1; this.left = 1; } Cell.prototype.getCellnoByCord = function(x, y){ return (this.yMax + 1) * y + x; } //获取邻居(返回邻居的编号) Cell.prototype.getNeighbors = function(){ var neighbors = [], x = this.x, y = this.y, xm = this.xMax, ym = this.yMax; if(x > 0){ //有左邻居 neighbors.push(this.getCellnoByCord(x-1, y)); } if(y > 0){ //有上邻居 neighbors.push(this.getCellnoByCord(x, y-1)); } if(x < xm){ //有右邻居 neighbors.push(this.getCellnoByCord(x+1, y)); } if(y < ym){ //有下邻居 neighbors.push(this.getCellnoByCord(x, y+1)); } return neighbors; } Cell.prototype.getLeftWall = function(){ var wallid = 'v_'+this.x+'_'+this.y; return wallid; } Cell.prototype.getRightWall = function(){ var wallid = 'v_'+(this.x+1)+'_'+this.y; return wallid; } Cell.prototype.getBottomWall = function(){ var wallid = 'h_'+this.x+'_'+(this.y+1); return wallid; } Cell.prototype.getTopWall = function(){ var wallid = 'h_'+this.x+'_'+this.y; return wallid; }
算法模型代码:
这里的算法采用了两种启发式策略,即“按秩求并”和“路径压缩”:
/***************** * 并查集算法绘制迷宫 * 运用2种启发式策略 */ var DSets = function(maxSize) { //最大节点数 this.maxSize = maxSize; //父节点集合 this.pa = new Array(maxSize); //高度上界 this.rank = new Array(maxSize); } /*创建单元集*/ DSets.prototype.makeSet = function(i){ this.pa[i] = i; this.rank[i] = 0; } /*带路径压缩的查找*/ DSets.prototype.findSet = function(i){ if(i != this.pa[i]) this.pa[i] = this.findSet(this.pa[i]); return this.pa[i]; } /*按秩合并*/ DSets.prototype.unionSet = function(i, j){ i = this.findSet(i); j = this.findSet(j); if(this.rank[i] > this.rank[j]) this.pa[j] = i; else{ this.pa[i] = j; if(this.rank[i] == this.rank[j]) this.rank[j]++; } }
浙公网安备 33010602011771号