CF 1093 Div

CF 1093 (Div.2) A~E1题解

比赛链接

A

这个题有两种做法。

做法1(暴力)

考虑新建两个大根堆,一个存储已经大于 \(c\) 的数,另一个存储小于等于 \(c\) 的数。

由于删除第二个堆中的数已经对答案没有任何帮助,反正这些最后都要删掉,所以只需要删掉小于等于 \(c\) 的数中的最大值就行了,剩下的拿出来乘 \(2\),然后放进去。

时间复杂度 \(O(Tn^2logn)\),显然能过。

做法2(转化)

注意到每次暴力操作会很慢,不如转化一下,转化成每次把 \(c\) 变成 \(\lceil \frac{c}{2} \rceil\),然后每次删除 \(\leq c\) 的最大值。

瓶颈在排序,复杂度 \(O(Tnlogn)\) 显然更快。

或许有人会疑问,为什么是上取整啊?

请看这个例子:

上取整 \(c\) 3 2 1
下取整 \(c\) 3 1 0
实际情况 1 2 4

如果是下取整的话,它就会被判断为花费 \(1\),但是上取整的判断是正确的\

代码就不给出了,非常简单。

B

这个题要求我们从一个数列的左右删除一个数,使得删除的数依次排成一个没有 \(5\) 个及以上连续上升/下降的序列。

感性理解。

尝试构造一个高低不平的序列,像这样:

注意现在只是理想状态,不一定能达到。

直接看代码应该能看懂:

#include <iostream>
#include <algorithm>
using namespace std;

const int MAXN = 200010;
int a[MAXN];
char ans[MAXN];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int t;
    cin >> t;
    while (t--) {
        int n;
        cin >> n;
        for (int i = 1; i <= n; i++) {
            cin >> a[i];
        }
        int l = 1, r = n;
        int last = 0;
        int op = 1;
        int cnt = 0;
        while (l <= r) {
            if (op == 1) {
                if (max(a[l], a[r]) > last) {
                    if (a[l] > a[r]) {
                        ans[cnt++] = 'L';
                        last = a[l];
                        l++;
                    } else {
                        ans[cnt++] = 'R';
                        last = a[r];
                        r--;
                    }
                    op = 0;
                } else {
                    if (a[l] < a[r]) {
                        ans[cnt++] = 'L';
                        last = a[l];
                        l++;
                    } else {
                        ans[cnt++] = 'R';
                        last = a[r];
                        r--;
                    }
                }
            } else {
                if (min(a[l], a[r]) < last) {
                    if (a[l] < a[r]) {
                        ans[cnt++] = 'L';
                        last = a[l];
                        l++;
                    } else {
                        ans[cnt++] = 'R';
                        last = a[r];
                        r--;
                    }
                    op = 1;
                } else {
                    if (a[l] > a[r]) {
                        ans[cnt++] = 'L';
                        last = a[l];
                        l++;
                    } else {
                        ans[cnt++] = 'R';
                        last = a[r];
                        r--;
                    }
                }
            }
        }
        ans[cnt] = '\0';
        cout << ans << '\n';
    }
    return 0;
}

C

我们构造几个数据进行观察。

首先对于n=1的数组,显然可以构造出来。

再看n=2的数组。

  • 如果两个元素相等,可以2步构造出来。比如{3, 3},第一次x=3,得到{3, 0},第二次x=3,得到
  • 如果a[1] > a[2],显然也可以构造出来。比如{3, 2},第一次x=3,得到{3, 0},第二次x=2,得到

现在来看a[1] < a[2]的情况,什么时候可以构造出来。

我们可以逐个枚举

{1, 2} // no
{1, 3} // no

{2, 3} // yes: {2, 0} -> {2, 1} -> {2, 3}
{2, 4} // no

{3, 4} // yes: {3, 0} -> {3, 1} -> {3, 4}
{3, 5} // yes: {3, 0} -> {3, 2} -> {3, 5}
{3, 6} // no

根据上面,我们猜测a[2] < 2*a[1]时,是可构造的。我们研究出一个构造套路

{y, y+y-1} // yes: {y, 0} -> {y, y-1} -> {y, y+y-1}

而当a[2] >= 2*a[1]时,此时显然不可解。

推广到任意长度,则有 mn < a[i] < mn*2-1,其中mn为前缀最小值。

const int N = 200010;

int n;
int a[N];
void solve() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
    }
    bool f = 1;
    int mn = a[1];
    for (int i = 2; i <= n; ++i) {
        if (a[i] >= mn) {
            if (a[i] >= mn + mn) {
                f = 0;
                break;
            }
        } else {
            mn = a[i];
        }
    }
    printf("%s\n", f ? "YES" : "NO");
}

D

暴力肯定不行,很显然是 \(O(n^3)=O(TLE)\)

考虑这个数列是一个下降的数列。

那么它的每一个子序列都是下降的。

答案就是考虑每一个元素对于答案的贡献(出现在多少个子序列中)

而题目中给出的条件:

\[max(a_i,a_{i+1}) \gt a_{i+2} \]

那么就不可能出现三个连续上升的元素。

所以答案就是假设所有的元素下降的答案扣掉含有上升一次(形如 \(a_i \lt a_{i+1}\))的段的个数,因为长度会 \(-1\)

总贡献为 \([1,i] * [i,n]\)

扣掉的形如 \(a_i \lt a_{i+1}\) 的贡献为 \([1,i] * [i+1,n]\)

代码在下方。

const int maxn = 500010;
#define ll long long

int n;
int a[maxn];
void solve() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
    }
    ll ans = 0;
    for (int i = n; i >= 1; --i) { // 总贡献
        ans += 1LL * i * (n - i + 1);
    }

    for (int i = 1; i < n; ++i) {
        if (a[i] < a[i+1]) { // 减去包含[i,i+1]的,不符合预期的所有区间
            ans -= 1LL * i * (n - i);
        }
    }
    printf("%lld\n", ans);
}

E

看到中位数,想起来 二分排序

对于每个 \(\texttt{submedian}\),考虑二分 \(v_{max}\)

把所有小于 \(v_{max}\) 的数标记为 \(-1\),剩下的标记为 \(1\)

考虑取长度 \(\geq k\) 的子段和。如果子段和大于等于 \(0\),那么这个可取。

有人会问,为什么不是等于 \(0\)

因为我们的二分是在不断缩小范围,最后一定会到最优解。

刚刚的子段和问题,是个标准的双指针。

时间复杂度 \(O(nlogn)\)

const int maxn = 300010;
#define ll long long

int n, k;
int a[maxn];
int pre[maxn];
bool check(int val, int &ansl, int &ansr) {
    pre[0] = 0;
    for (int i = 1; i <= n; ++i) {
        pre[i] = pre[i-1] + (a[i] >= val ? 1 : -1);
    }

    // j表示历史最小pre对应的下标 l-1
    for (int j = 0, i = k; i <= n; ++i) {
        if (pre[j] > pre[i-k]) {
            j = i - k;
        }
        if (pre[i] >= pre[j]) {
            ansl = j + 1;
            ansr = i;
            return true;
        }
    }

    return false;
}
void solve() {
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
    }
    int l = 1, r = n;
    int mid;
    int res = 1;
    int pos1, pos2;
    while (l <= r) {
        mid = (l + r) / 2;
        if (check(mid, pos1, pos2)) {
            res = mid;
            l = mid + 1;
        } else {
            r = mid - 1;
        }
    }
    printf("%d %d %d\n", res, pos1, pos2);
}
posted @ 2025-07-30 14:18  guoguo160  阅读(24)  评论(0)    收藏  举报