RMQ問題(from leetcode周賽的折磨)

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個數開始,連續一個數的區間的最值。連續一個數,其實就只有一個,就是他自己。spa

因此咱們獲得,這裏咱們假設數組arr 是從1開始。code

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

推導轉移方程

對於dpi,咱們如何作求值?blog

其實能夠將這個區間分爲2部分。第一部分爲dpi,第二部分爲dpi+(1<<(j-1))。而後依賴兩個區間的結果再求大區間的最值。
你們看到這兩個區間可能很懵逼。不着急看,咱們一個一個來分析。leetcode

  • 首先是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。前提是分出來的區間的結果是提早知道的。rem

因此咱們能夠獲得狀態轉移方程(以區間最大值舉例):get

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
}
相關文章
相關標籤/搜索