1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        ?LeetCode刷題實(shí)戰(zhàn)45:跳躍游戲 II

        共 5922字,需瀏覽 12分鐘

         ·

        2020-09-24 07:55

        算法的重要性,我就不多說了吧,想去大廠,就必須要經(jīng)過基礎(chǔ)知識(shí)和業(yè)務(wù)邏輯面試+算法面試。所以,為了提高大家的算法能力,這個(gè)公眾號(hào)后續(xù)每天帶大家做一道算法題,題目就從LeetCode上面選 !

        今天和大家聊的問題叫做?跳躍游戲 II,我們先來(lái)看題面:

        https://leetcode-cn.com/problems/jump-game-ii/

        Given an array of non-negative integers, you are initially positioned at the first index of the array.


        Each element in the array represents your maximum jump length at that position.


        Your goal is to reach the last index in the minimum number of jumps.

        題意


        給定一個(gè)非負(fù)整數(shù)數(shù)組,你最初位于數(shù)組的第一個(gè)位置。

        數(shù)組中的每個(gè)元素代表你在該位置可以跳躍的最大長(zhǎng)度。

        你的目標(biāo)是使用最少的跳躍次數(shù)到達(dá)數(shù)組的最后一個(gè)位置。

        樣例

        輸入: [2,3,1,1,4]
        輸出: 2
        解釋: 跳到最后一個(gè)位置的最小跳躍數(shù)是 2。從下標(biāo)為 0?跳到下標(biāo)為 1?的位置,跳?1?步,然后跳?3?步到達(dá)數(shù)組的最后一個(gè)位置。

        說明:
        假設(shè)你總是可以到達(dá)數(shù)組的最后一個(gè)位置。


        解題

        來(lái)源:
        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è)例子:
        [3, 1, 5, 1, 4, 2]
        如果我們從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)遍歷過了。
        這個(gè)時(shí)候代碼如下:
        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é)束了。
        如果還不明白,我們來(lái)看下下面這張圖:
        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ù)以上過程迭代就行了。
        這個(gè)思路理解了之后,代碼就很好寫了:
        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)力。


        上期推文:


        LeetCode1-20題匯總,速度收藏!
        LeetCode21-40題匯總,速度收藏!
        LeetCode刷題實(shí)戰(zhàn)40:組合總和 II
        LeetCode刷題實(shí)戰(zhàn)41:缺失的第一個(gè)正數(shù)
        LeetCode刷題實(shí)戰(zhàn)42:接雨水
        LeetCode刷題實(shí)戰(zhàn)43:字符串相乘
        LeetCode刷題實(shí)戰(zhàn)44:通配符匹配


        瀏覽 43
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            姐姐说家里没人可以让孩子成长 | 男女操逼网站 | 免费无码真人在线 | 欧美成人无码片免费看A片秀色 | 巨胸喷奶水视频www免费软件 | 暴操网站 | 国产清纯白嫩高中生在线播放 | 一区二区乱伦视频 | 中文字幕国产在线 | 小嫩苞一区二区三区 |