前言
之前抖音上看到一个泛洪算法,说是找迷宫很是好奇,于是乎自己打算写一个尝试一下,用0,1代表迷宫的墙和路,我们就直接使用泛红快速找到终点吧
泛洪算法找终点有两种思路,一种DFS深度优先,一种BFS广度优先,各有各的优缺点
DFS深度可能一次性就能找到终点,但也可能很慢,BFS广度一旦找到终点,且一定是最短路径
一般都是使用BFS寻找效果更佳
实现DFS和BFS
下面我们分别实现DFS和BFS
DFS的逻辑就是持续遍历,一直找到终点位置,且需要一个全局变量作为标记,用于合适时间结束遍历,控制好边界即可,逻辑比较简单不多介绍,路径的话,则是结束的时候回溯一下即可
BFS核心逻辑就是,为了保证为了同步前进一步,需要下次所有可能位置加入队列,同一时间执行,然后在分别进行下一步,再次全部执行一遍,将执行的下一步加入队列,直到找到终点结束;由于没有同时运行的情况,实际就是将同一级按照顺序加入队列,每次执行四个方向的格子,分别将下次四个方向内容加入队列尾部,依次执行,这样就实现了广度优先效果(我们的案例加入了树状遍历结构,也是为了能够方便最快回溯,直接出最短路径😄)
js
//手写一个泛洪算法
//泛洪就像洪水扩散一样,以最快的方式找到终点(不一定是最短路径),有两种方式,BFS、DFS
//我们同时使用BFS、DFS两种策略来解决实现泛洪(广度、深度),只是广度看起来更像是洪水扩散传播一样,深度更像是一端接住了所有的水,才溢出到
//从策略也能看出,广度优先最先找到终点的直接结束,找到就是最近的,深度优先的则是最先碰到终点就结束,并不一定是最优解,否则还要一轮一轮对
//由于算法只要求找到终点即可,这里不继续扩展深度最短的了,根据他们的特性编写即可
//DFS深度优先(不一定是最短路径,而是第一条探索到终点的路)
const fanhongByDFS = (migong, start, fillColor, endColor) => {
//拷贝一个迷宫,用于更新计算使用,可方便与原数组对比
const cpMigong = migong.map((item) => item.slice());
const maxWidth = migong[0].length;
const maxHeight = migong.length;
//保存回溯后找到终点的路径
let isEnd = false;
const result = [];
const dfs = (x, y) => {
//处理越界
if (x < 0 || y < 0 || x >= maxWidth || y >= maxHeight) return;
const current = cpMigong[x][y];
//已经结束/已经上色/是墙 结束
if (isEnd || current === fillColor || current === 0) return;
if (current === endColor) {
result.push([x, y]);
return (isEnd = true);
}
cpMigong[x][y] = fillColor;
dfs(x + 1, y);
dfs(x - 1, y);
dfs(x, y + 1);
dfs(x, y - 1);
if (isEnd) result.push([x, y]);
};
dfs(start[0], start[1]);
if (!isEnd) {
throw new Error("没有找到终点");
}
return {
migong: cpMigong,
paths: result.reverse(),
};
};
const fanhongByBFS = (migong, start, fillColor, endColor) => {
const cpMigong = migong.map((item) => item.slice());
//开始扩散计数的方式无论哪边先找到终点,直接结束
const maxWidth = migong[0].length;
const maxHeight = migong.length;
//想要保证带出轨迹,我们需要更新的我们的queue,保存一些额外的信息
const queue = [
{
history: [start],
current: start,
},
];
let endQueue = null;
while (queue.length) {
const first = queue.shift();
const [x, y] = first.current;
if (x < 0 || y < 0 || x >= maxWidth || y >= maxHeight) continue;
const current = cpMigong[x][y];
if (current === fillColor || current === 0) continue;
if (current === endColor) {
endQueue = first;
break;
}
cpMigong[x][y] = fillColor;
const newtasks = [
[x + 1, y],
[x - 1, y],
[x, y + 1],
[x, y - 1],
];
const taskQueue = newtasks.map((item) => {
return {
history: [...first.history, item],
current: item,
};
});
queue.push(...taskQueue);
}
if (!endQueue) {
throw new Error("没有找到终点");
}
return {
migong: cpMigong,
paths: endQueue.history,
};
};
我们生成测试案例
js
const generateMigong = () => {
//生成的时候确认终点,但执行者不知道
const end = [9, 8];
//2表示出入口,0是屏障,1表示路
const migong = [
[1, 1, 1, 1, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 1, 0, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 0, 0, 1, 0, 0],
[0, 1, 1, 1, 1, 0, 1, 1, 0, 0],
[0, 1, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0],
[1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 0, 0, 1, 0, 0, 1, 0],
];
migong[end[0]][end[1]] = -1;
return migong;
};
//最短路径我们就是用标准的边集数组标识,也就是初始位置、目标位置,这里就不带权重了
(() => {
//传入开始和目标节点,里面假设不知道
const start = [0, 0];
const migong = generateMigong(start);
//通过泛洪以最快速度找到终点未知
// const res = fanhongByDFS(migong, start, 2, -1);
const res = fanhongByBFS(migong, start, 2, -1);
console.log(
"原始迷宫",
migong.map((item) => item.join(",")),
);
console.log(
"被淹到终点前的迷宫",
res.migong.map((item) => item.join(",")),
);
console.log(
"最短路线集合",
res.paths.map((item) => item.join(",")),
);
})();
打印结果如下
js
原始迷宫 [
'1,1,1,1,0,0,0,0,1,0',
'0,0,0,1,0,1,1,1,1,0',
'0,0,0,1,1,0,0,1,0,0',
'0,1,1,1,1,0,1,1,0,0',
'0,1,0,1,1,1,1,1,0,0',
'0,1,0,1,0,0,0,1,0,0',
'0,1,0,1,0,0,0,1,0,0',
'1,1,1,0,0,0,0,0,0,0',
'1,1,1,1,1,1,1,1,1,0',
'0,0,1,0,0,1,0,0,-1,0'
]
被淹到终点前的迷宫 [
'2,2,2,2,0,0,0,0,2,0',
'0,0,0,2,0,2,2,2,2,0',
'0,0,0,2,2,0,0,2,0,0',
'0,2,2,2,2,0,2,2,0,0',
'0,2,0,2,2,2,2,2,0,0',
'0,2,0,2,0,0,0,2,0,0',
'0,2,0,2,0,0,0,2,0,0',
'2,2,2,0,0,0,0,0,0,0',
'2,2,2,2,2,2,2,2,2,0',
'0,0,2,0,0,2,0,0,-1,0'
]
最短路线集合 [
'0,0', '0,1', '0,2', '0,3',
'1,3', '2,3', '3,3', '3,2',
'3,1', '4,1', '5,1', '6,1',
'7,1', '8,1', '8,2', '8,3',
'8,4', '8,5', '8,6', '8,7',
'8,8', '9,8'
]
将泛洪算法可视化
为了视觉效果,我做了一个简易的html泛红迭代效果,如下所示,并附上gif
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>泛洪测试</title>
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
#root {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
#root > div > div {
width: 20px;
height: 20px;
margin: 5px;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
const rootDom = document.getElementById("root");
const generateMigong = () => {
//生成的时候确认终点,但执行者不知道
const end = [9, 8];
//2表示出入口,0是屏障,1表示路
const migong = [
[1, 1, 1, 1, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 1, 0, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 0, 0, 1, 0, 0],
[0, 1, 1, 1, 1, 0, 1, 1, 0, 0],
[0, 1, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0],
[1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 0, 0, 1, 0, 0, 1, 0],
];
migong[end[0]][end[1]] = -1;
return migong;
};
const originMigong = generateMigong();
//DFS深度优先(不一定是最短路径,而是第一条探索到终点的路)
const fanhongByDFS = (migong, start, fillColor, endColor, callback) => {
//拷贝一个迷宫,用于更新计算使用,可方便与原数组对比
const cpMigong = migong.map((item) => item.slice());
const maxWidth = migong[0].length;
const maxHeight = migong.length;
//保存回溯后找到终点的路径
let isEnd = false;
const result = [];
const dfs = (x, y) => {
//处理越界
if (x < 0 || y < 0 || x >= maxWidth || y >= maxHeight) return;
const current = cpMigong[x][y];
//已经结束/已经上色/是墙 结束
if (isEnd || current === fillColor || current === 0) return;
if (current === endColor) {
result.push([x, y]);
return (isEnd = true);
}
cpMigong[x][y] = fillColor;
callback(cpMigong);
dfs(x + 1, y);
dfs(x - 1, y);
dfs(x, y + 1);
dfs(x, y - 1);
if (isEnd) result.push([x, y]);
};
dfs(start[0], start[1]);
if (!isEnd) {
throw new Error("没有找到终点");
}
return {
migong: cpMigong,
paths: result.reverse(),
};
};
//BFS广度优先(最先找到的就是最短路径)
const fanhongByBFS = (migong, start, fillColor, endColor, callback) => {
const cpMigong = migong.map((item) => item.slice());
//开始扩散计数的方式无论哪边先找到终点,直接结束
const maxWidth = migong[0].length;
const maxHeight = migong.length;
//想要保证带出轨迹,我们需要更新的我们的queue,保存一些额外的信息
const queue = [
{
history: [start],
current: start,
},
];
let endQueue = null;
while (queue.length) {
const first = queue.shift();
const [x, y] = first.current;
if (x < 0 || y < 0 || x >= maxWidth || y >= maxHeight) continue;
const current = cpMigong[x][y];
if (current === fillColor || current === 0) continue;
if (current === endColor) {
endQueue = first;
break;
}
cpMigong[x][y] = fillColor;
callback(cpMigong);
const newtasks = [
[x + 1, y],
[x - 1, y],
[x, y + 1],
[x, y - 1],
];
const taskQueue = newtasks.map((item) => {
return {
history: [...first.history, item],
current: item,
};
});
queue.push(...taskQueue);
}
if (!endQueue) {
throw new Error("没有找到终点");
}
return {
migong: cpMigong,
paths: endQueue.history,
};
};
let interval = null;
//最短路径我们就是用标准的边集数组标识,也就是初始位置、目标位置,这里就不带权重了
const test = async (type) => {
//传入开始和目标节点,里面假设不知道
const start = [0, 0];
const migong = originMigong;
resetDom(migong);
interval && clearInterval(interval);
//通过泛洪以最快速度找到终点未知
const migongQueue = [];
const callback = (migong) => {
const cpMigong = migong.map((item) => item.slice());
migongQueue.push(cpMigong);
};
const fnObj = {
DFS: () => fanhongByDFS(migong, start, 2, -1, callback),
BFS: () => fanhongByBFS(migong, start, 2, -1, callback),
};
const fn = fnObj[type];
if (!fn) {
console.log("没有找到对应的算法");
return;
}
fn();
interval = setInterval(() => {
if (migongQueue.length === 0) {
clearInterval(interval);
return;
}
const first = migongQueue.shift();
for (let i = 0; i < first.length; i++) {
for (let j = 0; j < first[0].length; j++) {
const item = first[i][j];
if (item === 0) {
rootDom.children[i].children[j].style.backgroundColor = "gray";
} else if (item === 1) {
rootDom.children[i].children[j].style.backgroundColor = "lightgray";
} else if (item === 2) {
rootDom.children[i].children[j].style.backgroundColor = "green";
} else if (item === -1) {
if (migongQueue.length === 0) {
rootDom.children[i].children[j].style.backgroundColor = "green";
} else {
rootDom.children[i].children[j].style.backgroundColor = "red";
}
}
}
}
}, 50);
};
const init = () => {
const length = 10; //边长为10
//初始化
for (let i = 0; i < length; i++) {
const div = document.createElement("div");
div.style =
"width: 100%; height: 20px; display: flex; justify-content: center; align-items: center; margin: 5px;";
rootDom.appendChild(div);
for (let j = 0; j < length; j++) {
const item = document.createElement("div");
div.appendChild(item);
}
}
//上色
resetDom(originMigong);
const div = document.createElement("div");
div.style =
"width: 100%; height: 20px; display: flex; justify-content: center; align-items: center; margin: 5px;";
rootDom.appendChild(div);
let item = document.createElement("button");
item.style.backgroundColor = "skyblue";
item.style.color = "white";
item.style.marginLeft = "10px";
item.innerHTML = "DFS";
item.onclick = () => test("DFS");
div.appendChild(item);
item = document.createElement("button");
item.style.backgroundColor = "skyblue";
item.style.color = "white";
item.style.marginLeft = "10px";
item.innerHTML = "BFS";
item.onclick = () => test("BFS");
div.appendChild(item);
};
function resetDom(migong) {
for (let i = 0; i < migong.length; i++) {
for (let j = 0; j < migong[0].length; j++) {
const item = migong[i][j];
if (item === 0) {
rootDom.children[i].children[j].style.backgroundColor = "gray";
} else if (item === 1) {
rootDom.children[i].children[j].style.backgroundColor = "lightgray";
} else if (item === 2) {
rootDom.children[i].children[j].style.backgroundColor = "green";
} else if (item === -1) {
rootDom.children[i].children[j].style.backgroundColor = "red";
}
}
}
}
init();
</script>
</body>
</html>
让我们看一下实现效果

最后
就介绍到这里吧,时不时写点小东西,避免完全被业务和ai抚平我们的大脑😄