和同事談談Flood Fill 演算法

王子發表於2020-09-04

前言

今天忙完了公司的工作後,發現同事在做LeeCode的演算法題,頓時來了興趣,於是王子與同事一起探討如何能做好演算法題,今天在此文章中和大家分享一下。

什麼是Flood Fill 演算法

我們今天談論的是Flood Fill演算法,那麼什麼是Flood Fill演算法呢?

為了理解什麼是Flood Fill演算法,我們先拋開演算法本身的概念,王子給大家說一些平時工作生活中的場景。

1.畫圖工具的填充功能

相信大家都用過Windows的畫圖工具,我們看下圖

 

 

 

用顏料桶給一個圖形區域染色,這就是比較典型的Flood Fill 演算法的應用。

 

2.掃雷遊戲

 

 

 我們玩掃雷遊戲的時候,當我們選中一塊的時候,會向四周延展出一大片區域,那麼有沒有想過如果讓你來實現這個演算法,要怎麼實現呢?這也是Flood Fill 演算法的實際應用。

3.ps中的魔棒工具

 

 

 在我們使用魔棒工具摳圖的時候,原理其實也是一樣的,選中一處後,將相連的相似顏色的部分選中,只不過這裡面多了相似顏色的演算法。

 

好了,看完以上的場景,相信小夥伴們對Flood Fill 演算法應該有一個概念性的認識了,那麼再做這種題之前,我們先考慮一下要做出這種題的套路,也可以說是一種框架思維。

Flood Fill 演算法的解題框架

以上所有的例子都可以把它想成在一個圖上進行操作,而圖又可以叫做二維圖形,既然是二維圖形就可以給他加上座標軸,(x,y)來代表一個畫素點。

有了上邊的想法後,繼續思考,其實解決演算法無外乎就是遞迴、遍歷。

而這個問題就可以想象成一個4叉樹的遍歷問題,所以解題框架如下:

// (x, y) 為座標位置
void fill(int x, int y) {
    fill(x - 1, y); //
    fill(x + 1, y); //
    fill(x, y - 1); //
    fill(x, y + 1); //
}

這個框架其實很容易理解,對於四叉樹結構,或者說二維矩陣的結構,這個框架基本都可以解決,從選中的畫素點開始,向四周遞迴遍歷,獲得想要的結果。

有了框架的思維,那麼我們就去找一道真題來看看吧。

真題解析

今天我們選擇的真題是leecode上的733題,影像渲染,題目如下:

 

 

 下面我們就根據框架思維寫一下我們的解題步驟:

class Solution {
    public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
        // 當前顏色
        int origColor = image[sr][sc];
        fill(image,sr,sc,origColor,newColor);
        return image;
    }
    public void fill(int[][] image, int x, int y, int origColor, int newColor){
        // 判斷邊界超出陣列
        if(!inArea(image,x,y)){
            return;
        }
        // 判斷邊界遇到其他顏色
        if(image[x][y]!=origColor){
            return;
        }
        // 賦值新的顏色
        image[x][y]=newColor;

        // 引入框架
        fill(image,x-1,y,origColor,newColor);
        fill(image,x+1,y,origColor,newColor);
        fill(image,x,y-1,origColor,newColor);
        fill(image,x,y+1,origColor,newColor);
    }
    public Boolean inArea(int[][] image, int x, int y){
        return x>=0&&x<image.length&&y>=0&&y<image[0].length;
    }
}

上邊的註釋寫的已經跟完整了,希望小夥伴們動腦仔細看懂這段程式碼,看懂後你就明白瞭如何使用框架來解決問題了。

那麼上邊的就是正確答案嗎,其實這段程式碼是有問題的,就是如果origColor和newColor如果相同的話,就會導致陷入無限遞迴。

那麼如何解決呢?

最終答案

我們再次閱讀上邊的程式碼,可以知道出現無限遞迴的原因是每個座標都要搜尋上下左右,那麼對於一個座標,一定也會被上下左右的座標多次重複搜尋。所以我們必須保證重複搜尋時能正確退出遞迴

其實針對於上邊提到的這種情況,我們可以這樣想,如果我們將元素的搜尋路徑記錄下來,每次搜尋時如果發現之前已經走過了這條路,那麼就return,這樣不就行了嗎。

於是有了下邊的最終答案

