You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

7.0 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 1090. 绿色通道

一、题目描述

高二数学《绿色通道》总共有 n 道题目要抄,编号 1,2,…,n,抄第 i 题要花 a_i 分钟。

Y 决定只用 不超过 t 分钟 抄这个,因此必然有空着的题。

每道题要么不写,要么抄完,不能写一半。

下标连续的一些空题称为一个空题段,它的长度就是所包含的题目数。

这样应付自然会引起马老师的愤怒,最长的空题段越长,马老师越生气。

现在,小 Y 想知道他在这 t 分钟内写哪些题,才能够尽量减轻马老师的怒火。

由于小 Y 很聪明,你只要 告诉他最长的空题段至少有多长 就可以了,不需输出方案。

输入格式 第一行为两个整数 n,t

第二行为 n 个整数,依次为 a_1,a_2,…,a_n

输出格式 输出一个整数,表示最长的空题段至少有多长。

数据范围 0<n≤5×10^4,0<a_i≤3000,0<t≤10^8

输入样例:

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

输出样例

3

二、二分+DP

t<=10^8,好BT的数据上限!这就是在提示我们,不能选择O(N)的算法,那样基本上凉了!需要有一个O(logN)的算法: 二分

目标

  • t 分钟内写完作业
  • 尽量减轻马老师的怒火
  • 找出最长空白长度

分析

  • 空白区越大,用时越少
  • 空白区越小,用时越大

如果我们选择 二分 的思路:

通过二分,找出一个当前的尝试的区间空白最大值limit,

如果当前枚举到的区间空白最大值limit,

  • 可以在t分钟内完成,那么让limit再小一点试试,如果小一点的也可以在t分钟内完成,马老师的怒火会更小一点,惹毛了马老师,后果严重。
  • 不能在t分钟内完成,那么让limit再大一点试试,这样也许就能写完了,马老师肯定会更生气,但这不是最重要的,最重要的是我得能在t分钟内写完。

当这样不断的二分查找到最后时,就达到了一种 状态

  • 能在t分钟内完成
  • 尽量让空白区间最大值越小
  • 在能完成作业的情况下,尽量多的写一些,马老师的愤怒值最小

2、细节问题 如果i选中,那么它前面最远的可以是哪个?此处与烽火传递不太一样,那个是 在连续 m 个烽火台中至少要有一个发出信号,翻译一下就是连续m个中必须有一个,也就是空白区最多是m-1个,此题是空白区是limit个,比那个大1

Code

#include <bits/stdc++.h>

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]

指定区间内的最小值问题,可以使用单调队列来优化:

Q:答案在哪里?

需要在n \sim n-limit之间去枚举找出最小值做为答案。

举个栗子秒懂: n=10,limit=3,那么如果7被选中,那么8,9,10是不用写的。

#include <bits/stdc++.h>

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;
}