【訓練題28:二分+單調佇列】[NOIP2017 普及組] 跳房子 | P3957
阿新 • • 發佈:2021-01-18
難度
提
高
+
/
省
選
−
\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
表示每次跳躍你都可以向右移動一定整數長度,這個長度要在 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
1≤n≤5×105
1
≤
d
≤
2000
1\le d\le 2000
1≤d≤2000
1
≤
p
o
s
i
,
k
≤
1
0
9
1\le pos_i,k\le 10^9
1≤posi,k≤109
∣
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
- 因為你每次走的長度在區間
[
max
(
1
,
d
−
g
)
,
d
+
g
]
[\max(1,d-g),d+g]
[max(1,d−g),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 i−ed≥d2
- 若能進單調佇列,然後再按照單調佇列的程式碼,刪重
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 i−st≤d1
- 如果不滿足,我們直接將這個點給刪掉。
狀態轉移
- 我們如果
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;
}