天花板编程手把手计划-第1期-第3天

上一篇的迷宫问题难倒了很多人,对于初学者这个相对综合的问题可能的确有点难,不过并非完成不了。我们今天就来看看初学者如何用最基础的方法解决这个问题。

1. 题目

如图所示,有一个6 * 6的迷宫,左上角为入口,右下角为出口。图中0的位置可以走,1的位置不能走。请编程找出唯一的一条通过迷宫的路。

2. 分析

2.1 经典解法

这道题是一个C语言编程中的经典题目,网上的答案有很多。有人还真去查了,但结果有点崩溃,他们看到的是:回溯、递归、堆栈、迭代、DFS这些初学者根本没怎么听过的关键词。那些没有注释的例程也是怎么也看不懂。其实,就因为题目太过经典,所以才有各种五花八门的解法。

总结一下,主流的解法就两种:

  • DFS 深度优先遍历法

通过递归的方式不断从入口寻找下一个可行的点,依次执行下去。一旦发现死路就退回到上一个点重新寻找新路线。

理论上遍历了所有可能的路线之后,正确的路线一定能够找到。

  • BFS 广度优先遍历法

这个算法的特点是不需要递归,需要自己维护一个顺序表,之后通过循环同时寻找和当前点相连的每一个联通的点,直到找到出口。

这个算法的特点是不需要回退。

以上两种是数据结构中的经典算法,不过我们今天要讲的并非这两种。所以千万不要被吓到了。

2.2 功能分解

首先说一下,经典的编程问题,每个都有经典的解法。这些解法是很多人共同总结出来的可以解决一类问题的最优算法。然而,对于某一个具体的问题,这些算法并不一定是最优的或者说最简单的。

这道题就是这样。迷宫问题最大的难点就是它有很多岔路是走不通的。那我们想想,如果迷宫没有岔路你会做吗?

相信大部分人都能解决。那么这就是我们的目标。

  • 功能一:迷宫打印

把迷宫打印在屏幕上,其实是二维数组的打印。

  • 功能二:迷宫剪支

把迷宫中所有的死路删除。

  • 功能三:标记正确路线

最后我们需要把正确的路线打印在屏幕中。

从步骤上说,我们要做的就是这三件事。要把大象放冰箱,总共分几步,分三步。

3. 代码框架

依然是堆积木,我们今天先写main函数的框架,之后把积木填进去。

void main()
{
    int i, j, k;
    int maze[MAX_SIZE][MAX_SIZE] =
    {
        { 0, 1, 0, 1, 1, 1 },
        { 0, 0, 0, 1, 0, 1 },
        { 0, 1, 1, 0, 0, 0 },
        { 0, 1, 1, 0, 1, 0 },
        { 0, 0, 0, 0, 1, 0 },
        { 0, 1, 1, 1, 1, 0 }
    };

    // 打印原始迷宫
    printf("原始迷宫:\n");
    
    // 迷宫剪支

    // 打印剪支后的迷宫
    printf("\n剪支后的迷宫:\n");

    // 打印带正确路线的迷宫
    printf("\n标记迷宫路线:\n");

}

这里我多加了一部分“打印剪支后的迷宫”,为了让大家可以看到中间过程,方便调试。

4. 打印迷宫

这就是一个普通的二维数组打印,代码如下:

void PrintMaze(int arrMaze[][MAX_SIZE])
{
    int i, j;
    for (i = 0; i < MAX_SIZE; i++)
    {
        for (j = 0; j < MAX_SIZE; j++)
        {
            printf("%3d", arrMaze[i][j]);
        }
        
        printf("\n");
    }
}

5. 迷宫剪支

5.1 空间复制

由于我们做剪支时会修改二维数组的内容,为了保持原始数组不被破坏,我们要先复制出一个新的二维数组。复制函数如下:

void Copy(int* pDest, int* pSrc, int cnt)
{
    while (cnt--)
    {
        *pDest++ = *pSrc++;
    }
}

这几乎是课本上的例子程序,应该不用多介绍了。没学过指针的同学可以用数组的方法来实现。这里又涉及到二维数组的空间当一维数组来使用。

接下来,我们就可以在一块复制出来的迷宫里做任何想要的操作了。

5.2 剪支

如图所示,我们的目标就是把左边这个二维数组转换成右边这个二维数组。

算法很简单,遍历二维数组,找到上下左右四个位置中只有一个或根本没有0的位置,如果它是0则改为1。

用人话说就是找到每条死路的最后一个位置,把它变成1。这样循环下去就能够将整条死路彻底封死。如下图:

图中红色部分是一条死路,但我们遍历一遍只能把最右边的红色位置写入1,此时死路的尽头变成了中间这个红色方块。我们还需要继续循环,但这种死路有几个长度就循环几次的方式显然不是最优解。因此我们要设计一个算法能够一次就封死整个路径。

代码如下:

int CountPath(int maze[][MAX_SIZE], int i, int j)
{
    int cnt = 0;
    // Up Point
    if (i > 0)
    {
        if (maze[i - 1][j] == 0 || maze[i - 1][j] == 2)
        {
            cnt++;
        }
    }

    // Down Point
    if (i < MAX_SIZE - 1)
    {
        if (maze[i + 1][j] == 0 || maze[i + 1][j] == 2)
        {
            cnt++;
        }
    }

    // Left Point
    if (j > 0)
    {
        if (maze[i][j - 1] == 0 || maze[i][j - 1] == 2)
        {
            cnt++;
        }
    }

    // Right Point
    if (j < MAX_SIZE - 1)
    {
        if (maze[i][j + 1] == 0 || maze[i][j + 1] == 2)
        {
            cnt++;
        }
    }

    return cnt;
}

