泛洪DFS、BFS

前言

之前抖音上看到一个泛洪算法,说是找迷宫很是好奇,于是乎自己打算写一个尝试一下,用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抚平我们的大脑😄

相关推荐
qetfw1 小时前
Windows Server AD CS:根 CA、Web 证书模板与域内自动注册
前端·windows·windows-server
剪刀石头布啊1 小时前
为什么React组件很少前缀,而vue的不少组件都有前缀
前端
剪刀石头布啊1 小时前
gird网格布局
前端
啃火龙果的兔子1 小时前
Google Chrome(谷歌浏览器)常用快捷键
前端·chrome
剪刀石头布啊1 小时前
js真的是单线程实现异步并发么?
前端
剪刀石头布啊1 小时前
js中改变this指向的操作有哪些
前端
剪刀石头布啊1 小时前
vue、react 列表中使用 index 作为 key 的话,修改某一个元素后会发生什么
前端
方方洛1 小时前
glowglow:语言无关的语法高亮器是怎么实现的
前端·前端框架
java_nnnn2 小时前
JavaEE进阶-HTML基础认识
前端·java-ee·html