在相思树下 III 题解

前言

题目链接:洛谷

赛时脑子坨成一坨了,估计是 T1 的影响,写一篇题解来理清思路。

题意简述

给你一个长为 \(n\) 的序列 \(a_{1\dots n}\),你需要对它进行两种操作共 \(n-1\) 次。

对一个长度为 \(l\) 的序列 \(b_{1\dots l}\) 进行一次操作将会把序列变为一个长为 \(l-1\) 的序列 \(c_{1\dots l-1}\)

  • 操作一中,\(\forall i\in[1,l),c_i=\max(b_i,b_{i+1})\)
  • 操作二中,\(\forall i\in[1,l),c_i=\min(b_i,b_{i+1})\)

给定整数 \(m\),你只能进行至多 \(m\) 次操作一。进行 \(n-1\) 次操作后序列 \(a\) 的长度变为 \(1\)。你可以任意安排操作的顺序,求最终剩余的数 \(a_1\) 的最大值。

题目分析

方法 \(1\):二分 + 差分

很容易想到二分,把序列中 \(\geq mid\) 的统统看成 \(1\),反之是 \(0\),问题变成了最后留下的那一个数是不是 \(1\)

我们不需要考虑 \(1\) 之间,\(0\) 之间的相互操作,应为无论是 \(\min\)\(\max\)\(\geq mid\) 的永远不会 \(< mid\),反之亦然。所以,我们把序列分成若干极长的 \(\tt 01\) 段,考虑这两个操作对两段交界处的影响。

对于取 \(\max\),会把 \(\tt 01\) 变成 \(\tt 11\);对于取 \(\min\),会把 \(\tt 10\) 变成 \(\tt 00\)。由于我们关心 \(1\),所以,前者是把所有极长的 \(1\) 段左边界向左扩展,后者是对所有极长的 \(1\) 段右边界向左收缩。

考虑扩展和收缩的先后顺序。在答案的操作序列中,对于相邻的两个操作,把先收缩再扩展的,变成先扩展再收缩一定不劣。原因是,先收缩可能会完全消除某一个长度为 \(1\) 的段,但是后者不会。邻项交换证明得,答案就是先扩展,再收缩。显然,多用扩展一定不劣。所以,我们确定了答案的操作序列。

先扩展 \(m\) 次,再收缩 \(n - m - 1\) 次,相当于我们对于初始的极长 \(1\) 段的左端点 \(l\),把 \([l - m, l - 1]\) 覆盖成 \(1\),最后看看 \([1, n - m]\) 是否均为 \(1\)。这些操作直接差分就行了。因为我们只用知道那些地方一次都没被覆盖过。

时间复杂度:\(\Theta(n \log V)\),当然离散化后 \(V = \mathcal{O}(n)\)

方法 \(2\):单调队列