class Solution {
    public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
        // 當前顏色
        int origColor = image[sr][sc];
        fill(image,sr,sc,origColor,newColor);
        return image;
    }
    public void fill(int[][] image, int x, int y, int origColor, int newColor){
        // 判斷邊界超出陣列
        if(!inArea(image,x,y)){
            return;
        }
        // 判斷邊界遇到其他顏色
        if(image[x][y]!=origColor){
            return;
        }
        // 已經探索的origColor區域
        if(image[x][y]==-1){
            return;
        }
    
        // 賦值新的顏色之前先打一個標記,證明已經探索過
        image[x][y]=-1;
        // 引入框架
        fill(image,x-1,y,origColor,newColor);
        fill(image,x+1,y,origColor,newColor);
        fill(image,x,y-1,origColor,newColor);
        fill(image,x,y+1,origColor,newColor);
        // 全部遞迴後,再把標記賦值成新的顏色
        image[x][y]=newColor;
    }
    public Boolean inArea(int[][] image, int x, int y){
        return x>=0&&x<image.length&&y>=0&&y<image[0].length;
    }
}

為什麼要用-1做標記呢,其實這個無所謂,只要是一個題目中不會使用到的值就可以了(題目中規定值在0-65535之間)

說明一下,這種思路其實就是回溯演算法

 

擴充套件演算法

小夥伴們,上邊的演算法題如果已經弄明白了,那我們想一想,如果要實現PS的魔棒工具要怎麼實現呢,我們來分析一下。

魔棒工具和上邊的演算法其實原理是一樣的,只不過有兩點不同,一是選擇區域時選擇的不是相同顏色,而是相似顏色,在ps中是可以根據閾值來設定相似程度的;二是使用魔棒選擇區域後,在邊界上會有邊框,說明選中了哪些地方,這就變成了填充邊界問題。

對於第一個問題很好解決,只要做如下改動即可

        //// 判斷邊界遇到其他顏色
        if(image[x][y]!=origColor){
            return;
        }

        //// 遇到其他不在閾值內的顏色 threshold代表閾值
        if (Math.abs(image[x][y] - origColor) > threshold){
            return; 
        }
        

 

對於第二個問題,首先明確,我們要染色的部分只是邊界,而不是內部區域,思考一下,內部區域的每個畫素點的四周都是相同的顏色,而臨近邊界的畫素點至少有一個方向的顏色與其不同,這就是解決問題的方案。這次我們使用visited陣列來儲存搜尋路徑,程式碼如下

class Solution {
    boolean[][] visited = null;
    public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
        // 當前顏色
        int origColor = image[sr][sc];
        // 初始化訪問路徑陣列
        visited = new boolean[image.length][image[0].length];
        fill(image,sr,sc,origColor,newColor);
        return image;
    }
    public int fill(int[][] image, int x, int y, int origColor, int newColor){
        // 判斷邊界超出陣列
        if(!inArea(image,x,y)){
            return 0;
        }
        // 判斷邊界遇到其他顏色
        if(image[x][y]!=origColor){
            return 0;
        }
        // 已經探索的origColor區域
        if(visited[x][y]){
            return 1;
        }
    
        // 賦值新的顏色之前先打一個標記,證明已經探索過
        visited[x][y]=true;
        // 引入框架
        int result=
            fill(image,x-1,y,origColor,newColor)+
            fill(image,x+1,y,origColor,newColor)+
            fill(image,x,y-1,origColor,newColor)+
            fill(image,x,y+1,origColor,newColor);
        // 全部遞迴後,如果result<4代表畫素點是邊界,賦值顏色
        if(result<4){
            image[x][y]=newColor;
        }

        return 1;
    }
    public Boolean inArea(int[][] image, int x, int y){
        return x>=0&&x<image.length&&y>=0&&y<image[0].length;
    }
}

 

總結

以上詳解了Flood Fill 演算法解決的題目,其實掌握了這種框架思維,類似的題目都可以想出解決思路。

我們可以這麼認為,做演算法題就是要找到做題的套路,就像我們上學時做數學題,在其中不也是有些固定的套路嗎。

leecode上邊的1034題也是類似的思路,小夥伴們可以自己去試試怎麼解決,歡迎留言一起討論。

之後的文章,我也會不定期的分享一些有關演算法刷題的解題方案,和小夥伴們一起研究解題套路,畢竟有些公司還是會考一些演算法的嘛。

好了本文就到這裡,歡迎大家持續關注!

 

 

往期文章推薦:

中介軟體專輯:

什麼是訊息中介軟體?主要作用是什麼?

常見的訊息中介軟體有哪些?你們是怎麼進行技術選型的?

你懂RocketMQ 的架構原理嗎?

聊一聊RocketMQ的註冊中心NameServer

相關文章