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<n≤5×104,
0
<
a
i
≤
3000
0<a_i≤3000
0<ai≤3000,
0
<
t
≤
1
0
8
0<t≤10^8
0<t≤108
輸入樣例
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]
[i−x−1,i−1],因此:
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 i−x−1≤j≤i−1。
其中 min { f [ j ] } \min\{f[j]\} min{f[j]}就是求滑動視窗 [ i − x − 1 , i − 1 ] [i - x - 1, i - 1] [i−x−1,i−1]的最小值,因此可以使用單調佇列進行優化。
時間複雜度
二分搜尋的時間複雜度為 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;
}