1 条题解
-
0
自动搬运
来自洛谷,原作者为

FZzzz
**搬运于
2025-08-24 21:28:39,当前版本为作者最后更新于2019-06-16 15:26:53,作者可能在搬运后再次修改,您可在原文处查看最新版自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多
以下是正文
原题解锅了,这里整个重写一遍。
以下内容写于 2022 年 8 月 5 日。
论还有半个月就退役的我在这里水黄题。
三年前还是普及组选手的我做过这题,当时看不懂题解,自己整了个线性做法,然而没有正确性证明。
三年后这篇题解成为了我赞最多的博客,然而我发现它锅了。并且我毫不意外地还是看不懂其他题解。
现在我们来修这个锅。
下面令 表示题面里的 。
我们有两种操作:入栈和出栈。入栈这个操作会对栈产生影响,但不会对结果序列产生影响,这很不利于我们的分析。
我们希望每一步都能使结果序列后面多一个数,那么我们把两种操作改成:
- 出栈一个数。
- 连续入栈至少一个数,然后出栈一个数。
这和原来的两种操作是等价的。
执行第二种操作时,我们要入栈多少个数呢?由于我们要最小化的的是字典序,所以我们肯定贪心地想要让入栈的最后一个数,即出栈的那个数最小。
令 为 中第一个还没有被入栈的数的位置, 表示我们最多可以一次性入栈 个数,也就是说最多可以把下标在 中的所有数都入栈。如果当前栈中有 个元素,我们可以算出 。
那么,如果我们这一步入栈 个数,再出栈一个数即 ,我们想要 是 到 中最小的数。这样我们就可以知道,这一步如果执行第二种操作,需要连续入栈多少个数了。
比较严谨的小朋友就要问了,最小的 可能不止一个呀?等会我们再讨论这个问题。
但是,有两种操作,我们应该执行哪一个呢?不难发现,我们应该比较 和当前的栈顶元素。如果 更小,则执行第二种操作,否则执行第一种操作。
严谨的小朋友又要问了,如果两个数相等呢?我们还是等会再来讨论这个问题。
可能光说比较难以理解,我们来手玩一下样例:
6 3 5 2 3 8 7 4- 当前栈为空,,,。执行第二种操作,入栈两个数再出栈一个数即 。
- 当前栈为 ,,,。由于 小于栈顶 ,执行第二种操作,入栈一个数再出栈一个数 。
- 当前栈为 ,,,。由于 大于栈顶 ,执行第一种操作,出栈一个数 。
- 当前栈为空,,,。执行第二种操作,入栈三个数再出栈一个数 。
- 当前栈为 (右边是栈顶),(用 表示原序列中没有数没有被入栈了),。执行第一种操作,出栈一个数 。
- 当前栈为 ,执行第一种操作,出栈一个数 。
最终得到的结果序列是 ,跟样例输出是一样的。
如果没有相同的元素,问题就已经被解决了。真可惜,我们还得回过头来讨论上面提出的两种相等的情况。
如果最小元素不止一个?
猜测一下,我们选择最左边的那个。但是为什么呢?
考虑利用反证法证明这个结论:假设有两个数 和 满足 ,,我们假设选 可以得到更优的结果。那么,我们开辟一条新的世界线,考虑在这个世界线里选择 并进行一次操作二。
接着,我们把下标 的数全部入栈(不出栈)。这样,我们的情况相比于原世界线执行完这次操作二之后的情况,只是栈里的 跨越了一些不小于它的数跑到了栈顶。
这种情况显然是不劣于原世界线的。因为,我们之后可以对原世界线的每一次操作,都在新世界线里执行一样的操作。容易证明结果序列不劣于原来的世界线。
于是我们导出了假设的矛盾!根据反证法,我们可以证明结论,即我们应该选择左边的 而非右边的 。
如果 等于栈顶元素?
那么我们断言直接进行出栈操作是不劣的。证明和上面几乎一样,留给读者作练习。
下面的话留给普及组选手:
如果你不会证明,你可以手玩一些数据来猜测并验证结论。比如下面两个数据就对应了上面两种情况。
3 3 1 2 14 3 2 1 3 2但是!不要觉得这样证明就没有用了。你可能听说过“OIer 不需要证明”的说法,但是这种想法是绝对错误的。
不是所有结论都可以手玩或者感性理解,凭借直觉去猜结论经常会猜错。所以一定要养成多问证明的好习惯,即使在考场上你可能不会严谨证明每个结论。
……好吧,其实我是一个直觉很差的选手,而且每个结论我都需要一个严谨到每个字的证明。这对我平时做题和考试都造成了不小的负面影响。所以可能我是错的,我这种傻逼的建议听听就好。
等等!我们怎么找到这个 呢?当然可以暴力或者使用
std::multiset进行优化,得到 或者 的时间复杂度并通过此题。但是,不是说有线性做法吗?可以证明, 和 都是单调不减的。这样左右端点都只会往一个方向移动的区间,我们称之为滑动窗口。我们要做的就是维护这个窗口内最靠左的最小值。
我们使用单调队列线性解决这个问题。这里有一道模板题,也可以去看看这题的题解。如果你已经会了这个数据结构,你可以跳过这一段。
单调队列基于这样一个事实:如果有两个位置 和 ,满足 且 ,那 就永远没用了。因为 会更晚从窗口里被移出去,而且还更小。
我们用一个双端队列维护窗口内所有有用的数,队列里从队头到队尾下标递增,那么值是不降的。每次我们要移动窗口的左端点时,就把队头的一些元素出队。
而我们要移动右端点时,就入队一些元素。每次入队会使队尾的一些元素变得没用,要在入队之前把它们从队列里移出去。
这样,队头就是当前窗口内最靠左的最小值。那么这个做法的时间复杂度是多少呢?每个元素只会入队一次,出队一次,所以是 的。
那么我们在线性时间内完整解决了整个问题。由于输入就需要这么多时间,我们做到了理论最优的时间复杂度。
这里把单调队列讲一遍纯粹是因为原来写的题解讲了一遍。#include<bits/stdc++.h> using namespace std; using ll=long long; inline ll read(){ ll x=0; bool f=0; char c=getchar(); while(!isdigit(c)){ if(c=='-') f=1; c=getchar(); } while(isdigit(c)){ x=x*10+c-'0'; c=getchar(); } return f?-x:x; } const int maxn=1e4+5; int n,m,a[maxn],q[maxn],hd=1,tl=0; int main(){ #ifdef LOCAL freopen("in.txt","r",stdin); freopen("out.txt","w",stdout); #endif n=read(); m=read(); for(int i=1;i<=n;i++) a[i]=read(); for(int i=1;i<=m;i++){ while(hd<=tl&&a[i]<a[q[tl]]) tl--; // 必须是 a[i]<a[q[tl]] 而不是 <= q[++tl]=i; }// 入队最开始 m 个元素 int l=1,r=m; vector<int> s;// 栈 for(int i=1;i<=n;i++){ if(hd<=tl&&(!s.size()||a[q[hd]]<s.back())) // 比较两种操作,必须是 < 而不是 <= while(l<=q[hd]) s.push_back(a[l++]); // 入栈一堆元素 printf("%d ",s.back()); s.pop_back();// 出栈并输出 while(r<n&&r<l+m-(int)s.size()-1){// 移动 r r++; while(hd<=tl&&a[r]<a[q[tl]]) tl--; q[++tl]=r;// 入队 } while(hd<=tl&&q[hd]<l) hd++;// 移动 l,出队 } #ifdef LOCAL fprintf(stderr,"%f\n",1.0*clock()/CLOCKS_PER_SEC); #endif return 0; }以下是碎碎念。
事情是这样的。这篇题解被 hack 了,我发现好像在有相等元素时会寄。想了一下修好了,但是不太可能直接在原有题解上修,于是决定重写。
很久没有写黄题的题解了,我也不知道普及组选手喜欢什么样的。太简略的题解肯定是看不懂的,但是我当时好像也嫌弃一些题解总是啰里吧嗦讲一大堆有的没的。但是没办法,只能按怎么详细怎么来了。
这应该是我博客里现有的最早的文章,也是赞最多的一篇。我会认真对待它,希望对你有帮助。
莫名其妙写了这么长,比我很多黑题的题解都长多了。就到这里吧。
- 1
信息
- ID
- 725
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 3
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者