void FindNext(int maze[][MAX_SIZE], int i, int j)
{
    int cnt;
    int line, colum;
    // Up Point
    if (i > 0)
    {
        line = i - 1;
        colum = j;

        if (maze[line][colum] == 0)
        {
            if (CountPath(maze, line, colum) == 1)
            {
                maze[line][colum] = 1;
                FindNext(maze, line, colum);
            }
        }
    }

    // Down Point
    if (i < MAX_SIZE - 1)
    {
        line = i + 1;
        colum = j;

        if (maze[line][colum] == 0)
        {
            if (CountPath(maze, line, colum) == 1)
            {
                maze[line][colum] = 1;
                FindNext(maze, line, colum);
            }
        }
    }

    // Left Point
    if (j > 0)
    {
        line = i;
        colum = j - 1;

        if (maze[line][colum] == 0)
        {
            if (CountPath(maze, line, colum) == 1)
            {
                maze[line][colum] = 1;
                FindNext(maze, line, colum);
            }
        }
    }

    // Right Point
    if (j < MAX_SIZE - 1)
    {
        line = i;
        colum = j + 1;

        if (maze[line][colum] == 0)
        {
            if (CountPath(maze, line, colum) == 1)
            {
                maze[line][colum] = 1;
                FindNext(maze, line, colum);
            }
        }
    }
}

void CutBranch(int arrMaze[][MAX_SIZE])
{
    int i, j;

    arrMaze[0][0] = 2; // entry
    arrMaze[MAX_SIZE - 1][MAX_SIZE - 1] = 2; // exit
    for (i = 0; i < MAX_SIZE; i++)
    {
        for (j = 0; j < MAX_SIZE; j++)
        {
            if (arrMaze[i][j] != 0)
            {
                continue;
            }
            
            if (CountPath(arrMaze, i, j) <= 1)
            {
                arrMaze[i][j] = 1;
                FindNext(arrMaze, i, j);
            }
        }
    }
}

我们通过调用CutBranch()函数来遍历每一个位置。注意,这里只做了一次遍历。

当某个位置是0时,调用CountPath()函数计算出它的周围有几个0。如果是0个或1个,说明它是死路,修改它的值为1之后再调用FindNext()函数判断它周围的位置。

重点说一下,FindNext()函数,它的参数是一个二维数组和一组i,j表示的位置。功能是判断这个位置上下左右四个位置是否为死路,如果是就修改成1之后再调用FindNext()寻找修改之后它周围是否存在死路的情况。

注意:

  • 在FindNext()中首先要判断是否在迷宫边上,否则直接访问可能会造成数组越界
  • 由于入口和出口两个位置也满足死路的条件,因此我们在CutBranch()中把它们置为2

CutBranch()函数执行完后,我们就得到了一个只有一条正确路线的迷宫。

6. 标记路线

最后,把我们得到这条路径在原迷宫中标记出来。我们用9表示正确的路线。

void MarkMaze(int srcMaze[][MAX_SIZE], int markMaze[][MAX_SIZE])
{
    int i, j;
    for (i = 0; i < MAX_SIZE; i++)
    {
        for (j = 0; j < MAX_SIZE; j++)
        {
            if (markMaze[i][j] == 0)
            {
                srcMaze[i][j] = 9;
            }
        }
    }
}

遍历markMaze,发现0就在srcMaze相应的位置上写9。

最后我们看看main函数中的函数调用:

void main()
{
    int i, j, k;
    int maze[MAX_SIZE][MAX_SIZE] =
    {
        { 0, 1, 0, 1, 1, 1 },
        { 0, 0, 0, 1, 0, 1 },
        { 0, 1, 1, 0, 0, 0 },
        { 0, 1, 1, 0, 1, 0 },
        { 0, 0, 0, 0, 1, 0 },
        { 0, 1, 1, 1, 1, 0 }
    };
    
    int mazeCpy[MAX_SIZE][MAX_SIZE];

    // 打印原始迷宫
    printf("原始迷宫:\n");
    PrintMaze(maze);

    // 迷宫剪支
    Copy((int*)mazeCpy, (int*)maze, MAX_SIZE * MAX_SIZE);

    CutBranch(mazeCpy);

    // 打印剪支后的迷宫
    printf("\n剪支后的迷宫:\n");
    PrintMaze(mazeCpy);
    
    // 打印带正确路线的迷宫
    MarkMaze(maze, mazeCpy);

    printf("\n标记迷宫路线:\n");
    PrintMaze(maze);
}

看看最后的执行结果:

7. 总结

这个算法是不是没有用到任何大家没学过的知识呢。先把我当代码看懂,之后考虑下面两个问题:

  • 这个算法找到的路线是否一定是最短的路线呢?
  • 判断每个位置是否在迷宫边上非常麻烦,有没有什么好办法简化这个操作呢?

请大家仔细思考这两个问题,我会在群里公布优化后的代码。

8. 课后作业

把一个硬币抛5次,打印出所有可能出现的情况。1表示正面,0表示背面。比如:

全正面

1 1 1 1 1

全背面

0 0 0 0 0

我是天花板,让我们一起在软件开发中自我迭代。
如有任何问题,欢迎与我联系。


上一篇:天花板编程手把手计划-第1期-第2天

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容