前言
今天忙完了公司的工作後,發現同事在做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題也是類似的思路,小夥伴們可以自己去試試怎麼解決,歡迎留言一起討論。
之後的文章,我也會不定期的分享一些有關演算法刷題的解題方案,和小夥伴們一起研究解題套路,畢竟有些公司還是會考一些演算法的嘛。
好了本文就到這裡,歡迎大家持續關注!
往期文章推薦:
中介軟體專輯: