文章目录
代码随想录之贪心系列区间问题算法
1.跳跃游戏
1.1贪心算法
func canJump(nums []int) bool {
farthest := 0
for i, v := range nums {
//不断的计算能跳到的最大索引
farthest = max(farthest, i+v)
//如果faarthest大于等于了终点下标,直接return true就可以了
if farthest >= len(nums)-1 {
return true
}
//可能碰到0,卡住不动了,及时退出.不能再往后算了,否则不正确
if farthest == i {
return false
}
}
return false
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
1.2动态规划
2.跳跃游戏II
2.1贪心算法(详细版)
//题目说了:我们总是可以跳到最后一个位置的
func jump(nums []int) int {
a := 0 //即返回值,走的最大步数
maxdistace := 0 // 当前覆盖最远距离下标
nextmaxdistance := 0 // 下一步覆盖最远距离下标
for i, v := range nums {
// 更新下一步覆盖最远距离下标
nextmaxdistance = max(nextmaxdistance, i+v)
if i == maxdistace {
if maxdistace != len(nums)-1 {
a = a + 1 //如果当前覆盖最远距离下标不是终点,则需要再走一步
maxdistace = nextmaxdistance
// 下一步的覆盖范围已经可以达到终点,结束循环
//先移动,再跳
if nextmaxdistance >= len(nums)-1 {
return a
}
} else if maxdistace == len(nums)-1{
return a // 当前覆盖最远距离下标是集合终点,不用做ans++操作了,直接结束
}
}
}
return a
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
2.2贪心算法(优化版)
//相对于详细版来说,其精髓在于控制移动下标i只移动到nums.size() - 2的位置(原因:多加了一步),所以移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不用考虑别的
func main() {
jump([]int{2, 3, 1, 1, 4, 3, 2, 5})
}
func jump(nums []int) int {
a := 0 //即返回值,走的最大步数
maxdistace := 0 // 当前覆盖最远距离下标
nextmaxdistance := 0 // 下一步覆盖最远距离下标
//注意:这里是len[nums]-1,这是关键所在
//由于一定能到达,所以不用考虑[0,2,1]这种开头第一个是0元素的数据
//当nums=[0]时,不会进入for循环,直接return 0
//不能改成i<len(nums),会出错
//如果这道题的nums时的元素个数>=2时,第一个元素一定不会等于0
// 这里有个小细节,因为是起跳的时候就 + 1 了,如果最后一次跳跃刚好到达了最后一个位置,那么遍历到最后一个位置的时候就会再次起跳,这是不允许的,因此不能遍历最后一个位置
for i :=0;i<len(nums)-1;i++ {
// 更新下一步覆盖最远距离下标
nextmaxdistance = max(nextmaxdistance, i+nums[i])
//即遍历完了上次所能覆盖的最大范围,该重新定义范围
if i == maxdistace {
//遇到当前覆盖的最远距离下标,则更新当前覆盖的最远距离下标
maxdistace = nextmaxdistance
a++//先跳再移动
}
}
return a
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
//之后会一直循环,一直不进入if循环,maxdistace=8 nextmaxdistance=8 a=3 一直保持不变
//题目说明:假设你总是可以到达数组的最后一个位置。
即一定存在一条路线能够到达最后一个位置,而不是说中间没有 0 ,只是存在可以越过 0 的路线
//
i=0: nextmaxdistance=2 进入if循环 maxdistace=2 a=1
i=1:nextmaxdistance=2 不进入if循环
i=2 nextmaxdistance=4 进入if循环 maxdistace=4 a=2
i=3 nextmaxdistance=4 不进入if循环
i=4 nextmaxdistance=8 进入if循环 maxdistace=8 a=3
//https://www.bilibili.com/video/BV1SA41147aU?from=search&seid=6340119370474831070&spm_id_from=333.337.0.0
2.3动态规划
// v1 动态规划
func jump(nums []int) int {
// 动态规划四步走:
// 1、状态:f[i] 表示从起点到当前位置跳的最小次数
// 2、推导:f[i] = min(f[j]+1),a[j]+j >=i 表示从j位置用一步跳到当前位置,这个j位置可能有多个,取最小的一个就行
// 3、初始化:f[0] = 0
// 4、结果:f[n-1]
f := make([]int, len(nums))
f[0] = 0//跳到第一个数的是最小次数是0
for i := 1; i < len(nums); i++ {
// f[i] 先取一个默认最大值i,i是最大值
f[i] = i
// 遍历之前结果取一个最小值+1
//假如说i是3,那么j只遍历到2
for j := 0; j < i; j++ {
///穷举每一个选择
// 计算每一个子问题的结果
//”j当前所在索引+它可以走的最大值"大于当前i所在索引
if nums[j]+j >= i {
//j所在索引再走一步即可走到i ; 一次次的于F[i]做比较
f[i] = min(f[j]+1,f[i])
}
}
}
return f[len(nums)-1]//跳到最后一个元素的最小次数
}
func min(a, b int) int {
if a > b {
return b
}
return a
}
//就可以暴力穷举所有可能的跳法,通过备忘录 memo 消除重叠子问题,取其中的最小值最为最终答案
使用数组dp[i]记录跳跃到当前位置所需的最少跳跃次数,dp[i]由dp[j]决定(其中0<=j<i),当j+nums[j]>=i时,说明可以从位置j跳跃到位置i,从所有可以跳跃的j中选出最小的dp[j],则到达当前位置的最小跳跃次数为dp[i]=dp[j]+1。最后返回终点的dp[n-1]即为到达数组最后一个位置所需的最小跳跃次数(其中n为nums长度)。
3.合并区间
3.1贪心算法
//贪心算法
func merge(intervals [][]int) [][]int {
//先从小到大排序,对左边接进行排序
sort.Slice(intervals,func(i,j int)bool{
return intervals[i][0]<intervals[j][0] s
})
//再弄重复的
for i:=0;i<len(intervals)-1;i++{
/*有4种情况
[1,5] [2,3] ------[1,5]=[1,5] 这是包含关系,区间包含
[1,5] [2,6] ------[1,5]=[1,6] 区间重叠
[1,5] [1,5] ------[1,5]=[1,5]
[1,5] [6,9]
*/
if intervals[i][1]>=intervals[i+1][0]{
intervals[i][1]=max(intervals[i][1],intervals[i+1][1])//赋值最大值
intervals=append(intervals[:i+1],intervals[i+2:]...)//删除第i+1个元素
i--
}
}
return intervals
}
func max(a,b int)int{
if a>b{
return a
}
return b
}
//时间复杂度:O(n*logn)
4.划分字母区间
4.1贪心算法
//贪心算法
func partitionLabels(s string) []int {
var res []int
var marks [26]int
size, left, right := len(s), 0, 0
//用于记录每个字母最后一次出现的下标位置,第二次遍历时进行字符串的划分。
for i := 0; i < size; i++ {
//marks[0]代表的是0;marks[25]代表的是z;且
marks[s[i]-'a'] = i
/*不能直接写成marks[s[i]] = i的原因是s[i]的asscii码太大,会超出索引26*/
}
/*从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点*/
for i := 0; i < size; i++ {
//marks[s[i]-'a']是单当前字母最后一次出现的索引
right = max(right, marks[s[i]-'a'])
if i == right {
res = append(res, right-left+1)
left = i + 1 //记录每一个片段的第一个值所对应的索引
}
}
return res
}
func max(a, b int) int {
if a < b {
a = b
}
return a
}
//传入[abab]输出[4]
//时间复杂度:O(N)
5.无重叠区间
5.1贪心算法
//贪心算法
func eraseOverlapIntervals(intervals [][]int) int {
var flag int
//先从小到大排序,对左边界进行排序
升序排序,时间复杂度O(NlogN),快排
sort.Slice(intervals,func(i,j int)bool{
return intervals[i][0]<intervals[j][0]
})
/*有4种情况
[1,5] [2,3] 这是包含关系,区间包含
[1,5] [2,6] 区间重叠
[1,5] [1,5]
[1,5] [6,9]
*/
for i:=0;i<len(intervals)-1;i++{
// 因为已经按照左端点进行升序排序,所以只要左边的右端点越过了右边的左端点,便出现了重叠区间
if intervals[i][1]>intervals[i+1][0]{
// 此时需要移除的最小重叠区间数累加1
flag++
// 要移除的重叠区间数最小,自然是要右边的右端点尽可能小,才能尽可能的不与后继区间(i+1区间)重叠
intervals[i+1][1]=min(intervals[i][1],intervals[i+1][1])
}
}
return flag
}
func min(a,b int)int{
if a>b{
return b
}
return a
}
//时间复杂度O(NlogN+N), 空间复杂度O(1)
6.用最少数量的箭引爆气球
6.1贪心算法
func findMinArrowShots(points [][]int) int {
var res int =1//弓箭数,初始为1
//先按照第一位排序
sort.Slice(points,func (i,j int) bool{
return points[i][0]<points[j][0]
})
for i:=0;i<len(points)-1;i++{
if points[i][1]<points[i+1][0]{//如果前一位的右边界小于后一位的左边界,则一定不重合
res++//不重合的话需射一箭
}else{
//复制完后相等于重新开始计算
points[i+1][1] = min(points[i][1], points[i+1][1]); // 更新重叠气球最小右边界,覆盖该位置的值,留到下一步使用
}
}
return res
}
func min(a,b int) int{
if a>b{
return b
}
return a
}
//如果有重合的话,射击的距离一定是再a[i+1]与min(a[i,1],a[i+1,1])之间的,为了更大距离的可以射到下一个气球则赋值a[i+1,1]为min(a[i,1],a[i+1,1])