CF1821D Black Cells 题解 贪心

发布时间 2023-04-23 18:14:54作者: quanjun

题目链接:https://codeforces.com/problemset/problem/1821/D

题目大意

在一条数轴上有无穷个点,下标为 \(0, 1, 2, \ldots\),初始时每个点都是白色的。

你控制着一个机器人,初始时机器人位于坐标为 \(0\) 的那个点。

机器人有两种状态:激活状态 和 非激活状态。

当处于激活状态时,机器人所在的点都会被染成黑色。

处于非激活状态时,机器人所在的点不会被染成黑色。

初始时机器人处于非激活状态。

你可以对这个机器人进行若干次操作,操作分为三种类型。每一次操作,你可以选择如下三种操作之一执行:

  • 将机器人的位置像数轴的正方向移动一个单位(即:若当前机器人在坐标 \(x\),则执行一次该操作后机器人将移动到坐标 \(x+1\) 的那个点);
  • 激活机器人:该操作只有当机器人处于非激活状态时才能执行,执行该操作后机器人将变为 激活状态;
  • 撤销激活机器人:该操作只有当机器人处于激活状态时才能执行,执行该操作后机器人将变为 非激活状态。

\(n\) 个区间,第 \(i\) 个区间用 \([l_i, r_i]\) 表示。

你需要使用最少的操作次数,将至少 \(k\) 个点染成黑色,但是有一个限制,就是:这些染成黑色的点必须包含在至少一个给定的区间中,这也就是说,如果你要将坐标为 \(x\) 的那个点染成黑色,则必须保证存在一个 \(i(1 \le i \le n)\) 满足 \(l_i \le x \le r_i\)

同时,本题也要求操作结束时机器人恢复到非激活状态(这也就意味着最少操作次数对应的最后一次操作是 撤销激活机器人)。

问:至少需要进行几次操作能够使至少 \(k\) 个点被染成黑色,且最终机器人处于非激活状态?

解题思路

首先是考虑从前往后枚举\(i\) 个线段。

考虑只对前 \(i\) 个线段里面的点进行染色。那么会出现两种情况:

  • 情况1:前 \(i\) 个线段里面可以被染色的点的个数 \(\lt k\) 个 —— 此时因为点的数量不够,所以不做讨论;
  • 情况2:前 \(i\) 个线段里面可以被染色的点的个数 \(\ge k\) 个,此时需要重点分析。

当前 \(i\) 个线段里面可以被染色的点的个数 \(\ge k\) 个时,我们还需要考虑 —— 最优解有没有给第 \(i\) 个线段中的点染色?

如果最优解没有给第 \(i\) 个线段中的点染色,则该情况可以直接转移到前 \(i-1\) 个线段的情况(因为我们时枚举前 \(i\) 个线段的,枚举前 \(i\) 个线段之前肯定已经枚举了前 \(i-1\) 个线段,所以这种情况实际上已经分析过了)。

所以此时的情况肯定是给第 \(i\) 个线段中的点染色的。

首先,对于前面的所有线段,只要是选择染色的,将前面这些线段中的点全部都染色肯定不会差,因为:

对于一个线段,只要有一个点选择染色了,那么肯定具有 激活(原题中的 shift)和 撤销激活(原题中的 release shift)操作了,此时再对这个线段中的其它点进行染色的额外操作此时是确定的,多染一个点只需要多操作一次。

所以,对于前 \(i-1\) 个线段:

  • 要么选择全都不染(这个线段弃用掉);
  • 要么选择全都染(然后第 \(i\) 个线段把剩余的个数补齐)。

所以此时需要考虑的是前 \(i-1\) 个线段中哪些需要弃用掉。

我一开始有一种错误的贪心思想,就是在保证前 \(i\) 个线段中保留的线段对应的点的个数 \(\ge k\) 的情况下,尽量多删除前面的长度小的线段,但是发现这种错误思想是错误的。举个反例:

1
2 3
1 3
2 5

此时:当 \(i = 2\) 时,弃用掉第一段的最少操作次数为 \(7\);不弃用第 \(1\) 段的操作此时是 \(6\)

只有长度为 \(1\) 的线段才有可能被弃用。

为什么呢,因为在考虑前 \(i\) 个线段并且第 \(i\) 个线段是需要激活的情况下,是肯定需要移动到 \(l_i\) 的,而 \(r_{i-1} \lt l_i\),对于一个长度 \(\ge 2\) 的线段(假设为第 \(j\) 个线段)\([l_j, r_j]\),这些点段中的点都是要移动到的,当线段中的点数 \(\ge 2\) 时,只需要两步额外的操作(激活/shift 和 撤销激活release shift)就能够使这个线段中的所有点都染色(这不是很香嘛),至少不会比在第 \(i\) 个线段中向右移动一格要花更多的操作次数。

而对于长度为 \(1\) 的线段,移动也是需要移动的,所以不考虑移动的开销。但是如果给这一个点染色需要额外操作两次(激活/shift 和 撤销激活release shift)。而后面的第 \(i\) 个线段只需要多移动依次就能多一个点染色。所以对于长度为 \(1\) 的点,弃用它们更优。

所以,只有长度为 \(1\) 的线段才有可能被弃用。

但是弃用归弃用,对于每个枚举到的 \(i\),当前 \(i\) 个线段的对应的所有可以染色的点的数量已经 \(\ge k\) 时,不能启用到剩余点的个数 \(\lt k\) —— \(\lt k\) 了就不是一个可发的答案了。

综上所述,就是枚举前 \(i\) 个线段,然后把前面长度为 \(1\) 的线段能弃用的都弃用了。那么此时,假设:

  • 没有被弃用的线段对应的点的数量为 \(sum\)
  • 没有被弃用的线段的数量是 \(cnt\)

则:考虑到从坐标 \(0\) 移动到 \(r_i\),并且没有弃用的线段都染色了,那么:

  • 移动带来的花费是 \(r_i\)
  • 每个线段激活和撤销激活的花费是 \(cnt \times 2\)

所以给前 \(i\) 个线段中没有弃用的线段中的点都染色的花费是 \(r_i + cnt \times 2\)

但是,因为我只需要给 \(k\) 个点染色就可以了,但是我却选择给了 \(sum\) 个点染色(\(sum \ge k\)),那么在最后一个线段中,我其实可以减少 \(sum - k\) 个点的染色(减小 \(sum - k\) 次向右的移动),此时答案的一个候选项是:

\(r_i + cnt \times 2 - (sum - k)\)

而我的答案就是所有合法的 \(r_i + cnt \times 2 - (sum - k)\) 的最小值。

示例程序

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 5;

int T, n, k, l[maxn], r[maxn];

int cal() {
    int ans = -1, cnt = 0, sum = 0, cnt1 = 0;
    for (int i = 1; i <= n; i++) {
        sum += r[i] - l[i] + 1;
        cnt++;
        cnt1 += (l[i] == r[i]);
        if (sum >= k) {
            int x = min(sum-k, cnt1);
            sum -= x;
            cnt1 -= x;
            cnt -= x;
            int tmp = r[i] + cnt * 2 - (sum - k);
            if (ans == -1 || ans > tmp)
                ans = tmp;
        }
    }
    return ans;
}

int main() {
    scanf("%d", &T);
    while (T--) {
        scanf("%d%d", &n, &k);
        for (int i = 1; i <= n; i++) scanf("%d", l+i);
        for (int i = 1; i <= n; i++) scanf("%d", r+i);
        printf("%d\n", cal());
    }
    return 0;
}