<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一道題目帶你搞懂回溯算法

          共 2900字,需瀏覽 6分鐘

           ·

          2021-02-12 19:35

          明天就過年了,無論你有沒有回到家鄉(xiāng),都祝你新的一年,有所收獲。

          今天分享一道算法題,希望能讓你學(xué)會回溯算法的思路。

          學(xué)會了回溯,你就能解決著名的八皇后問題,數(shù)學(xué)家高斯窮其一生都沒有解出八皇后的解,而借助現(xiàn)代計(jì)算機(jī)和回溯算法,你分分鐘就搞定了,當(dāng)然,N 皇后也不在話下。

          回溯法(back tracking)是一種選優(yōu)搜素算法,又稱為試探法,按選優(yōu)條件向前搜索,以達(dá)到目標(biāo)。但當(dāng)探索到某一步時(shí),發(fā)現(xiàn)達(dá)不到目標(biāo),就退回一步重新選擇,這種走不通就退回再走的技術(shù)為回溯法,當(dāng)然,回溯也是暴力搜索法中的一種。

          昨天看到一道回溯算法題目,非常燒腦,不過我很喜歡這種感覺,程序員應(yīng)該定期刷一刷算法題,只有刷算法題目的時(shí)候,我才覺得那是真正意義上的編程,平時(shí)的工作在多數(shù)情況下,都是熟練調(diào)用編程語言或框架的 API 而已。

          這道題目是 leetcode 第 93 題,難度為中等,讓我們根據(jù)一個包含數(shù)字的字符串,復(fù)原它所有可能的 IP 地址。具體如下:

          給定一個只包含數(shù)字的字符串,復(fù)原它并返回所有可能的 IP 地址格式。

          有效的 IP 地址 正好由四個整數(shù)(每個整數(shù)位于 0 到 255 之間組成,且不能含有前導(dǎo) 0),整數(shù)之間用 '.' 分隔。

          例如:"0.1.2.201" 和 "192.168.1.1" 是有效的 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 是無效的 IP 地址。

          示例 1:

          輸入:s =?"25525511135"
          輸出:["255.255.11.135","255.255.111.35"]

          示例 2:

          輸入:s =?"0000"
          輸出:["0.0.0.0"]

          示例 3:

          輸入:s =?"1111"
          輸出:["1.1.1.1"]

          示例 4:

          輸入:s =?"010010"
          輸出:["0.10.0.10","0.100.1.0"]

          示例 5:

          輸入:s =?"101023"
          輸出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

          來源:力扣(LeetCode) 鏈接:https://leetcode-cn.com/problems/restore-ip-addresses

          暴力窮舉

          這個題目,我相信你大腦里最先想到的就是找三個點(diǎn)來分隔出 4 個字符串,然后判斷分隔出的 4 個字符串是否滿足 ip 某一段的要求,假如 4 個字符串都在 0 到 255 之間并且沒有前導(dǎo)的零,那就是一個合法的 ip 地址。

          但是三個點(diǎn)號的位置不太容易窮舉,4 個字符串的長度倒是好窮舉的,每個字符串的長度至少是 1,至多是 3,只有 3 種可能,,因此可以窮盡 4 個字符串的所有長度,也就是 3 的 4 次方 81 種可能。

          如果 4 個字符串的長度加起來等于給定字符串的長度時(shí),就可以按長度分隔,然后分別進(jìn)行判斷了。能想到這一點(diǎn),就不難寫出如下代碼:

          class?Solution(object):
          ????def?restoreIpAddresses(self,?s):
          ????????"""
          ????????:type?s:?str
          ????????:rtype:?List[str]
          ????????"""

          ????????result?=?[]
          ????????for?a?in?range(1,4):
          ????????????for?b?in?range(1,4):
          ????????????????for?c?in?range(1,4):
          ????????????????????for?d?in?range(1,4):
          ????????????????????????if?a+b+c+d?==?len(s):
          ????????????????????????????s1?=?s[0:a]
          ????????????????????????????s2?=?s[a:a+b]
          ????????????????????????????s3?=?s[a+b:a+b+c]
          ????????????????????????????s4?=?s[-d:]
          ????????????????????????????if?self.isValid(s1)?\
          ????????????????????????????and?self.isValid(s2)?\
          ????????????????????????????and?self.isValid(s3)?\
          ????????????????????????????and?self.isValid(s4):
          ????????????????????????????????result.append("{}.{}.{}.{}".format(s1,s2,s3,s4))
          ????????return?result;
          ????????????????????????????
          ????def?isValid(self,s_sub):
          ????????if?len(s_sub)?>?1?and?s_sub.startswith('0'):
          ????????????return?False
          ????????if?int(s_sub)?<=?255:?#全部都由數(shù)字組成
          ????????????return?True
          ????????return?False

          但這種方法非常易懂,但是卻不夠通用,無法舉一反三,比如說題目改成 ipv6 的地址,這種方法就不太合適了。

          回溯思想

          接下來我們嘗試一下回溯的思路。

          比如 25525511135,先來確定 ip 的第一段,第一段最多有 3 種可能:2,25,255,這里可以使用一個小循環(huán)。假如先選擇 2 做為 ip 的第一段,2 小于等于 255,滿足要求。

          接下來確定 ip 的第二段,也就是說對剩余的字符串 5525511135 重復(fù)上述操作,同樣的,最多有 3 種可能:5,55,525。假如這里選擇 5, 5 是小于等于 255 的,因此滿足條件。

          接下來確定 ip 的第三段。

          接下來確定 ip 的第四段。

          每一段的選擇,都是同樣的操作。這就很像是一個決策樹,每做一次選擇,都是沿著樹的某一分支走到葉子節(jié)點(diǎn)的過程,我這里使用腦圖來展示一下這個決策樹。

          上圖中除了葉子節(jié)點(diǎn),其他節(jié)點(diǎn)都是 3 個子節(jié)點(diǎn),某些我沒有畫出,希望不影響你理解。

          每一層的檢索都是一個遞歸操作,遞歸的退出條件就是到第 5 層結(jié)束,第 5 層結(jié)束后如果沒有剩余字符串,說明找到了一個正確的 IP 地址,保存到結(jié)果集里即可。

          不可避免地需要遍歷這棵決策樹的每個節(jié)點(diǎn),比如 2.5.5.2,本質(zhì)就是多叉樹的遍歷操作,這也就是回溯思想的核心。寫代碼時(shí)我們可以把多叉樹的遍歷骨架寫出來:


          def?backtrace(root?:str)?->?None:
          ????"""
          ????有 3 個子節(jié)點(diǎn)的多叉樹的中序遍歷。
          ????"""

          ????if?滿足退出條件:
          ????????if?滿足要求:
          ????????????加入結(jié)果集
          ????????退出

          ????for?i?in?range(0,3):
          ????????if?i?#索引不能超過字符串的長度
          ????????????#選擇?root[0:i+1]
          ????????????#具體做法就是?tmp_list.append(root[0:i+1])
          ????????????backtrace(root[i+1:])
          ????????????#撤銷選擇?root[0:i+1]?
          ????????????#具體做法就是?tmp_list.pop()

          進(jìn)入下一輪決策(遞歸)之前,先做選擇,把當(dāng)前 ip 段加入路徑 tmp_list 中,決策(遞歸)完成后,再撤銷選擇。

          這里有人可能不太理解,為什么需要撤銷選擇?其實(shí)不難理解,看上圖決策樹的最左邊的分支,當(dāng)遍歷到 2.5.5.2 發(fā)現(xiàn)不合適的時(shí)候,需要回溯到 2.5.5,然后選擇 25,也就是說最后的 2 加入 tmp_list 之后,判斷不合適,遞歸返回之后,我們需要把 2 刪除,然后騰出空間放 25,這也是為什么叫回溯算法的原因,遇到不符合目標(biāo)的,就回頭重新選擇。當(dāng)然了,遇到合適的,也要重新選擇,是因?yàn)槲覀円x出所有合法的 ip 地址。

          接下來,為這個骨架填充一點(diǎn)血肉。遍歷了每個節(jié)點(diǎn),需要把這些節(jié)點(diǎn)的順序保存下來,這里使用一個 tmp_list 來保存,為了編寫退出條件,還需要一個變量指示現(xiàn)在是第幾層,為了返回最終結(jié)果,再傳入一個 result 的數(shù)組來保存。

          def?backtrace(root?:str,?tmp_list:list,?levle:?int,?result:list?)?->?None:
          ????"""
          ????有 3 個子節(jié)點(diǎn)的多叉樹的中序遍歷。
          ????tmp_list?保存遍歷的路徑,比如?2.5.5.2
          ????level?表示現(xiàn)在是第幾層,初始調(diào)用時(shí)傳入?1
          ????"""

          ????##剩余字符串為空,或者遍歷到第 5 層,終止遞歸。
          ????if?len(root)?==?0?or?level?==?5:
          ????????##同時(shí)滿足時(shí),說明已經(jīng)找到了合法的ip
          ????????if?len(root)?==?0?and?level?==?5:
          ???????????result.append(".".join(tmp_list))?
          ????????return?

          ????for?i?in?range(0,3):
          ????????if?i?#索引不能超過字符串的長度
          ????????????#選擇?root[0:i+1]
          ????????????part?=?root[0:i+1]
          ????????????if?isValid(part):
          ????????????????#合法的部分,才去遞歸
          ????????????????#加入選擇
          ????????????????tmp_list.append(part)
          ????????????????backtrace(root[i+1:],tmp_list,level+1,result)
          ????????????????#撤銷選擇
          ????????????????tmp_list.pop()
          ????????????else:
          ????????????????pass

          組裝一下,以下是完整代碼,可直接在 leetcode 運(yùn)行的,提交后看看結(jié)果:

          class?Solution(object):

          ????def?restoreIpAddresses(self,?s):
          ????????"""
          ????????:type?s:?str
          ????????:rtype:?List[str]
          ????????"""

          ????????if?len(s)?4:
          ????????????return?[]
          ????????result?=?[]
          ????????tmp_list?=?[]
          ????????self.backtrace(s,tmp_list,1,result);
          ????????return?result

          ????def?backtrace(self,?root:str?,?tmp_list:list,?level:int,?result:list)?->?None:
          ????????"""
          ????????有 3 個子節(jié)點(diǎn)的多叉樹的中序遍歷。
          ????????tmp_list?保存遍歷的路徑,比如?2.5.5.2
          ????????level?表示現(xiàn)在是第幾層,初始調(diào)用時(shí)傳入?1
          ????????"""


          ????????if?len(root)?==?0?or?level?==?5:
          ????????????if?len(root)?==?0?and?level?==?5:
          ????????????????result.append(".".join(tmp_list))?
          ????????????return?

          ????????for?i?in?range(0,3):
          ????????????if?i?#索引不能超過字符串的長度
          ????????????????#選擇?root[0:i+1]
          ????????????????part?=?root[0:i+1]
          ????????????????if?self.isValid(part):
          ????????????????????#合法的部分,才去遞歸
          ????????????????????#加入選擇
          ????????????????????tmp_list.append(part)
          ????????????????????self.backtrace(root[i+1:],tmp_list,level+1,result)
          ????????????????????#撤銷選擇
          ????????????????????tmp_list.pop()
          ????????????????else:
          ????????????????????pass

          ????
          ????def?isValid(self,?sub_s?:?str)?->?bool:
          ????????if?len(sub_s)?>?1?and?sub_s.startswith('0'):
          ????????????return?False
          ????????if?0?<=?int(sub_s)?<=?255:
          ????????????return?True
          ????????return?False

          運(yùn)行結(jié)果如下:

          心心苦苦搞了半天,看來還沒有第一段暴力解法來得快,別灰心,一定有什么可以優(yōu)化的地方,其實(shí),只要有某一段 ip 的長度大于 1,且是 0 開頭的時(shí)候,后面就不需要向下遞歸了,可以提升點(diǎn)效率。

          比如:輸入:s = "010010",當(dāng) "01"做為第一段時(shí)就可以 break 跳出循環(huán)了。

          優(yōu)化一下 backtrace 函數(shù)和 isValid 函數(shù):

          ????def?backtrace(self,?root:str?,?tmp_list:list,?level:int,?result:list)?->?None:
          ????????"""
          ????????有 3 個子節(jié)點(diǎn)的多叉樹的中序遍歷。
          ????????tmp_list?保存遍歷的路徑,比如?2.5.5.2
          ????????level?表示現(xiàn)在是第幾層,初始調(diào)用時(shí)傳入?1
          ????????"""


          ????????if?len(root)?==?0?or?level?==?5:
          ????????????if?len(root)?==?0?and?level?==?5:
          ????????????????result.append(".".join(tmp_list))?
          ????????????return?

          ????????for?i?in?range(0,3):
          ????????????if?i?#索引不能超過字符串的長度
          ????????????????#選擇?root[0:i+1]
          ????????????????part?=?root[0:i+1]

          ????????????????##如果某段以0開頭,且長度超過?1?,那么跳出循環(huán),提升效率
          ????????????????if?part.startswith('0')?and?len(part)>1:
          ????????????????????break;

          ????????????????if?self.isValid(part):
          ????????????????????#合法的部分,才去遞歸
          ????????????????????#加入選擇
          ????????????????????tmp_list.append(part)
          ????????????????????self.backtrace(root[i+1:],tmp_list,level+1,result)
          ????????????????????#撤銷選擇
          ????????????????????tmp_list.pop()
          ????????????????else:
          ????????????????????pass

          ????
          ????def?isValid(self,?sub_s?:?str)?->?bool:
          ????????#?if?len(sub_s)?>?1?and?sub_s.startswith('0'):
          ????????#?????return?False
          ????????if?int(sub_s)?<=?255:
          ????????????return?True
          ????????return?False

          一個小小的優(yōu)化,再次提交看結(jié)果,確實(shí)提升了不少:

          由于 leetcode 同時(shí)有很多人使用,因此不同的時(shí)間提交,服務(wù)器的計(jì)算壓力是不同的,得出的結(jié)果會有少量的差異,這個理解就好。

          到這里不知道你是否理解了回溯算法的思路。如果有不理解的地方,請?jiān)谖哪┝粞越涣鳌?/p>

          最后的話

          其實(shí)不管多么復(fù)雜的算法,歸根結(jié)底都逃離不開最基本的循環(huán)語句、if、else 的組合,再高級一點(diǎn)的,就是與棧、隊(duì)列、遞歸的組合應(yīng)用。

          本文提到的回溯算法,本質(zhì)就是暴力遍歷多叉樹(本題是 3 叉樹)求解,先確定決策樹,寫出多叉樹的遍歷框架,然后填充內(nèi)容。不要忘記在遞歸完成后撤銷選擇。如果還有點(diǎn)不理解,這里我提個問題:

          請問二叉樹前、中、后序遍歷的區(qū)別是什么,你可能會說不就是訪問根節(jié)點(diǎn)的順序不同么,先訪問根節(jié)點(diǎn)就是前序遍歷....

          其實(shí)這樣的回答是錯的,無論哪一種遍歷,都是要先訪問根節(jié)點(diǎn)的,不訪問根節(jié)點(diǎn),你怎么可能訪問得到子節(jié)點(diǎn)?

          真正的區(qū)別在于對根節(jié)點(diǎn)的處理是放在進(jìn)入子節(jié)點(diǎn)的遞歸調(diào)用之前,還是在遞歸調(diào)用之后。前序遍歷的代碼在進(jìn)?某?個節(jié)點(diǎn)之前的那個時(shí)間點(diǎn)執(zhí)?,后序遍歷代碼在離開某個節(jié)點(diǎn)之后的那個時(shí)間點(diǎn)執(zhí)?,如下圖所示:

          def?trace(root):
          ????##前序
          ????trace(root.left)
          ????##中序
          ????trace(root.right)
          ????##后序

          因此后序遍歷之后,需要撤銷選擇的 child,加入新的 child 進(jìn)行遍歷。

          PS:如果你也在刷 Leetcode,我這里有一份從 Leetcode 中精選大概 200 左右的題目,去除了某些繁雜但是沒有多少算法思想的題目,同時(shí)保留了面試中經(jīng)常被問到的經(jīng)典題目,對本號發(fā)消息回復(fù)「算法」即可獲取,讓你更高效地刷力扣。

          如果覺得本文對你有用,請點(diǎn)贊在看轉(zhuǎn)發(fā)支持,感謝老鐵。

          留言討論


          瀏覽 73
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日日爽夜夜爽 | 高清无码做爱视频 | 天天插天天爽 | 国产色爱综合操网 | 亚洲影院之台湾 |