RMQ问题(from leetcode周赛的折磨)

gosh

1.概述

这篇blog来源于leetcode。参加了第198场周赛,结果比前几次周赛惨很多。不过没关系,及时发现了自己很菜,路漫漫其修远兮!这边blog主要是针对周赛第四题衍发出来的思考。主要包括RMQ问题以及自己思考题目的过程。价值不是很大,随便写写。

2.RMQ问题

RMQ(Range Minimum / Maximum Query )主要是用来求区间最值问题研究出来的算法。
对于RMQ问题有很多方法可以求解,比如线段树,或者使用动态规划。对于静态区间的RMQ问题,使用DP是非常好理解的。下面我们就来聊聊。

举个简单的例子

比如给定一个无序数组arr。求数组所有区间的最值。如果通过暴力枚举,肯定会TLE。但是我们很容易想到。对于一个大的区间[0,1],我们可以将其分为两个子区间:[0,1]和[2,3]。那么大区间的最值,其实可以通过两个子区间得到。

使用动态规划的思想

大问题的结果依赖若干个子问题。

既然使用动态规划,我们就需要列出状态转移方程。
我们令dpi表示:以第i个数为起点,连续2^j个数 的区间最值。比如dp2就是区间[2,3]的最值。3怎么来。其实就是2+2^1-1 ,减1时减掉第一个数。

接下来我们考虑一下边界条件(对于动态规划,边界条件是解决问题的突破口)

依据上面定义:dpi代表数组第i个数开始,连续一个数的区间的最值。连续一个数,其实就只有一个,就是他本身。

所以我们得到,这里我们假设数组arr 是从1开始。

dp[i][0]=arr[i];

推导转移方程

对于dpi,我们如何做求值?

其实可以将这个区间分为2部分。第一部分为dpi,第二部分为dpi+(1<<(j-1))。然后依赖两个区间的结果再求大区间的最值。
大家看到这两个区间可能很懵逼。不着急看,我们一个一个来分析。

  • 首先是dpi,这个区间我们应该很容易理解。就是以i开始,共2^(j-1)个数。其实就是以i开始,2^j的前半部分。因为2^(j-1)是2^j的一半。
  • 其次是dpi+(1<<(j-1))。这个看起来复杂。但从上可知,这个表示的就是剩余半个区间的最值。我们根据dp的定义来推到一下。后半部分区间就是从前半个区间最后一个元素的后一个元素到区间末尾。那么我们就需要计算一下后半个区间的开始位置。其实就是大区间初始位置i+区间长度的一半。长度计算依赖j。所以就可以得到是i+(1<<(j-1))。


这里的思想就是一分为2。前提是分出来的区间的结果是提前知道的。

所以我们可以得到状态转移方程(以区间最大值举例):

dp[i][j]= max {dp[i][j-1],dp[i+(1<<(j-1)][j-1]}  

查询最值

通过上面过程,我们将最值计算出来,但是我们如何获取结果呢?

我们假设len为要查询区间的长度。我们log(len)也就是我们dpi中j的长度。但是我们并不能保证2^log(len)==len。因为len不一定是2的整数幂。所以我们并不能保证区间的完整性。

如果该长度正好是2的幂。那么没毛病,结果为dpi,否则我们会遗漏一些区间,如下图。那么如何解决问题呢?


大家可以看到我们可以使用dpi和dpr-(1<<k)+1。使用后者是为了补充我们的遗漏。但是大家可能会担心有重复。但是如果是求最值问题,重复是不会影响结果的。所以,很ok。

3.比赛题目分析

题目链接:

https://leetcode-cn.com/probl...

题目分析:

我们对函数分析后,发现对于l<=r,他的结果是一个递减的序列。因为与运算。与的越多最终值越小。如果一个区间[l,r]按上述函数进行与。结果肯定小于等于区间最小值。

既然是一个有序序列,我们就可以使用二分。我们枚举右边界。然后通过二分对区间求值并记录结果。时间复杂为nlog(n)

没错,开始我就是这么做的,但是:


看到这个,我就开始定位耗时。应该是在进行区间与运算的时候浪费时间。
所以我们需要进行优化。
于是我想到了区间最值问题。与运算其实和其是一样的。比如同一个大的区间的与运算结果,我们可以通过两个小区间的结果再进行与操作。

并且对于重复与相同数字,结果是不会受影响的,这个比较关键,因为我们在查询区间最值的时候,会重复计算。

于是我用动态规划构建区间结果。最终解决了问题。

贴出代码

RMQ动态规划代码实现

//RMQ问题代码
type RMQ struct {
    Dp [][]int
}
func (rmq *RMQ) init(arr []int) {
    dp := make([][]int, len(arr))
    rmq.Dp = dp
    for i := 0; i < len(arr); i++ {
        dp[i] = make([]int, 20)
    }
    //初始化条件。从i起的一个数(2^0)的最小值  就是该数。
    for i := 1; i < len(arr); i++ {
        dp[i][0] = arr[i]
    }
    //
    for j := 1; (1 << j) < len(arr); j++ {
        for i := 1; i+(1<<(j-1)) < len(arr); i++ {
        //这里需要注意 为什么临界条件为i+(1<<(j-1)) < len(arr)。
        //因为i会被j限制。 j越大。i能取的就越小。我们只需要保证从i开始到结束的元素全覆盖就可以了。
        //这里将范围分成了两部分。 因为我们基于2的幂。 其实就是参考二进制的性质。通过移位运算符可以进行二分。
            dp[i][j] = rmq.withStrategy(i, j)
        }
    }
}
func (rmq *RMQ) withStrategy(i int, j int) int {
    return rmq.Dp[i][j-1] & rmq.Dp[i+(1<<(j-1))][j-1]
}
func (rmq *RMQ) withStrategyQuery(l int, r int, k int) int {
    return rmq.Dp[l][k] & rmq.Dp[r-(1<<k)+1][k]
}
func (rmq *RMQ) query(l int, r int) int {
    k := 0
    for ; (1 << (k + 1)) <= r-l+1; k++ {
    }
    return rmq.withStrategyQuery(l, r, k)
}

算法逻辑(二分)

func closestToTarget(arr []int, target int) int {
    minVal := math.MaxInt32

    rmq := RMQ{}
    tmp := make([]int, len(arr)+1)
    for k := 0; k < len(arr); k++ {
        tmp[k+1] = arr[k]
    }
    rmq.init(tmp)
    for r := 1; r < len(tmp); r++ {
        left := 1
        right := r
        for left <= right {
            mid := left + (right-left)/2
            res := rmq.query(mid, r)
            if res == target {
                return 0
            } else if res > target {
                right = mid - 1
            } else {
                left = mid + 1
            }
        }
        if right == 0 {
            minVal = min(minVal, rmq.query(left, r)-target)
        } else if left == r+1 {
            minVal = min(minVal, target-rmq.query(right, r))
        } else {
            minVal = min(min(rmq.query(left, r)-target, minVal), target-rmq.query(right, r))
        }
    }
    return minVal
}
func min(x, y int) int {
    if x > y {
        return y
    }
    return x
}
阅读 311
6 声望
1 粉丝
0 条评论
你知道吗?

6 声望
1 粉丝
宣传栏