能否优化呢?我们可以从另一种思路来分析。感觉是贪心,考虑答案操作序列的相邻两个操作,是先取 \(\max\) 还是先取 \(\min\)。不妨列出式子,对于之前连续的三个数 \(a, b, c\),操作两次后得到的 \(a'\) 分别为 \(\max \lbrace \min \lbrace a, b \rbrace, \min \lbrace b, c \rbrace \rbrace\)\(\min \lbrace \max \lbrace a, b \rbrace, \max \lbrace b, c \rbrace \rbrace\)。然后凭借你的数学直觉……额,看不出来……没关系!枚举 \(1 \sim 3\) 的全排列来看看:

\(a, b, c\) \(\min\)\(\max\) \(\max\)\(\min\)
\(1, 2, 3\) \(\max \lbrace \min \lbrace 1, 2 \rbrace, \min \lbrace 2, 3 \rbrace \rbrace = 2\) \(\min \lbrace \max \lbrace 1, 2 \rbrace, \max \lbrace 2, 3 \rbrace \rbrace = 2\)
\(1, 3, 2\) \(\max \lbrace \min \lbrace 1, 3 \rbrace, \min \lbrace 3, 2 \rbrace \rbrace = 2\) \(\min \lbrace \max \lbrace 1, 3 \rbrace, \max \lbrace 3, 2 \rbrace \rbrace = 3\)
\(2, 1, 3\) \(\max \lbrace \min \lbrace 2, 1 \rbrace, \min \lbrace 1, 3 \rbrace \rbrace = 1\) \(\min \lbrace \max \lbrace 2, 1 \rbrace, \max \lbrace 1, 3 \rbrace \rbrace = 2\)
\(2, 3, 1\) \(\max \lbrace \min \lbrace 2, 3 \rbrace, \min \lbrace 3, 1 \rbrace \rbrace = 2\) \(\min \lbrace \max \lbrace 2, 3 \rbrace, \max \lbrace 3, 1 \rbrace \rbrace = 3\)
\(3, 1, 2\) \(\max \lbrace \min \lbrace 3, 1 \rbrace, \min \lbrace 1, 2 \rbrace \rbrace = 1\) \(\min \lbrace \max \lbrace 3, 1 \rbrace, \max \lbrace 1, 2 \rbrace \rbrace = 2\)
\(3, 2, 1\) \(\max \lbrace \min \lbrace 3, 2 \rbrace, \min \lbrace 2, 1 \rbrace \rbrace = 2\) \(\min \lbrace \max \lbrace 3, 2 \rbrace, \max \lbrace 2, 1 \rbrace \rbrace = 2\)

容易发现,由于大的数在取最值得时候没取到,前者是不优的。这也恰好对应了我们方法 \(1\) 中的结论。

所以,依然得出了先弄 \(m\) 次操作一,再弄 \(n - m - 1\) 次操作二。

在前 \(m\) 次操作后,对于一个数 \(a_i\),它最终的值便是 \(\max \lbrace a_{i \dots i + m} \rbrace\),使用 ST 表,线段树都行,或者滑动窗口用单调队列。对于后 \(n - m - 1\) 次操作同理,不过我们只要知道 \(a_1\),所以求一遍 \(a_{1 \dots n - m}\)\(\min\) 即可。

时间复杂度:\(\Theta(n)\)

代码

方法 \(1\):二分 + 差分

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

const int N = 1000010;

int n, m, val[N];

int yzh[N];

inline bool check(int x) {
    for (int i = 1; i <= n; ++i) yzh[i] = (val[i] >= x) - (val[i + 1] >= x);
    for (int i = n; i >= 1; --i) {
        if (val[i] >= x && val[i - 1] < x)
            --yzh[max(0, i - m - 1)], ++yzh[i - 1];
        yzh[i] += yzh[i + 1];
    }
    for (int i = 1; i <= n - m; ++i) if (yzh[i] <= 0) return false;
    return true;
}

signed main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &val[i]);
    int l = 1, r = 1e9, ans = 736520;
    while (l <= r) {
        int mid = (l + r) >> 1;
        if (check(mid)) ans = mid, l = mid + 1;
        else r = mid - 1;
    }
    printf("%d", ans);
    return 0;
}

方法 \(2\):单调队列

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

const int N = 1000010;

inline void read(int &x) {
	x = 0; char ch = getchar();
	for (; !isdigit(ch); ch = getchar());
	for (; isdigit(ch); x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar());
}

int n, m, val[N];
int ans = 0x3f3f3f3f;

int Q[N], head, tail;

signed main() {
    read(n), read(m);
    for (register int i = 1; i <= n; ++i) read(val[i]);
    
    head = 0, tail = -1;
    for (register int i = n; i >= 1; --i) {
        while (head <= tail && Q[head] > i + m) ++head;
        while (head <= tail && val[Q[tail]] <= val[i]) --tail;
        Q[++tail] = i;
        if (i <= n - m) ans = min(ans, val[Q[head]]);
    }
    
    printf("%d", ans);
    return 0;
}

后记

遇到贪心操作的题目,考虑答案的操作序列,然后对这个操作序列邻项交换。

二分答案,能够成为答案的和不能成为答案的,看做 \(1\)\(0\),分别考虑。

posted @ 2024-08-17 19:17  XuYueming  阅读(6)  评论(1编辑  收藏  举报