1. 程式人生 > 其它 >【訓練題28:二分+單調佇列】[NOIP2017 普及組] 跳房子 | P3957

【訓練題28:二分+單調佇列】[NOIP2017 普及組] 跳房子 | P3957

技術標籤:【各類ACM真題】演算法單調佇列

[NOIP2017 普及組] 跳房子 | P3957

難度

提 高 + / 省 選 − \color{cyan}提高+/省選- +/
− 8.02 k 48.38 k -\frac{8.02k}{48.38k} 48.38k8.02k

題意

  • 首先給你一個 x x x 座標軸,開始在原點,正方向處給定 n n n 個點。
  • 給定這每個點的座標 p o s i pos_i posi 以及每個點的收益 s c o i sco_i scoi (可正可負)
  • 給你一個彈跳力 d d d 。一開始每次跳躍只能向右移動 d d d 個單位。
  • 你花 g g
    g
    塊錢,使得彈跳力變成一個區間 S = [ max ⁡ ( 1 , d − g ) , d + g ] S=[\max(1,d-g) ,d+g] S=[max(1,dg),d+g]
    表示每次跳躍你都可以向右移動一定整數長度,這個長度要在 S S S 區間中。
  • 但是你每次必須跳在給定的點中。

問你至少花多少錢,才可以使得某種跳躍方案,你的收益超過 k k k
若無法收益超過 k k k ,輸出 − 1 -1 1

資料範圍

1 ≤ n ≤ 5 × 1 0 5 1\le n\le 5\times 10^5 1n5×105
1 ≤ d ≤ 2000 1\le d\le 2000

1d2000
1 ≤ p o s i , k ≤ 1 0 9 1\le pos_i,k\le 10^9 1posi,k109
∣ s c o i ∣ < 1 0 5 |sco_i|<10^5 scoi<105

思路

1.最開始的思路

首先我們拋開其他的條件,就問你:給定 g g g 塊錢,你怎麼算此時的收益最大值?

  • 每次都可以從一些點跑到另一些點,直接 D P DP DP 不就好了?
  • d p ( i ) dp(i) dp(i) 表示走到第 i i i 個點的最大收益,然後列出簡單的狀態轉移方程:
  • d p ( i ) = max ⁡ { d p ( s t ) , d p ( s t + 1 ) , ⋯   , d p ( e d ) } + s c o i dp(i)=\max\{dp(st),dp(st+1),\cdots,dp(ed)\}+sco_i
    dp(i)=max{dp(st),dp(st+1),,dp(ed)}+scoi
  • 因為你每次走的長度在區間 [ max ⁡ ( 1 , d − g ) , d + g ] [\max(1,d-g),d+g] [max(1,dg),d+g] 範圍之內,因此轉移圖肯定是這樣的:
    在這裡插入圖片描述
  • 如果是上面那種轉移一推多,時間複雜度就會大幅增加。所以我們選擇下面這種。
  • 對於每一個 i i i ,易得這一段 [ s t , e d ] [st,ed] [st,ed] 肯定是 像滑動視窗一樣向右移動的,這就是這題的突破口。
  • 我們每次要求這一段區間的最大值,直接使用單調佇列RMQ即可。時間複雜度 O ( N ) O(N) O(N)

為什麼不能用 S T ST ST 表?

  • S T ST ST 表是靜態的。每次你轉移都會更新 d p ( i ) dp(i) dp(i),那就無法在時間內實現該效果了。

2.求花費最小?

來,跟我讀:

  • 求收益最大的最小花費用二分
  • x x x 最小時 y y y 最大用二分
  • x x x 最大時 y y y 最小用二分
  • 我咋老記不住呢???

對於花費 g g g,明顯符合二分性質。我們就二分它唄。

3.具體實現單調佇列?

  • 這單調佇列的程式碼實現也是讓我敲得很費勁。
  • 由於我們的狀態轉移會修改陣列的值,我們每次就 c o p y copy copy 一下原 s o c soc soc 陣列變成 d p dp dp 陣列
  • 首先你最開始在原點,即第 0 0 0 個點。狀態轉移的方程 i i i 肯定是從 1 1 1 n n n 的。
  • 因為 i i i和滑動視窗的右端點是不對應的,我們還要記錄一下滑動視窗的右端點下標 s t st st

繼續看圖啦!在這裡插入圖片描述
進佇列

  • 首先 e d ed ed 能進滑動視窗的條件: i − e d ≥ d 2 i-ed\ge d_2 iedd2
  • 若能進單調佇列,然後再按照單調佇列的程式碼,刪重 while(Q.size() && dp[Q.back()] <= dp[ed])Q.pop_back();
  • 當然也可以每次 c h e c k check check 到一半發現收益大於 k k k 然後返回,我這裡沒有這麼寫。

出佇列

  • 然後,考慮隊頭是否還在視窗中的條件: i − s t ≤ d 1 i-st\le d_1 istd1
  • 如果不滿足,我們直接將這個點給刪掉。

狀態轉移

  • 我們如果 Q.size()!=0 滿足的,也就是說該點可以通過前面的點轉移過來,直接轉移
  • dp[i] = dp[Q.front()] + sc[i];
  • 如果 Q.size()==0 也就是說該點不能通過前面的轉移過來,那麼直接給該點設定一個負無窮大的收益即可。

答案

  • 收益最大值就是 max ⁡ { d p ( 0 ) , d p ( 1 ) , ⋯   , d p ( n ) } \max\{dp(0),dp(1),\cdots,dp(n)\} max{dp(0),dp(1),,dp(n)}

4. 小優化

  • 資料量大,快讀。
  • 如果所有的收益為正的點全拿了,都無法超過 k k k ,直接輸出 − 1 -1 1

核心程式碼

時間複雜度: O ( N log ⁡ N ) O(N\log N) O(NlogN)
空間複雜度: O ( N ) O(N) O(N)

/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
const int MAX = 5e5+50;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;

ll dis[MAX],sc[MAX];
ll dp[MAX];
deque<ll>Q;
int n,d,k;

ll check(int x){
    int d1 = d + x;
    int d2 = max(1,d-x);
    ll ans = 0;

    for(int i = 1;i <= n;++i){
        dp[i] = sc[i];
    }
    while(Q.size())Q.pop_back();

    int ed = -1;		/// 因為一開始原點的下標為0的點還沒有進入佇列

    dis[0] = 0;
    sc[0]  = 0;
    dp[0]  = 0;

    for(int i = 1;i <= n;++i){
        while(ed + 1 < i && dis[i] - dis[ed+1] >= d2){		/// 進佇列
            ed++;
            while(Q.size() && dp[Q.back()] <= dp[ed])Q.pop_back();
            Q.push_back(ed);
        }
        while(Q.size() && dis[i] - dis[Q.front()] > d1)Q.pop_front();	/// 出佇列

        if(Q.size()){		/// 狀態轉移
            dp[i] = dp[Q.front()] + sc[i];
            ans = max(ans,dp[i]);
        }else dp[i] = -LINF;
    }
    return ans;
}

int main()
{
    n = read();
    d = read();
    k = read_ll();
    ll pos = 0;
    for(int i = 1;i <= n;++i){
        dis[i] = read_ll();
        sc[i] = read_ll();
        if(sc[i] > 0)pos += sc[i];
    }
    if(pos < k){
        puts("-1");
        return 0;
    }

    int L = 0,R = dis[n];
    while(L < R){
        int M = L + R >> 1;
        if(check(M) >= k)R = M;
        else L = M + 1;
    }
    printf("%d",L);
    return 0;
}