## [$AcWing$ $1090$. 绿色通道](https://www.acwing.com/problem/content/1092/) ### 一、题目描述 高二数学《绿色通道》总共有 $n$ 道题目要抄,编号 $1,2,…,n$,抄第 $i$ 题要花 $a_i$ 分钟。 小 $Y$ 决定只用 **不超过 $t$ 分钟** 抄这个,因此必然有空着的题。 每道题要么不写,要么抄完,不能写一半。 **下标连续的一些空题称为一个空题段,它的长度就是所包含的题目数。** 这样应付自然会引起马老师的愤怒,最长的空题段越长,马老师越生气。 现在,小 $Y$ 想知道他在这 $t$ 分钟内写哪些题,才能够尽量减轻马老师的怒火。 由于小 $Y$ 很聪明,你只要 **告诉他最长的空题段至少有多长** 就可以了,不需输出方案。 **输入格式** 第一行为两个整数 $n,t$。 第二行为 $n$ 个整数,依次为 $a_1,a_2,…,a_n$。 **输出格式** 输出一个整数,表示最长的空题段至少有多长。 **数据范围** $0 using namespace std; const int N = 50010; const int INF = 0x3f3f3f3f; int n, m; int w[N]; int f[N]; // 已经完成前i个,并且第i个被选中 情况下抄袭时间最小值 // 暴力+DP 通过了 8/12个数据 bool check(int limit) { // 注意这个初始化,我吃了这个亏~ memset(f, 0x3f, sizeof f); f[0] = 0; for (int i = 1; i <= n; i++) // 计算前i道题,填表填数据 for (int j = max(0, i - limit - 1); j <= i - 1; j++) // 第i题肯定是选中的,那么它前面的是哪个位置选中呢? f[i] = min(f[i], f[j] + w[i]); // 最后[n-limit,n]之间必然得有一题答了吧,这些都是合法解 int res = INF; for (int i = n - limit; i <= n; i++) res = min(res, f[i]); // 最小值比m小的话,那么就是有可行解 return res <= m; } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) cin >> w[i]; // 最长的空题段∈[1,n] // 二分(求最长的最小) int l = 0, r = n; // 空段区间长度0~n都有可能,注意此处是带0的,因为可能老师要求每题必答,对吧,你以为隔一题空一题老师能忍,其实人家忍不了! // 事实也证明了这一点,我尝试修改为 int l=1,r=n;会挂掉一个测试点!  /* 空白时段的长度越长,花费的时间越少 空白时段的长度越短,花费的时间越长 */ while (l < r) { int mid = (l + r) >> 1; // 一定要搞清楚mid的含义:模拟的最长空题段长度 if (check(mid)) // 如果这个空白长度可以使得整体时间<=m,那么空白长度可以再小一点,这是个弯,好好想想 r = mid; else l = mid + 1; // 如果现在的长度不能使得时间够用,只能是长度再长一点 } printf("%d\n", l); return 0; } ``` ### 三、单调队列优化 上面的思路是对的,因为可以正常通过$12$中的$7$个测试点,但其它的$TLE$了,需要优化: 因为状态转移方程定义的方式,决定了$i$是必选的,那么$w[i]$是 **确定** 要忍受的代价,整体代价要想最小,只能是前面的代价最小! $f[i]$的所有前序可选状态是:$i$左侧$limit+1$个范围,也就是要找出这个范围内的$min(f[j])$,$j \in [i-limit-1,i-1]$。 指定区间内的最小值问题,可以使用单调队列来优化: ![](https://img2020.cnblogs.com/blog/8562/202201/8562-20220118133645815-596123684.png) #### $Q$:答案在哪里? 需要在$n \sim n-limit$之间去枚举找出最小值做为答案。 举个栗子秒懂: $n=10,limit=3$,那么如果$7$被选中,那么$8,9,10$是不用写的。 ```cpp {.line-numbers} #include using namespace std; const int N = 50010; const int INF = 0x3f3f3f3f; int n, m; int w[N]; int f[N]; int q[N], hh, tt; bool check(int limit) { hh = 0, tt = 0, q[0] = 0; for (int i = 1; i <= n; i++) { // 窗口的长度是limit while (hh <= tt && q[hh] < i - limit - 1) hh++; // 直接计算 f[i] = f[q[hh]] + w[i]; // i入队列 while (hh <= tt && f[q[tt]] >= f[i]) tt--; q[++tt] = i; } // 答案可能存在于 n,n-2,...n-m中,枚举一下求最少值即可 int res = INF; for (int i = n - limit; i <= n; i++) res = min(res, f[i]); // 最小值比m小的话,那么就是有可行解,我才不管是不是有其它的值也比m小不小! return res <= m; } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) cin >> w[i]; // 二分(求最长的最小),就是碰一下区间的长度,大了就变小,小了就变大 int l = 0, r = n; // 空段区间长度0~n都有可能 // 当我们允许的空白时段的长度越长,那么花费的时间越少 while (l < r) { int mid = (l + r) >> 1; if (check(mid)) r = mid; else l = mid + 1; } cout << l << endl; return 0; } ```