1. 程式人生 > 其它 >C++題解:綠色通道——單調佇列優化DP

C++題解:綠色通道——單調佇列優化DP

技術標籤:動態規劃資料結構

題目描述

高二數學《綠色通道》總共有 n n n 道題目要抄,編號 1 , 2 , … , n 1,2,…,n 1,2,,n,抄第 i i i 題要花 a i a_i ai 分鐘。

小 Y 決定只用不超過 t t t 分鐘抄這個,因此必然有空著的題。

每道題要麼不寫,要麼抄完,不能寫一半。

下標連續的一些空題稱為一個空題段,它的長度就是所包含的題目數。

這樣應付自然會引起馬老師的憤怒,最長的空題段越長,馬老師越生氣。

現在,小 Y 想知道他在這 t t t 分鐘內寫哪些題,才能夠儘量減輕馬老師的怒火。

由於小 Y 很聰明,你只要告訴他最長的空題段至少有多長就可以了,不需輸出方案。

輸入格式

第一行為兩個整數 n , t n,t n,t

第二行為 n n n 個整數,依次為 a 1 , a 2 , … , a n a_1,a_2,…,a_n a1,a2,,an

輸出格式

輸出一個整數,表示最長的空題段至少有多長。

資料範圍

0 < n ≤ 5 × 1 0 4 0<n≤5×10^4 0<n5×104,
0 < a i ≤ 3000 0<a_i≤3000 0<ai3000,
0 < t ≤ 1 0 8 0<t≤10^8 0<t108

輸入樣例

17 11
6 4 5 2 5 3 4 5 2 3 4 5 2 3 6 3 5

輸出樣例

3

演算法思想

二分搜尋

分析題目描述,求最長的空題段至少有多長,才能保證在 t t t分鐘內抄完題目。求至少有多長,可以考慮使用二分搜尋進行查詢。假設最長的空題段的長度至少為 x x x滿足要求,那麼 x x x一定在區間 [ 0 , n ] [0,n] [0,n]中,如下圖所示:
在這裡插入圖片描述

  • 區間的左邊,即空題段的長度小於 x x x,此時要花更多的時間,因此不滿足要求
  • 區間的右邊,即空題段的長度大於等於 x x x,此時花更少的時間就可以滿足要求

因此本題符合二分的性質,可以使用二分搜尋來確定 x x x的值。

DP+單調佇列優化

二分之後,需要確定在最長空題段長度不超過 x x x的情況下,需要花費的最少時間,可以使用DP求解。

狀態表示

f[i]表示完成前i題在滿足情況的條件下,且抄第i 題所花費的最小總時間。

狀態計算

因為最長只能有 x x x個連續的空題,那麼可以根據前面 x x x個題的狀態計算出f[i],也就是說能轉移到 f [ i ] f[i] f[i]的合法狀態的區間為 [ i − x − 1 , i − 1 ] [i - x - 1, i - 1] [ix1,i1],因此:

f [ i ] = min ⁡ { f [ j ] } + a [ i ] f[i]=\min\{f[j]\} + a[i] f[i]=min{f[j]}+a[i] i − x − 1 ≤ j ≤ i − 1 i-x-1\le j\le i-1 ix1ji1

其中 min ⁡ { f [ j ] } \min\{f[j]\} min{f[j]}就是求滑動視窗 [ i − x − 1 , i − 1 ] [i - x - 1, i - 1] [ix1,i1]的最小值,因此可以使用單調佇列進行優化。

時間複雜度

二分搜尋的時間複雜度為 O ( l o g n ) O(logn) O(logn),DP時間複雜度為 O ( n ) O(n) O(n),總的時間複雜度為 O ( n l o g n ) O(nlogn) O(nlogn)

程式碼實現

#include <iostream>

using namespace std;

const int N = 50010;
int n, t;
int a[N], q[N];
int f[N];

bool check(int x)
{
    f[0] = 0; //初始狀態
    int hh = 0, tt = 0;
    q[0] = 0;
    for(int i = 1; i <= n; i ++)
    {
        //處理滑出視窗的元素
        if(hh <= tt && q[hh] + x + 1 < i) hh ++;
        
        //從區間[i - x - 1, i - 1]的最小值轉移到f[i]
        f[i] = f[q[hh]] + a[i];
        
        while(hh <= tt && f[q[tt]] >= f[i]) tt --;
        q[++ tt] = i;
    }
    
    //打擂臺求最後一段的最小值
    int res = 1e9;
    for(int i = n - x; i <= n; i ++) res = min(res, f[i]);
    
    return res <= t;
}

int main()
{
    cin >> n >> t;
    
    for(int i = 1; i <= n; i ++) cin >> a[i];
    
    //二分搜尋求最長的空題段至少有多長
    int l = 0, r = n;
    while(l < r)
    {
        int mid = l + r >> 1;
        if(check(mid)) r = mid;
        else l = mid + 1;
    }
    cout << r << endl;
    return 0;
}