LeetCode45——从搜索算法推导到贪心
本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是LeetCode系列的第25篇文章,今天我们一起来看的是LeetCode的第45题,Jump Game II。
有同学后台留言问我说,我每次写文章的题目是怎么选的,很简单基本上是按照顺序选择Medium和Hard难度,然后会根据题目内容以及评价过滤掉一些不太靠谱或者是比较变态没有意思的题。
这些题当然会比Easy难度的要难上一点,但是并不是高不可攀的。至少如果你熟悉编程语言,然后会一点基础算法的话,就可以尝试了,我个人觉得不是很高的门槛。
好了,我们回到正题。
今天这题的题目蛮有意思,它是说给定我们一个非负整数的数组。让我们把这个数组想象成一个大富翁里的那种长条形的地图。数组当中的数字表示这个位置向前最多能前进的距离。现在我们从数组0号位置开始移动,请问至少需要移动多少步可以走到数组的结尾?
搜索
我拿到题目的第一反应就是搜索,因为感觉贪心是不可以的。我们把数组当中每个位置的数字称为前进能力,我们当下能达到的最远的位置前进能力可能很差,所以贪心能够达到最远的位置并不可行,举个例子:
[3, 1, 5, 1, 4, 2]
如果我们从0开始的时候走到3的话,由于3的前进能力很小,所以我们需要3步才能走完数组。但是如果我们一开始不走满3,而是走到2的话,我们只需要两步就可以完成。所以贪心是有反例的,我们不能简单地来贪心。而且这题的状态转移十分明显,几乎是裸的顺推。那么我们只需要搜就完事了,由于这是一个求解最优的问题,所以我们应该使用宽度优先搜索。
这个代码我想应该很好写,我们信手拈来:
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))
但是显然这么交上去是一定会gg的,想想也知道,我们遍历转移状态的这个for-loop看起来就很恐怖,数组当中的状态很有可能出现重复,那么必然会出现大量的冗余。所以我们需要加上一些剪枝,由于我们使用的是宽度优先搜索,所以所有状态第一次在队列当中弹出的时候就是最优解,不可能同样的位置,我多走几步会达到更优的结果,所以我们可以放心地把之前出现过的位置全部标记起来,阻止重复遍历:
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)):
# 如果已经入过队列了则跳过
if i in visited:
continue
que.put((i, step+1))
visited.add(i)
很遗憾,虽然我们加上了优化,但是还是会被卡掉。所以还需要继续优化,我们来分析一下会超时的原因很简单,虽然我们通过标记排除了重复进入队列的情况。但是for循环本身的计算量可能就很大,尤其在数组当中存在大量前进能力很大的位置的时候。举个例子,比如我们超时的样例:
[25000,24999,24998,24997,24996,24995,24994...]
可以看到,这个数组的前进能力都很大,我们会大量地重复遍历,这个才是计算量的根源。所以我们要避免循环重复的部分,有办法解决吗?
当然是有的,我们来分析一下问题,对于某一个位置x而言,它的前进能力是m。那么它可以达到的最远距离是x + m,这是显然的,但是很有可能从x到x+m的区间当中已经有一部分被加入队列了。所以当我们从x向x+m遍历的时候,必然会重复遍历一部分已经在队列当中的状态。那怎么解决呢?
其实很简单,我们只需要把遍历的顺序倒过来就好了。也就是说我们从x+m向x反向遍历,当我们遇到一个状态已经在队列当中的时候,就可以break了,没必要继续往下了。因为后面的状态肯定已经遍历过了。
这个时候代码如下:
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):
# 当遇到已经遍历过的元素的时候直接break
if i in visited:
break
que.put((i, step+1))
visited.add(i)
除了上面的方法之外,我们还可以想到一种优化,我们可以使用优先队列对队列当中的元素进行排列,将潜力比较大的元素排在前面,而将潜力差的排在后面。但是你会发现如果我们把前进能力当做是潜力或者是所处的位置当做潜力都有反例,因为位置靠前的可能前进能力很差,但是前进能力比较好的,又可能位置靠后。有没有两全其美的办法呢?
当然是有的,方法也很简单,我们把两者相加,也就是位置加上它的前进能力当做这个位置的潜力。也可以认为是最远能够移动到的位置当做是潜力,这样我们每次都挑选出其中潜力最好的进行迭代,从而保证我们可以最快地找到答案。
class Solution:
def jump(self, nums: List[int]) -> int:
import queue
n = len(nums)
# 使用优先队列
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
# 由于优先队列是默认元素小的排在前面,所以加上负号
que.put((-i - nums[i] ,i, step+1))
visited.add(i)
这种方法也是可以AC的,耗时比上一种方法略小。
贪心
不知道大家在写完上面这串代码之后有什么感觉,我最大的感觉不是成就感,而是觉得奇怪,就好像总觉得有哪里不太对劲,但是又不知道到底是哪里不对。
后来我想了很久,终于想明白了。不对的地方在于既然我们已经想到了这么具体的策略来优化搜索,我们为什么还要用搜索呢?因为我们没必要维护状态了,直接贪心不行吗?
在正常的bfs搜索当中,我们是一层一层地遍历状态的,每次遍历的都是搜索树上同样深度的节点。只有某一个深度的节点都遍历结束了,我们才会遍历下一个深度的节点。但是现在使用了优先队列之后,我们打破了这个限制,也就是说我们拿到的状态根本不知道是来源于哪一个深度的。而从这个题目的题意来看,潜力大的排在前面,会使得一开始潜力小的状态一直得不到迭代,沉积在队列的底部。
既然如此,我们为什么还要用队列来存储呢,直接维护最大的潜力值不就可以了?
解释一下上面这段话的意思,在当前问题当中,由于我们可以走的距离是连续的。我们可以维护一个当前步数能够移动的范围,举个例子,比如nums[0]=10,也就是说从0开始,一直到10的区间都是我们可以移动的。对于这个区间里的每一个x来说,它可以移动的范围就是[x, x+nums[x]]。所以我们可以得到x+nums[x]的集合,这里面最大的那个,就是下一步我们能够移动的范围。也就是说第二步的移动范围就是[11, max(x+nums[x])]。我们不停地迭代,当能够达到的最远位置大于或等于数组长度的时候,就表示遍历结束了。
如果还不明白,我们来看下下面这张图:
rangeI表示第一步能够移动到的范围,显然由于我们起始位置是0,所以这个范围就是[0, nums[0]]。等于rangeI当中的每一个位置都有一个潜力值,其实就是它能达到的最远的距离。对于rangeI当中的每一个位置的潜力值而言,它们显然有一个最大值,我们假设最大值的下标是x,它的潜力值就是x+nums[x]。那么我们就可以得到rangeII的范围是[nums[0]+1, x+nums[x]]。我们只需要在遍历rangeI的时候记录下这个x就可以得到rangeII的范围,我们重复以上过程迭代就行了。
这个思路理解了之后,代码就很好写了:
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
# 维护下一个区间
for i in range(start, end+1):
if i + nums[i] > maxi:
maxi, idx = i + nums[i], i
# 下一个区间的起始范围
start, end = end+1, maxi
step += 1
return step
只有短短十来行,我们就解出了一个LeetCode当中的难题。一般来说都是我们先试着用贪心,然后发现不行,再换算法用搜索,而这道题刚好相反,我们是先想到搜索的解法,然后一点一点推导出了贪心。我想如果你能把上面思路推导的过程全部理解清楚,一定可以对这两种算法都有更深的感悟。当然,也有些大神是可以直接想到最优解的,如果做不到也没什么好遗憾的,只要我们勤于思考,多多理解,迟早有一天,这些问题对我们来说也不会是难事。
今天的文章就是这些,如果觉得有所收获,请顺手点个关注或者转发吧,你们的举手之劳对我来说很重要。
LeetCode45——从搜索算法推导到贪心的更多相关文章
- $HNOI\ 2010$ 解题报告
HNOI 2010 解题报告 0. HNOI2010 AC代码包下载地址 注: 戳上面的标题中的'地址' 下载 代码包, 戳下面每一题的文件名 可进入 题目链接. 每一题 对应代码的文件名 我在 每一 ...
- Scratch不仅适合小朋友,程序员和大学老师都应该广泛使用!!!
去年接触到了Scratch这个编程工具,它是一种简易图形化编程工具,这个软件的开发团队来自于麻省理工大学称为“终身幼儿园团队”(Lifelong Kindergarten Group). 网址http ...
- FJoi2017 1月20日模拟赛 直线斯坦纳树(暴力+最小生成树+骗分+人工构造+随机乱搞)
[题目描述] 给定二维平面上n个整点,求该图的一个直线斯坦纳树,使得树的边长度总和尽量小. 直线斯坦纳树:使所有给定的点连通的树,所有边必须平行于坐标轴,允许在给定点外增加额外的中间节点. 如下图所示 ...
- 【推导】【贪心】Codeforces Round #472 (rated, Div. 2, based on VK Cup 2018 Round 2) D. Riverside Curio
题意:海平面每天高度会变化,一个人会在每天海平面的位置刻下一道痕迹(如果当前位置没有已经刻划过的痕迹),并且记录下当天比海平面高的痕迹有多少条,记为a[i].让你最小化每天比海平面低的痕迹条数之和. ...
- 【推导】【贪心】XVII Open Cup named after E.V. Pankratiev Stage 4: Grand Prix of SPb, Sunday, Octorber 9, 2016 Problem H. Path or Coloring
题意:给你一张简单无向图(但可能不连通),再给你一个K,让你求解任意一个问题:K染色或者输出一条K长路径. 直接贪心染色,对一个点染上其相邻的点的颜色集合之中,未出现过的最小的颜色. 如果染成就染成了 ...
- 【推导】【贪心】XVII Open Cup named after E.V. Pankratiev Stage 14, Grand Prix of Tatarstan, Sunday, April 2, 2017 Problem D. Clones and Treasures
给你一行房间,有的是隐身药水,有的是守卫,有的是金币. 你可以任选起点,向右走,每经过一个药水或金币就拿走,每经过一个守卫必须消耗1个药水,问你最多得几个金币. 药水看成左括号,守卫看成右括号, 就从 ...
- 【推导】【贪心】Codeforces Round #402 (Div. 2) E. Bitwise Formula
按位考虑,每个变量最终的赋值要么是必为0,要么必为1,要么和所选定的数相同,记为2,要么和所选定的数相反,记为3,一共就这四种情况. 可以预处理出来一个真值表,然后从前往后推导出每个变量的赋值. 然后 ...
- BZOJ 3119 Book (贪心+数学推导)
手动博客搬家: 本文发表于20191029 22:49:41, 原地址https://blog.csdn.net/suncongbo/article/details/78388925 URL: htt ...
- AI广度优先搜索算法,项目实战北京地图/贪心学院
广度优先搜索算法详解地铁路线 北京很大,附上地铁图,不要迷路!!! 作为一个程序员,在北京,你很有可能住在回龙观地区,经常从龙泽上地铁,然后畅游北京. 当有一天,你老家的朋友来北京了,希望你能够带她去 ...
随机推荐
- [Golang]字符串拼接方式的性能分析
本文100%由本人(Haoxiang Ma)原创,如需转载请注明出处. 本文写于2019/02/16,基于Go 1.11.至于其他版本的Go SDK,如有出入请自行查阅其他资料. Overview 写 ...
- LeetCode~报数(简单)
报数(简单) 题目描述: 报数序列是一个整数序列,按照其中的整数的顺序进行报数,得到下一个数.其前五项如下: 1 11 21 1211 111221 1 被读作 "one 1" ( ...
- 记一个 Base64 有关的 Bug
本文原计划写两部分内容,第一是记录最近遇到的与 Base64 有关的 Bug,第二是 Base64 编码的原理详解.结果写了一半发现,诶?不复杂的一个事儿怎么也要讲这么长?不利于阅读和理解啊(其实是今 ...
- Day 1 模拟
1. P1088 火星人 利用STL中的next_permutation();函数求一种排列的下一种排列,循环m次即为答案.(STL大法好~~C++是世界上最好的语言~~逃 #include < ...
- VUE实现Studio管理后台(二):Slot实现选项卡tab切换效果,可自由填装内容
作为RXEditor的主界面,Studio UI要使用大量的选项卡TAB切换,我梦想的TAB切换是可以自由填充内容的.可惜自己不会实现,只好在网上搜索一下,就跟现在你做的一样,看看有没有好事者实现了类 ...
- 在windows上极简安装GPU版AI框架(Tensorflow、Pytorch)
在windows上极简安装GPU版AI框架 如果我们想在windows系统上安装GPU版本的AI框架,比如GPU版本的tesnorflow,通常我们会看到类似下面的安装教程 官方版本 安装CUDA 安 ...
- 【Spring Data 系列学习】Spring Data JPA @Query 注解查询
[Spring Data 系列学习]Spring Data JPA @Query 注解查询 前面的章节讲述了 Spring Data Jpa 通过声明式对数据库进行操作,上手速度快简单易操作.但同时 ...
- java原子操作CAS
本次内容主要讲原子操作的概念.原子操作的实现方式.CAS的使用.原理.3大问题及其解决方案,最后还讲到了JDK中经常使用到的原子操作类. 1.什么是原子操作? 所谓原子操作是指不会被线程调度机制打断的 ...
- C++ 标准模板库(STL)-string
总结了一些c++ string库常用的库函数用法 #include <iostream> #include <string>//string类可以自动管理内存 using na ...
- JS高精度乘法计算问题(牛客网乘法-求 a 和 b 相乘的值,a 和 b 可能是小数,需要注意结果的精度问题)
用到的知识点===> toFixed(num); toFixed() 方法可把 Number 四舍五入为指定小数位数的数字; 参数num: 代表小数位数: 例:var num = 5.56789 ...