【算法】万圣节前夕的迷宫挑战
这一天阳光和煦,小悦将捣蛋的侄子小明送回家后,紧绷的神经终于得以放松。在过去的一周里,小悦以无比的耐心和细心照顾着小明,同时也不忘在编程的道路上引领他迈出第一步。
万圣节前夕的一天,书房中的陈设在阳光下显得庄重而温暖,小悦正专心致志地处理着手头的工作。突然,一封邮件如不速之客般打破了这份宁静。邮件标题简短,仅以“随机迷宫-最短路径”几个字概括,而内容更像是一个简单的图像附件,几个由字符“W”和“.”组成的3x3或6x6网格。这个迷宫的难题在于,迷宫的面积是不固定的,并且一旦走到边缘,就会被迫停滞不前。
小悦看到这封邮件后,脸上充满了惊奇。这封邮件的来源不明,而显然作者也没想到邮件会辗转发到她的邮箱。但小悦是一个喜欢接受挑战的人,她决心找到这个迷宫的最短路径。
虽然小悦经常面对各种解谜挑战,但这个迷宫的难度对她来说确实不小。迷宫的起点是二维数组的(0,0),而终点是(5,5)。她可以按上下左右四个方向移动,但每一步只能移动一格。此外,一旦她走到了迷宫的边缘,她就不能再继续前进。这也就意味着她需要找到一条从起点到终点的不重复路径,而且不能通过迷宫的边缘。
小悦轻皱眉头,眼神里充满了困惑和挑战。她知道,这并不是一项简单的任务。但她没有表现出一丝的畏难情绪,反而跃跃欲试。她深吸了一口气,放松了自己的肩膀,然后开始尝试着找出可能的路径。
小悦首先使用了一个广度优先搜索(BFS)的策略。她从起点开始,每一步都尝试所有可能的路径,直到找到终点或者所有可能的路径都被排除。在这个过程中,她需要储存所有路径的信息,并且在探索过程中进行有效的回溯。
时间在不知不觉中流逝,小悦的脸上依然保持着淡定的神情。她的眼睛紧紧盯着迷宫图,目光中闪烁着思考的光芒。虽然解题的过程充满了挑战,但是她依然保持着冷静和专注。
当小悦遇到困难时,她并没有表现出一丝的焦虑或者急躁。相反,她更加专注于自己的思考和分析。她反复地观察着迷宫的特点,尝试着找到突破口。她的手指在键盘上轻轻地敲击着,她在不断地尝试和修正自己的路径算法。她的表情从认真到困惑,再到若有所思,最后到胸有成竹,这些微妙的变化都展示出她的解题过程是如此迷人。
经过一段时间的思考和探索,小悦终于找到了一个可行的路径算法。她的脸上露出了欣喜的笑容,这是她在解题过程中最美的表情。她的眼睛闪烁着兴奋的光芒,这是她在成功的喜悦中最为动人的地方。她轻轻地松开了握紧的拳头,似乎在跟自己的胜利庆祝。最终,小悦成功地解出了这个迷宫,她的脸上露出了一丝得意的笑容。
小悦面临的迷宫最短路径图例如下,她需要绕开W表示的墙,然后通过计算得到从(0,0)到终点(2,2)或(5,5)或(n,n)的最短路径步数:
3X3图例:
6X6图例:
算法实现1:
1 public static int PathFinder(string maze) 2 { 3 // 将迷宫字符串转换为二维数组 4 string[] rows = maze.Split('\n'); 5 int n = rows.Length; char[,] grid = new char[n, n]; 6 for (int i = 0; i < n; i++) 7 { for (int j = 0; j < n; j++) { grid[i, j] = rows[i][j]; } } 8 9 // 创建一个队列来执行广度优先搜索 10 Queue<(int, int)> queue = new Queue<(int, int)>(); 11 queue.Enqueue((0, 0)); 12 13 // 创建一个二维数组来存储到达每个位置的最小步数 14 int[,] steps = new int[n, n]; 15 for (int i = 0; i < n; i++) 16 { 17 for (int j = 0; j < n; j++) 18 { 19 steps[i, j] = int.MaxValue; 20 } 21 } 22 steps[0, 0] = 0; 23 24 // 定义四个基本方向 25 int[] dx = { 0, 0, 1, -1 }; 26 int[] dy = { 1, -1, 0, 0 }; 27 28 // 执行广度优先搜索 29 while (queue.Count > 0) 30 { 31 var (x, y) = queue.Dequeue(); 32 33 // 检查是否到达出口 34 if (x == n - 1 && y == n - 1) 35 { 36 return steps[x, y]; 37 } 38 39 // 探索四个基本方向 40 for (int i = 0; i < 4; i++) 41 { 42 int nx = x + dx[i]; 43 int ny = y + dy[i]; 44 45 // 检查新位置是否在迷宫边界内 46 if (nx >= 0 && nx < n && ny >= 0 && ny < n) 47 { 48 // 检查新位置是否为空并且可以移动到该位置 49 if (grid[nx, ny] == '.' && steps[x, y] + 1 < steps[nx, ny]) 50 { 51 steps[nx, ny] = steps[x, y] + 1; 52 queue.Enqueue((nx, ny)); 53 } 54 } 55 } 56 } 57 58 // 如果执行到这里,表示没有路径到达出口 59 return -1; 60 }
这段代码实现了一个迷宫路径搜索的算法。它使用了c#的Queue对象和二维数组来完成搜索过程。
-
Queue对象:在这段代码中,Queue<(int, int)> queue = new Queue<(int, int)>(); 创建了一个队列,用于执行广度优先搜索。队列是一种先进先出(FIFO)的数据结构,它可以用来存储一系列元素,并按照它们被添加的顺序进行处理。在这个算法中,队列用于存储待探索的位置坐标,以便按照广度优先的顺序进行搜索。
-
二维数组:int[,] steps = new int[n, n]; 创建了一个二维数组,用于存储到达每个位置的最小步数。二维数组是一个由行和列组成的表格结构,可以用来表示和操作二维数据。在这个算法中,二维数组用于记录从起点到达每个位置的最小步数,以便在搜索过程中更新和比较步数。
区别:
-
Queue对象是一个动态大小的数据结构,可以在运行时添加和删除元素。它适用于需要按照先进先出的顺序处理元素的场景,比如广度优先搜索、任务调度等。而IList对象是一个接口,它定义了一组用于访问和操作元素的方法和属性。IList接口的实现类(如List)可以用来创建一个可变大小的集合,可以通过索引访问和修改元素。
-
二维数组是一个固定大小的表格结构,它的行数和列数在创建时就确定,无法在运行时改变。二维数组适用于需要表示和操作二维数据的场景,比如矩阵运算、图像处理等。而IList对象可以通过List、ArrayList等实现类来创建一个可变大小的集合,可以动态添加和删除元素。
在这个算法中,使用Queue<(int,int)>对象是合适的,因为广度优先搜索需要按照先进先出的顺序处理元素。Queue<(int,int)>对象提供了Enqueue()和Dequeue()方法,可以方便地实现先进先出的逻辑。
虽然IList<(int,int)>对象也可以用来实现广度优先搜索,但是需要手动实现队列的先进先出的逻辑,比如使用Add()方法添加元素到末尾,使用RemoveAt(0)方法移除队首元素。这样会增加代码的复杂性,并且性能可能不如直接使用Queue<(int,int)>对象。
通过测试用例解释算法:
1 public void TestBasic() 2 { 3 4 string a = ".W.\n" + 5 ".W.\n" + 6 "...", 7 8 b = ".W.\n" + 9 ".W.\n" + 10 "W..", 11 12 Assert.AreEqual(4, Finder.PathFinder(a)); 13 Assert.AreEqual(-1, Finder.PathFinder(b)); 14 }
首先,我们定义了两个测试用例a和b。a和b都是由三行三列的字符矩阵表示的迷宫。其中,字符'.'表示可以通过的路径,字符'W'表示墙。
对于测试用例a,迷宫矩阵如下:
.W.
.W.
...
我们希望找到从起点(0, 0)到终点(2, 2)的最短路径。在迷宫中,我们可以向右移动两步,然后向下移动两步,即可到达终点。因此,最短路径的步数为4。
对于测试用例b,迷宫矩阵如下:
.W.
.W.
W..
我们希望找到从起点(0, 0)到终点(2, 2)的最短路径。在迷宫中,无论如何移动,都无法到达终点。因为迷宫中的第三行全部是墙,无法通过。因此,不存在从起点到终点的路径,返回-1。
在PathFinder算法中,我们首先将迷宫字符串转换为二维数组,并创建一个队列来执行广度优先搜索。然后,我们创建一个二维数组来存储到达每个位置的最小步数。初始时,所有位置的最小步数都设置为最大值,除了起点位置的最小步数为0。
var (x, y) = queue.Dequeue();
用于从队列中取出队首元素,并将其赋值给变量(x, y)
。在这个算法中,(x, y)
表示当前位置的坐标。
queue.Enqueue((nx, ny));
用于将新位置的坐标(nx, ny)
加入队列中。在这个算法中,我们在探索四个基本方向时,如果新位置满足条件,就将其加入队列中,以便在下一轮循环中继续探索该位置。通过使用队列,我们可以确保广度优先搜索按照层级顺序进行,从而找到最短路径的步数。
接下来,我们使用一个while循环来执行广度优先搜索。在每一轮循环中,我们从队列中取出队首元素,并检查是否到达出口。如果到达出口,则返回该位置的最小步数。否则,我们探索四个基本方向,并检查新位置是否在迷宫边界内,是否为空并且可以移动到该位置。如果满足条件,我们更新新位置的最小步数,并将新位置加入队列中。
最后,如果执行到循环结束仍然没有找到路径到达出口,则返回-1。
算法实现2(递归洪水填充):
洪水递归填充算法,也称为洪水填充算法或种子填充算法,是一种用于计算机图形学中的图像处理技术。它的目标是根据给定的种子点和阈值,将图像中的某个区域进行填充。
这个算法的由来可以追溯到计算机图形学的早期。在早期的计算机图形学中,人们希望能够通过计算机来实现图像的自动填充,以便进行图像编辑和处理。而洪水递归填充算法就是为了满足这个需求而提出的。
洪水递归填充算法的基本思想是从种子点开始,将其颜色值扩散到相邻的像素点,然后再递归地将这些像素点的颜色值扩散到它们的相邻像素点,直到达到某个结束条件。这个结束条件通常是像素点的颜色值与种子点的颜色值不同或者达到了设定的阈值。
洪水递归填充算法的优势在于它的简单性和效率。它只需要对每个像素点进行一次颜色值的比较和填充操作,而不需要对整个图像进行复杂的计算。因此,它在处理简单的图像填充任务时非常高效。
洪水递归填充算法在许多图像编辑和处理软件中得到了广泛应用。例如,它可以用于图像的背景去除、图像的颜色替换、图像的区域分割等任务。它也可以用于计算机游戏中的地图填充、连连看游戏中的消除算法等场景。
1 using System.Linq; 2 using static System.Math; 3 4 public class Finder 5 { 6 public static int PathFinder(string maze) 7 { 8 // 将迷宫字符串转换为二维数组 9 var m = maze.Split('\n').Select(a => a.Select(b => (b == '.') ? 999 : 0).ToArray()).ToArray(); 10 // 从起点开始执行递归洪水填充 11 Flood(0, 0, m, 0); 12 // 返回终点的最小步数,如果没有路径到达终点,则返回-1 13 return m.Last().Last() < 999 ? m.Last().Last() : -1; 14 } 15 16 17 private static void Flood(int x, int y, int[][] m, int step) 18 { 19 // 检查当前位置是否可以到达 20 if (Min(x, y) > -1 && Max(x, y) < m.GetLength(0) && m[x][y] > step) 21 { // 更新当前位置的最小步数 22 m[x][y] = step; // 探索四个基本方向 23 var d = new[] { 0, 0, 1, -1 }; 24 for (int i = 0; i < 4; i++) Flood(x + d[i], y + d[3 - i], m, step + 1); 25 } 26 } 27 }
对比算法1和算法2,可以得出以下结论:
-
效率和性能:算法1使用队列来执行广度优先搜索,而算法2使用递归洪水填充算法来执行搜索。在某些情况下,递归函数可能比队列更高效。递归函数可以避免队列入队出队的操作,节省了一些时间和空间开销。因此,算法2可能比算法1更高效。
-
如果迷宫规模较小且不太复杂,可以选择算法1。算法1使用队列和二维数组来进行搜索,代码更直观易懂。如果迷宫规模较大或者复杂度较高,建议选择算法2。算法2使用递归函数和一维数组来进行搜索,可能更高效。另外,算法2还可以节省一些内存空间。
这两个算法可以应用于汽车导航躲避拥堵和路线计算等场景。
-
算法1(广度优先搜索):广度优先搜索算法可以用于计算最短路径。在汽车导航中,可以使用广度优先搜索来计算最短路径以避免拥堵。通过将道路网格化,每个网格表示一个道路交叉口或路段,可以使用广度优先搜索算法来找到从起点到终点的最短路径。通过计算每个网格的最小步数,可以确定最短路径,并根据步数来规划导航路线。
-
算法2(递归洪水填充):递归洪水填充算法可以用于计算到达目标位置的最小步数。在汽车导航中,可以使用递归洪水填充算法来计算到达目标位置的最小时间或最小距离。通过将道路网格化,每个网格表示一个道路交叉口或路段,可以使用递归洪水填充算法来计算从起点到每个网格的最小步数。通过计算每个网格的最小步数,可以确定最短路径,并根据步数来规划导航路线。
除了汽车导航,这两个算法还可以应用于以下场景:
-
游戏开发:广度优先搜索算法可以用于游戏中的路径规划,例如寻找最短路径或避免障碍物。递归洪水填充算法可以用于游戏中的区域填充,例如地图生成或区域探索。
-
图像处理:广度优先搜索算法可以用于图像分割,例如将图像分割为不同的区域或对象。递归洪水填充算法可以用于图像填充,例如颜色填充或图像修复。
-
社交网络分析:广度优先搜索算法可以用于社交网络中的关系分析,例如寻找两个人之间的最短路径或查找具有特定关系的人。递归洪水填充算法可以用于社交网络中的信息传播分析,例如查找信息传播的路径或影响力传播的范围。
-
网络路由:广度优先搜索算法可以用于网络路由中的路由选择,例如寻找最短路径或避免拥堵。递归洪水填充算法可以用于网络路由中的路由优化,例如根据网络拓扑和流量情况来优化路由选择。
虽然这两个算法的实现方式和应用场景不同,但它们都可以用于解决路径规划和区域填充等问题。在实际应用中,可以根据具体的需求和场景选择合适的算法。
可以将算法1修改为同时返回最小步数和路径的形式:
修改之后的PathFinder算法已具有一定的实用价值,可以实现找到避免拥堵的最短路径,并实时显示在地图上。
1 public static (int, List<(int, int)>) PathFinder(string maze) 2 { 3 // 将迷宫字符串转换为二维数组 4 string[] rows = maze.Split('\n'); 5 int n = rows.Length; 6 char[,] grid = new char[n, n]; 7 for (int i = 0; i < n; i++) 8 { 9 for (int j = 0; j < n; j++) 10 { 11 grid[i, j] = rows[i][j]; 12 } 13 } 14 15 // 创建一个队列用于执行广度优先搜索 16 Queue<(int, int)> queue = new Queue<(int, int)>(); 17 queue.Enqueue((0, 0)); 18 19 // 创建一个二维数组来存储到达每个位置的最小步数 20 int[,] steps = new int[n, n]; 21 for (int i = 0; i < n; i++) 22 { 23 for (int j = 0; j < n; j++) 24 { 25 steps[i, j] = int.MaxValue; 26 } 27 } 28 steps[0, 0] = 0; 29 30 // 创建一个二维数组来存储每个位置的前一个位置 31 (int, int)[,] prev = new (int, int)[n, n]; 32 33 // 定义四个方向:上、下、左、右 34 int[] dx = { 0, 0, 1, -1 }; 35 int[] dy = { 1, -1, 0, 0 }; 36 37 // 执行广度优先搜索 38 while (queue.Count > 0) 39 { 40 var (x, y) = queue.Dequeue(); 41 42 // 检查是否抵达出口 43 if (x == n - 1 && y == n - 1) 44 { 45 // 使用前一个位置的信息构建路径 46 List<(int, int)> path = new List<(int, int)>(); 47 path.Add((x, y)); 48 while (x != 0 || y != 0) 49 { 50 var (px, py) = prev[x, y]; 51 path.Add((px, py)); 52 x = px; 53 y = py; 54 } 55 path.Reverse(); 56 57 return (steps[x, y], path); 58 } 59 60 // 探索四个方向 61 for (int i = 0; i < 4; i++) 62 { 63 int nx = x + dx[i]; 64 int ny = y + dy[i]; 65 66 // 检查新位置是否在迷宫边界内 67 if (nx >= 0 && nx < n && ny >= 0 && ny < n) 68 { 69 // 检查新位置是否为空并且可以移动到它 70 if (grid[nx, ny] == '.' && steps[x, y] + 1 < steps[nx, ny]) 71 { 72 steps[nx, ny] = steps[x, y] + 1; 73 prev[nx, ny] = (x, y); 74 queue.Enqueue((nx, ny)); 75 } 76 } 77 } 78 } 79 80 // 如果到达这里,表示没有路径到达出口 81 return (-1, null); 82 }
测试用例:
1 using NUnit.Framework; 2 using System; 3 using System.Collections.Generic; 4 using System.Linq; 5 using System.Text; 6 7 public class SolutionTest 8 { 9 10 [Test] 11 public void TestBasic() 12 { 13 14 string a = ".W.\n" + 15 ".W.\n" + 16 "...", 17 18 b = ".W.\n" + 19 ".W.\n" + 20 "W..", 21 22 c = "......\n" + 23 "......\n" + 24 "......\n" + 25 "......\n" + 26 "......\n" + 27 "......", 28 29 d = "......\n" + 30 "......\n" + 31 "......\n" + 32 "......\n" + 33 ".....W\n" + 34 "....W."; 35 36 Assert.AreEqual(4, Finder.PathFinder(a)); 37 Assert.AreEqual(-1, Finder.PathFinder(b)); 38 Assert.AreEqual(10, Finder.PathFinder(c)); 39 Assert.AreEqual(-1, Finder.PathFinder(d)); 40 } 41 42 [Test] 43 public void TestRandom50() 44 { 45 46 for (int i = 0; i < 50; i++) 47 { 48 int len = 4 + rand.Next(7); 49 string str = RandomMaze(len); 50 Console.WriteLine(str+ '\n'); 51 Assert.AreEqual(TestFinder.PathFinder(str), Finder.PathFinder(str), str); 52 } 53 } 54 55 [Test] 56 public void TestRandomBigMaze10() 57 { 58 for (int i = 0; i < 10; i++) 59 { 60 String str = RandomMaze(100); 61 Assert.AreEqual(TestFinder.PathFinder(str), Finder.PathFinder(str), str); 62 } 63 } 64 65 [Test] 66 public void TestSnakesLabirinth() 67 { 68 69 Assert.AreEqual(96, Finder.PathFinder( 70 ".W...W...W...\n" + 71 ".W.W.W.W.W.W.\n" + 72 ".W.W.W.W.W.W.\n" + 73 ".W.W.W.W.W.W.\n" + 74 ".W.W.W.W.W.W.\n" + 75 ".W.W.W.W.W.W.\n" + 76 ".W.W.W.W.W.W.\n" + 77 ".W.W.W.W.W.W.\n" + 78 ".W.W.W.W.W.W.\n" + 79 ".W.W.W.W.W.W.\n" + 80 ".W.W.W.W.W.W.\n" + 81 ".W.W.W.W.W.W.\n" + 82 "...W...W...W.")); 83 84 } 85 86 [Test] 87 public void TestVerySmallLabirinth() 88 { 89 90 Assert.AreEqual(0, Finder.PathFinder(".")); 91 } 92 93 private static readonly Random rand = new Random(); 94 private string RandomMaze(int len) 95 { 96 97 string template = new string('.', len); 98 StringBuilder[] maze = Enumerable.Range(0, len).Select(i => new StringBuilder(template)).ToArray(); 99 100 for (int j = 0; j < len * len / 3; j++) 101 { 102 maze[rand.Next(len)][rand.Next(len)] = 'W'; 103 } 104 maze[0][0] = '.'; 105 maze[len - 1][len - 1] = '.'; 106 return string.Join("\n", maze.Select(b => b.ToString())); 107 } 108 109 private class TestFinder 110 { 111 public static int PathFinder(string maze) 112 { 113 return new TestFinder().find(maze); 114 } 115 116 private string[] m; 117 private int[,] steps; 118 private Queue<int[]> front; 119 private int goal; 120 121 public int find(string maze) 122 { 123 m = maze.Split('\n'); 124 125 steps = new int[m.Length, m.Length]; 126 127 steps[0, 0] = 1; 128 front = new Queue<int[]>(); 129 front.Enqueue(new[] { 0, 0 }); 130 131 goal = m.Length - 1; 132 133 while (front.Any()) 134 { 135 136 int[] c = front.Dequeue(); 137 138 if (c[0] == goal && c[1] == goal) 139 return steps[goal, goal] - 1; 140 141 int step = steps[c[0], c[1]] + 1; 142 TryStep(c[0] - 1, c[1], step); 143 TryStep(c[0] + 1, c[1], step); 144 TryStep(c[0], c[1] - 1, step); 145 TryStep(c[0], c[1] + 1, step); 146 147 } 148 return -1; 149 } 150 151 private void TryStep(int r, int c, int v) 152 { 153 if (r < 0 || r > goal || c < 0 || c > goal || steps[r, c] != 0) 154 return; 155 156 string row = m[r]; 157 if (row[c] == '.') 158 { 159 steps[r, c] = v; 160 front.Enqueue(new int[] { r, c }); 161 } 162 } 163 } 164 }