泛洪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抚平我们的大脑😄

相关推荐
风骏时光牛马几秒前
AI_Coding:智能代码创作助手
前端
剪刀石头布啊9 小时前
vue-react为何要虚拟dom
前端
剪刀石头布啊9 小时前
ahooks常用的一些库,我们使用一下
前端
剪刀石头布啊9 小时前
从git更新本地提交记录到更新已经推送到远端记录
前端
Flynt11 小时前
axios 被投毒半年后,我把 npm 的"安全防线"实测了一遍:最信任的 SHA512 是安慰剂
前端·npm·node.js
excel12 小时前
Base62 和 Base64 有什么区别?
前端
风骏时光牛马13 小时前
大模型底层架构与AI源码深度剖析
前端
掘金挖土14 小时前
前端手摸手跑路之 AI 应用开发(九)
前端·后端
中文呀16 小时前
三维 GIS 离线缓存实战:逆向 CesiumOfflineCache,并亲手为它补上"自动更新"能力
前端