天花板編程手把手計劃-第1期-第3天

上一篇的迷宮問題難倒了很多人桐猬,對于初學者這個相對綜合的問題可能的確有點難麦撵,不過并非完成不了。我們今天就來看看初學者如何用最基礎的方法解決這個問題溃肪。

1. 題目

如圖所示免胃,有一個6 * 6的迷宮,左上角為入口惫撰,右下角為出口羔沙。圖中0的位置可以走,1的位置不能走厨钻。請編程找出唯一的一條通過迷宮的路扼雏。

2. 分析

2.1 經典解法

這道題是一個C語言編程中的經典題目坚嗜,網上的答案有很多。有人還真去查了诗充,但結果有點崩潰苍蔬,他們看到的是:回溯、遞歸蝴蜓、堆棧碟绑、迭代、DFS這些初學者根本沒怎么聽過的關鍵詞茎匠。那些沒有注釋的例程也是怎么也看不懂格仲。其實,就因為題目太過經典诵冒,所以才有各種五花八門的解法抓狭。

總結一下,主流的解法就兩種:

  • DFS 深度優(yōu)先遍歷法

通過遞歸的方式不斷從入口尋找下一個可行的點造烁,依次執(zhí)行下去。一旦發(fā)現死路就退回到上一個點重新尋找新路線午笛。

理論上遍歷了所有可能的路線之后惭蟋,正確的路線一定能夠找到。

  • BFS 廣度優(yōu)先遍歷法

這個算法的特點是不需要遞歸药磺,需要自己維護一個順序表告组,之后通過循環(huán)同時尋找和當前點相連的每一個聯通的點,直到找到出口癌佩。

這個算法的特點是不需要回退木缝。

以上兩種是數據結構中的經典算法,不過我們今天要講的并非這兩種围辙。所以千萬不要被嚇到了我碟。

2.2 功能分解

首先說一下,經典的編程問題姚建,每個都有經典的解法矫俺。這些解法是很多人共同總結出來的可以解決一類問題的最優(yōu)算法。然而掸冤,對于某一個具體的問題厘托,這些算法并不一定是最優(yōu)的或者說最簡單的。

這道題就是這樣稿湿。迷宮問題最大的難點就是它有很多岔路是走不通的铅匹。那我們想想,如果迷宮沒有岔路你會做嗎饺藤?

相信大部分人都能解決包斑。那么這就是我們的目標流礁。

  • 功能一:迷宮打印

把迷宮打印在屏幕上,其實是二維數組的打印舰始。

  • 功能二:迷宮剪支

把迷宮中所有的死路刪除崇棠。

  • 功能三:標記正確路線

最后我們需要把正確的路線打印在屏幕中。

從步驟上說丸卷,我們要做的就是這三件事枕稀。要把大象放冰箱,總共分幾步谜嫉,分三步萎坷。

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成箫。這樣循環(huán)下去就能夠將整條死路徹底封死。如下圖:

圖中紅色部分是一條死路旨枯,但我們遍歷一遍只能把最右邊的紅色位置寫入1蹬昌,此時死路的盡頭變成了中間這個紅色方塊。我們還需要繼續(xù)循環(huán)攀隔,但這種死路有幾個長度就循環(huán)幾次的方式顯然不是最優(yōu)解皂贩。因此我們要設計一個算法能夠一次就封死整個路徑栖榨。

代碼如下:

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()函數執(zhí)行完后,我們就得到了一個只有一條正確路線的迷宮键闺。

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,發(fā)現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);
}

看看最后的執(zhí)行結果:

7. 總結

這個算法是不是沒有用到任何大家沒學過的知識呢盅藻。先把我當代碼看懂购桑,之后考慮下面兩個問題:

  • 這個算法找到的路線是否一定是最短的路線呢?
  • 判斷每個位置是否在迷宮邊上非常麻煩氏淑,有沒有什么好辦法簡化這個操作呢勃蜘?

請大家仔細思考這兩個問題,我會在群里公布優(yōu)化后的代碼假残。

8. 課后作業(yè)

把一個硬幣拋5次缭贡,打印出所有可能出現的情況。1表示正面辉懒,0表示背面阳惹。比如:

全正面

1 1 1 1 1

全背面

0 0 0 0 0

我是天花板,讓我們一起在軟件開發(fā)中自我迭代眶俩。
如有任何問題莹汤,歡迎與我聯系。


上一篇:天花板編程手把手計劃-第1期-第2天

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末颠印,一起剝皮案震驚了整個濱河市纲岭,隨后出現的幾起案子抹竹,更是在濱河造成了極大的恐慌,老刑警劉巖止潮,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窃判,死亡現場離奇詭異,居然都是意外死亡喇闸,警方通過查閱死者的電腦和手機袄琳,發(fā)現死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仅偎,“玉大人跨蟹,你說我怎么就攤上這事¢倭ぃ” “怎么了窗轩?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長座咆。 經常有香客問我痢艺,道長,這世上最難降的妖魔是什么介陶? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任堤舒,我火速辦了婚禮,結果婚禮上哺呜,老公的妹妹穿的比我還像新娘舌缤。我一直安慰自己,他們只是感情好某残,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布国撵。 她就那樣靜靜地躺著,像睡著了一般玻墅。 火紅的嫁衣襯著肌膚如雪介牙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天澳厢,我揣著相機與錄音环础,去河邊找鬼。 笑死剩拢,一個胖子當著我的面吹牛线得,可吹牛的內容都是我干的。 我是一名探鬼主播徐伐,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼框都,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起魏保,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤熬尺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谓罗,有當地人在樹林里發(fā)現了一具尸體粱哼,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年檩咱,在試婚紗的時候發(fā)現自己被綠了揭措。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡刻蚯,死狀恐怖绊含,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情炊汹,我是刑警寧澤躬充,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站讨便,受9級特大地震影響充甚,放射性物質發(fā)生泄漏。R本人自食惡果不足惜霸褒,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一伴找、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧废菱,春花似錦技矮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至梳凛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間梳杏,已是汗流浹背韧拒。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留十性,地道東北人叛溢。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像劲适,于是被迫代替她去往敵國和親楷掉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容