二分金币把问题化为定跳距区间的可达性判定,双指针加单调队列把 O(n²) 的 DP 压到 O(n),总复杂度 O(n log G)。
OJ: roj
题目 ID: 3647
难度:提高
标签:动态规划二分答案单调队列python
创建: 2026-10-02 12:55
更新: 2026-10-10 11:30
形式化题目
数轴上有
内任意选取(
样例:
| 落点区间 | 起点 |
|||||
|---|---|---|---|---|---|---|
| 跳距 | 2 | 3 | 5 | 3 | 4 | 3 |
| 该格得分 | 6 | -3 | 3 | 1 | 6 | 2 |
六跳全部落在
正解
思路
朴素做法:定
窗口为空则格子
瓶颈不在 DP,而在"求窗内最大值"。 真正相关的前驱只有坐标落在
- 队列存下标,队内对应
值保持递减;新格子入队前弹出队尾所有不比它大的 (这些下标从此不可能成为最大值); - 过期格子一定在队首——下标递增即坐标递增,队首坐标最小,坐标
就出队; - 于是队首恒为窗内最大
,每次转移 摊还,整个判定 。
固定
| 0(起点) | 0 | 0 | — | 0 | — | 0 |
| 1 | 2 | 6 | — | |||
| 2 | 5 | -3 | ||||
| 3 | 10 | 3 | ||||
| 4 | 11 | -3 | — | |||
| 5 | 13 | 1 | ||||
| 6 | 17 | 6 | ||||
| 7 | 20 | 2 |
“窗"列是该格子的合法前驱坐标区间与实际选中的前驱,”—"表示窗内没有格子、该格不可达。
读表可见:
对
- 无解判据:每个格子至多踩一次,理论最高分 = 所有正分之和;它
时输出 。 题面样例 2 要 分,正分和只有 ,因此输出 。 - 上界:依次踩上全部正分格子(负格全跳过)必是一条合法路线。设相邻间隔最大为
(含起点到第一个正格),取 : 使 缩到 ,盖住任何 的间隔; 使 覆盖最大间隔。此 可行,答案必在 。
样例上:正分和
正确性要点:DP 满足最优子结构(以
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll NEG=-(ll)1000000000000000000LL;
bool check_g(ll g, vector<ll>&xs, vector<ll>&ss, ll d, ll k){ll lo=max(1LL,d-g), hi=d+g; int m=xs.size(); vector<ll> f(m,NEG); f[0]=0; deque<int> dq; int r=-1; for(int i=1;i<m;i++){ll right=xs[i]-lo; while(r+1<m && xs[r+1]<=right){r++; while(!dq.empty() && f[dq.back()]<=f[r]) dq.pop_back(); dq.push_back(r);} ll left=xs[i]-hi; while(!dq.empty() && xs[dq.front()]<left) dq.pop_front(); f[i]=(dq.empty()?NEG:f[dq.front()])+ss[i]; if(f[i]>=k) return true;} return false;}
ll min_coins(vector<ll>&xs,vector<ll>&ss,ll d,ll k){ll pos=0; for(int i=0;i<(int)ss.size();i++) if(ss[i]>0) pos+=ss[i]; if(pos<k) return -1; ll prev=0,maxgap=0; for(int i=0;i<(int)ss.size();i++) if(ss[i]>0){maxgap=max(maxgap,xs[i]-prev); prev=xs[i];} ll lo=0,hi=max(d-1,maxgap-d); while(lo<hi){ll mid=(lo+hi)/2; if(check_g(mid,xs,ss,d,k)) hi=mid; else lo=mid+1;} return lo;}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr); int n; ll d,k; if(!(cin>>n>>d>>k)) return 0; vector<ll> xs(1,0), ss(1,0); for(int i=0;i<n;i++){ll x,s;cin>>x>>s; xs.push_back(x); ss.push_back(s);} cout<<min_coins(xs,ss,d,k)<<'\n'; return 0;}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 13:01
# update_at: 2026-10-02 13:01
import sys
from collections import deque
NEG = -10**18 # 不可达哨兵:合法分数和最小只到 -n·max|s_i| = -5×10^10,哨兵区不会被误判
def check(g: int, xs: list[int], ss: list[int], d: int, k: int) -> bool:
"""花 g 个金币改造后能否拿到至少 k 分:单调队列滑窗 DP,够分即提前返回。"""
lo = max(1, d - g) # 改进后最短弹跳距离(题面规定至少为 1)
hi = d + g # 改进后最长弹跳距离
m = len(xs)
f = [NEG] * m
f[0] = 0 # 虚拟起点:位于 0,分数 0
dq: deque[int] = deque() # 窗内下标按 f 递减,队首就是窗内最高分
r = -1 # 已入队的最右下标;-1 表示虚拟起点还没入队
for i in range(1, m):
xi = xs[i]
right = xi - lo # 只有 xs[j] <= xi - lo 才跳得过来
while r + 1 < m and xs[r + 1] <= right:
r += 1
while dq and f[dq[-1]] <= f[r]: # 维持 f 单调递减
dq.pop()
dq.append(r)
left = xi - hi # xs[j] < xi - hi 的格子已经跳不到,出窗
while dq and xs[dq[0]] < left:
dq.popleft()
f[i] = (f[dq[0]] if dq else NEG) + ss[i] # 哨兵区加分数仍是哨兵区
if f[i] >= k: # 游戏可在任意格子结束,够分即可停
return True
return False
def min_coins(xs: list[int], ss: list[int], d: int, k: int) -> int:
"""二分最少金币数:正分总和不足 k 直接无解,否则在「依次访问全部正格」的上界内二分。"""
pos_sum = sum(s for s in ss if s > 0) # 只访问正格、跳过负格的最高分
if pos_sum < k:
return -1
# 上界:依次访问所有正格时,g 要让 [max(1, d-g), d+g] 盖住起点到各正格的每个间隔
prev, max_gap = 0, 0
for i, s in enumerate(ss):
if s > 0:
max_gap = max(max_gap, xs[i] - prev)
prev = xs[i]
lo_g, hi_g = 0, max(d - 1, max_gap - d) # d-1 让下界缩到 1,max_gap-d 让上界够长
while lo_g < hi_g:
mid = (lo_g + hi_g) // 2
if check(mid, xs, ss, d, k):
hi_g = mid
else:
lo_g = mid + 1
return lo_g
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, d, k = next(data), next(data), next(data)
xs, ss = [0], [0]
for _ in range(n):
x = next(data)
s = next(data)
xs.append(x)
ss.append(s)
print(min_coins(xs, ss, d, k))
if __name__ == "__main__":
solve()复杂度
- 时间:单次判定
(双指针 + 单调队列,每个下标至多进出队各一次), 二分 次( ,至多约 31 次), 合计 ,与标准 C++ 解法同阶。实测 的数据本地 0.6–0.8s; OJ 时限 1000ms,Python 存在 TLE 风险,但算法复杂度不降级。 - 空间:
,三个长度 的数组加单调队列。 - 同样的算法在 C++ 里用
vector<long long>存、 std::deque<int>做单调队列即可;的范围约 ,需 long long。
总结
- 题目 = “定
判定” + "对 二分"两层:判定层是线性 DP,二分层靠区间单调性。 - 朴素
的瓶颈是全量扫前驱;观察到转移窗口随 单调滑动后, 单调队列把判定压到 ,总复杂度 。 与二分上界都来自"只踩正格"这条构造性路线:正分和给出无解判据, 正格最大间隔给出可行上界 。 - 跳距下界是
而不是 :样例中 只能拿 分正是这个下界卡出来的。