樣例 輸入: [2,3,1,1,4] 輸出: 2 解釋: 跳到最后一個(gè)位置的最小跳躍數(shù)是 2 。從下標(biāo)為 0 ?跳到下標(biāo)為 1 ?的位置,跳?1 ?步,然后跳?3 ?步到達(dá)數(shù)組的最后一個(gè)位置。 說明: 假設(shè)你總是可以到達(dá)數(shù)組的最后一個(gè)位置。
解題 https://www.cnblogs.com/techflow/p/12596586.html 今天這題的題目蠻有意思,它是說給定我們一個(gè)非負(fù)整數(shù) 的數(shù)組。讓我們把這個(gè)數(shù)組想象成一個(gè)大富翁里的那種長(zhǎng)條形的地圖。數(shù)組當(dāng)中的數(shù)字表示這個(gè)位置向前最多能前進(jìn)的距離?,F(xiàn)在我們從數(shù)組0號(hào)位置開始移動(dòng),請(qǐng)問至少需要移動(dòng)多少步可以走到數(shù)組的結(jié)尾? 搜索 我拿到題目的第一反應(yīng)就是搜索,因?yàn)楦杏X貪心是不可以的。我們把數(shù)組當(dāng)中每個(gè)位置的數(shù)字稱為前進(jìn)能力,我們當(dāng)下能達(dá)到的最遠(yuǎn)的位置前進(jìn)能力可能很差,所以貪心能夠達(dá)到最遠(yuǎn)的位置并不可行,舉個(gè)例子: 如果我們從0開始的時(shí)候走到3的話,由于3的前進(jìn)能力很小 ,所以我們需要3步才能走完數(shù)組。但是如果我們一開始不走滿3,而是走到2的話,我們只需要兩步就可以完成。所以貪心是有反例的,我們不能簡(jiǎn)單地來(lái)貪心。而且這題的狀態(tài)轉(zhuǎn)移十分明顯,幾乎是裸的順推。那么我們只需要搜就完事了,由于這是一個(gè)求解最優(yōu)的問題 ,所以我們應(yīng)該使用寬度優(yōu)先搜索。 這個(gè)代碼我想應(yīng)該很好寫,我們信手拈來(lái): class Solution : def jump (self, nums: List[int ]) -> int : import queue n = len (nums) que = queue.Queue() que.put((0 , 0 )) while not que.empty (): pos, step = que.get () if pos >= n-1 : return step for i in range(pos, min(n, pos+nums[pos] + 1 )): que.put((i, step +1 ))但是顯然這么交上去是一定會(huì)gg的,想想也知道,我們遍歷轉(zhuǎn)移狀態(tài)的這個(gè)for-loop看起來(lái)就很恐怖,數(shù)組當(dāng)中的狀態(tài)很有可能出現(xiàn)重復(fù) ,那么必然會(huì)出現(xiàn)大量的冗余。所以我們需要加上一些剪枝 ,由于我們使用的是寬度優(yōu)先搜索,所以所有狀態(tài)第一次在隊(duì)列當(dāng)中彈出的時(shí)候就是最優(yōu)解,不可能同樣的位置,我多走幾步會(huì)達(dá)到更優(yōu)的結(jié)果,所以我們可以放心地把之前出現(xiàn)過的位置全部標(biāo)記起來(lái),阻止重復(fù)遍歷: class Solution : def jump (self, nums: List[int ]) -> int : import queue n = len (nums) que = queue.Queue() que.put((0 , 0 )) visited = set () while not que.empty (): pos, step = que.get () if pos >= n-1 : return step for i in range(pos, min(n, pos+nums[pos] + 1 )): # 如果已經(jīng)入過隊(duì)列了則跳過 if i in visited: continue que.put((i, step +1 )) visited.add(i)很遺憾,雖然我們加上了優(yōu)化,但是還是會(huì)被卡掉 。所以還需要繼續(xù)優(yōu)化,我們來(lái)分析一下會(huì)超時(shí)的原因很簡(jiǎn)單,雖然我們通過標(biāo)記排除了重復(fù)進(jìn)入隊(duì)列的情況。但是for循環(huán)本身的計(jì)算量可能就很大 ,尤其在數(shù)組當(dāng)中存在大量前進(jìn)能力很大的位置的時(shí)候。舉個(gè)例子,比如我們超時(shí)的樣例: [25000,24999,24998,24997,24996,24995,24994...] 可以看到,這個(gè)數(shù)組的前進(jìn)能力都很大,我們會(huì)大量地重復(fù)遍歷 ,這個(gè)才是計(jì)算量的根源。所以我們要避免循環(huán)重復(fù)的部分,有辦法解決嗎? 當(dāng)然是有的,我們來(lái)分析一下問題,對(duì)于某一個(gè)位置x而言,它的前進(jìn)能力是m。那么它可以達(dá)到的最遠(yuǎn)距離是x + m,這是顯然的,但是很有可能從x到x+m的區(qū)間當(dāng)中已經(jīng)有一部分被加入隊(duì)列了。所以當(dāng)我們從x向x+m遍歷的時(shí)候,必然會(huì)重復(fù)遍歷一部分已經(jīng)在隊(duì)列當(dāng)中的狀態(tài)。那怎么解決呢? 其實(shí)很簡(jiǎn)單,我們只需要把遍歷的順序倒過來(lái) 就好了。也就是說我們從x+m向x反向遍歷,當(dāng)我們遇到一個(gè)狀態(tài)已經(jīng)在隊(duì)列當(dāng)中的時(shí)候,就可以break了,沒必要繼續(xù)往下了。因?yàn)楹竺娴臓顟B(tài)肯定已經(jīng)遍歷過了。 class Solution : def jump (self, nums: List[int ]) -> int : import queue n = len (nums) que = queue.Queue() que.put((0 , 0 )) visited = set () while not que.empty (): pos, step = que.get () if pos >= n-1 : return step # 倒敘遍歷 for i in range(min(n-1 , pos+nums[pos]), pos, -1 ): # 當(dāng)遇到已經(jīng)遍歷過的元素的時(shí)候直接break if i in visited: break que.put((i, step +1 )) visited.add(i)除了上面的方法之外,我們還可以想到一種優(yōu)化,我們可以使用優(yōu)先隊(duì)列對(duì)隊(duì)列當(dāng)中的元素進(jìn)行排列 ,將潛力比較大的元素排在前面,而將潛力差的排在后面。但是你會(huì)發(fā)現(xiàn)如果我們把前進(jìn)能力當(dāng)做是潛力或者是所處的位置當(dāng)做潛力都有反例,因?yàn)槲恢每壳暗目赡芮斑M(jìn)能力很差,但是前進(jìn)能力比較好的,又可能位置靠后。有沒有兩全其美的辦法呢? 當(dāng)然是有的,方法也很簡(jiǎn)單,我們把兩者相加,也就是位置加上它的前進(jìn)能力當(dāng)做這個(gè)位置的潛力 。也可以認(rèn)為是最遠(yuǎn)能夠移動(dòng)到的位置當(dāng)做是潛力,這樣我們每次都挑選出其中潛力最好的進(jìn)行迭代,從而保證我們可以最快地找到答案。 class Solution : def jump (self, nums: List[int ]) -> int : import queue n = len (nums) # 使用優(yōu)先隊(duì)列 que = queue.PriorityQueue() que.put((0 , 0 , 0 )) visited = set () while not que.empty (): _, pos, step = que.get () if pos >= n-1 : return step # 倒敘遍歷 for i in range(min(n-1 , pos+nums[pos]), pos, -1 ): if i in visited: break # 由于優(yōu)先隊(duì)列是默認(rèn)元素小的排在前面,所以加上負(fù)號(hào) que.put((-i - nums[i] ,i, step +1 )) visited.add(i)這種方法也是可以AC的,耗時(shí)比上一種方法略小。 貪心 不知道大家在寫完上面這串代碼之后有什么感覺,我最大的感覺不是成就感,而是覺得奇怪,就好像總覺得有哪里不太對(duì)勁 ,但是又不知道到底是哪里不對(duì)。 后來(lái)我想了很久,終于想明白了。不對(duì)的地方在于既然我們已經(jīng)想到了這么具體的策略來(lái)優(yōu)化搜索,我們?yōu)槭裁催€要用搜索呢?因?yàn)槲覀儧]必要維護(hù)狀態(tài)了,直接貪心不行嗎? 在正常的bfs搜索當(dāng)中,我們是一層一層地遍歷狀態(tài)的,每次遍歷的都是搜索樹上同樣深度的節(jié)點(diǎn) 。只有某一個(gè)深度的節(jié)點(diǎn)都遍歷結(jié)束了,我們才會(huì)遍歷下一個(gè)深度的節(jié)點(diǎn)。但是現(xiàn)在使用了優(yōu)先隊(duì)列之后,我們打破了這個(gè)限制 ,也就是說我們拿到的狀態(tài)根本不知道是來(lái)源于哪一個(gè)深度的。而從這個(gè)題目的題意來(lái)看,潛力大的排在前面,會(huì)使得一開始潛力小的狀態(tài)一直得不到迭代,沉積在隊(duì)列的底部。 既然如此,我們?yōu)槭裁催€要用隊(duì)列來(lái)存儲(chǔ)呢,直接維護(hù)最大的潛力值不就可以了? 解釋一下上面這段話的意思,在當(dāng)前問題當(dāng)中,由于我們可以走的距離是連續(xù)的。我們可以維護(hù)一個(gè)當(dāng)前步數(shù)能夠移動(dòng)的范圍 ,舉個(gè)例子,比如nums[0]=10,也就是說從0開始,一直到10的區(qū)間都是我們可以移動(dòng)的。對(duì)于這個(gè)區(qū)間里的每一個(gè)x來(lái)說,它可以移動(dòng)的范圍就是[x, x+nums[x]]。所以我們可以得到x+nums[x]的集合,這里面最大的那個(gè),就是下一步我們能夠移動(dòng)的范圍 。也就是說第二步的移動(dòng)范圍就是[11, max(x+nums[x])]。我們不停地迭代,當(dāng)能夠達(dá)到的最遠(yuǎn)位置大于或等于數(shù)組長(zhǎng)度的時(shí)候,就表示遍歷結(jié)束了。 rangeI表示第一步能夠移動(dòng)到的范圍,顯然由于我們起始位置是0,所以這個(gè)范圍就是[0, nums[0]]。等于rangeI當(dāng)中的每一個(gè)位置都有一個(gè)潛力值,其實(shí)就是它能達(dá)到的最遠(yuǎn)的距離。對(duì)于rangeI當(dāng)中的每一個(gè)位置的潛力值而言,它們顯然有一個(gè)最大值,我們假設(shè)最大值的下標(biāo)是x,它的潛力值就是x+nums[x] 。那么我們就可以得到rangeII的范圍是[nums[0]+1, x+nums[x]]。我們只需要在遍歷rangeI的時(shí)候記錄下這個(gè)x 就可以得到rangeII的范圍,我們重復(fù)以上過程迭代就行了。 class Solution : def jump (self, nums: List[int ]) -> int : n = len (nums) start, end = 0 , nums[0 ] step = 1 if n == 1 : return 0 while end < n-1 : maxi, idx = 0 , 0 # 維護(hù)下一個(gè)區(qū)間 for i in range(start, end +1 ): if i + nums[i] > maxi: maxi, idx = i + nums[i], i # 下一個(gè)區(qū)間的起始范圍 start, end = end +1 , maxi step += 1 return step 只有短短十來(lái)行,我們就解出了一個(gè)LeetCode當(dāng)中的難題。一般來(lái)說都是我們先試著用貪心,然后發(fā)現(xiàn)不行,再換算法用搜索,而這道題剛好相反,我們是先想到搜索的解法,然后一點(diǎn)一點(diǎn)推導(dǎo)出了貪心 。我想如果你能把上面思路推導(dǎo)的過程全部理解清楚,一定可以對(duì)這兩種算法都有更深的感悟。當(dāng)然,也有些大神是可以直接想到最優(yōu)解的,如果做不到也沒什么好遺憾的,只要我們勤于思考,多多理解,遲早有一天,這些問題對(duì)我們來(lái)說也不會(huì)是難事。 好了,今天的文章就到這里,如果覺得有所收獲,請(qǐng)順手點(diǎn)個(gè)在看 或者轉(zhuǎn)發(fā)吧,你們的支持是我最大的動(dòng)力。