[省选联考 2021 A/B 卷] 卡牌游戏 做题笔记
发表于|更新于
|阅读量:
前言
非常有趣的一道双指针好题,前前后后思考了好几天并写了一天半,终于实现了题解大佬的线性做法,做完这道题也让我对双指针以及函数单调性有了更深的理解。目前已经在所有 oj 上都 AC 了,正确性应该是没有问题的。这篇做题笔记思路与该大佬的题解基本一致,在细节方面有一些补充。
思路
这道题的切入点就是观察最终序列的翻转方式。
性质 1
观察可得性质 1:
证明 1
由于给出的 a 是升序的,所以翻转中间的卡片要么对最小值和最大值无影响(因为中间的 ai 一定 ≥a1,≤an),要么会使最小值更小/使最大值更大,而我们要求的是最小极差,所以翻转中间的卡片没有意义。
思考怎么用这个性质去解题。
设已经翻转了长度为 l 的一段前缀和长度为 r 的一段后缀,可以定义如下变量:
- lmaxl=max{bi}(1≤i≤l);
- lminl=min(min{bi}(1≤i≤l),al+1);
- rmaxr=max(max{bi}(n−r+1≤i≤n),an−r);
- rminr=min{bi}(n−r+1≤i≤n)。
此时序列的最小值为 min(lminl,rminr),最大值为max(lmaxl,rmaxr)。
性质 2
下面切入性质 2:
- lmax 和 lmin 单调不减,rmax 和 rmin 单调不增。
证明 2
lmax 和 rmin 显然单调,但剩下两个量并不单调。
现在就到了这个做法的精妙之处:我们可以对 lmin 和 rmin 进行一些处理,使它们变得单调,这样就方便用二分或者双指针之类对单调性有要求的算法来解题;而这个处理就是对 lmin 取前缀最大值,对 rmax 取前缀最小值。
假设存在一个位置 l′,满足 l′<l 且 lminl′>lminl。
因为 lmax 单调不减,所以 lmaxl′≤lmaxl,
故此时在左边翻转 l′ 个数一定比翻转 l 个更优。
同时由于 l′<l,满足 l+r≤m 的 r 也可以用在 l′ 上,所以对 lmin 取前缀最大值既满足最优性,又满足合法性。
对 rmax 的证明同理。
处理完之后就可以直接开始通过双指针解题。
考虑枚举 l,r 和当前的最小值,于是我们需要枚举两次,第一次枚举 l 并将 lminl 作为当前最小值,第二次枚举 r 并将 rminr 作为当前最小值,这样可以确保不重不漏。在枚举 l 的过程中,我们的目标就是找到最大并满足 rminr≥lminl 和 l+r≤m 的 r(因为 rmax 单调不增,所以rmax 也一定最小)来更新答案,而枚举 r 时要找的就是满足条件的最小的 l。
关于枚举顺序的问题,以 l 为例:l 减小时,lminl 减小,因为 rminr≥lminl 所以 rminr 可以适当减小,r 可以适当增大,于是 rmax 减小,就取到了对当前 l 最优的 r,所以 l 要倒序枚举,如果正序枚举,则最后 rmax 会变大,取到的解就不是最优的。r 的枚举顺序同理可证。
现在这题我们就做完了,代码很短也很好写,但是细节非常多。一定要在各个 oj 上都交一遍,因为这题能卡掉错解的数据貌似不是很好构造。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| constexpr int N = 1e6+5;
int a[N],b[N],lmax[N],lmin[N],rmax[N],rmin[N],n,m;
void solve(){ read(n);read(m); for(int i=1;i<=n;i++) read(a[i]); for(int i=1;i<=n;i++) read(b[i]); lmin[0] = rmin[0] = inf; for(int i=1;i<n;i++){ lmin[i] = min(lmin[i-1],b[i]); lmax[i] = max(lmax[i-1],b[i]); rmin[i] = min(rmin[i-1],b[n-i+1]); rmax[i] = max(rmax[i-1],b[n-i+1]); } for(int i=0;i<n;i++){ lmin[i] = min(lmin[i],a[i+1]); rmax[i] = max(rmax[i],a[n-i]); if(i>0){ lmin[i] = max(lmin[i],lmin[i-1]); rmax[i] = min(rmax[i],rmax[i-1]); } } int ans = inf; for(int l=m,r=0;l>=0;l--){ while(lmin[l]<=rmin[r+1] && l+r<m) r++; while(l+r>m && r) r--; if(lmin[l]>rmin[r]) continue; int nowmax = max(lmax[l],rmax[r]); ans = min(ans,nowmax-lmin[l]); } for(int r=1,l=m-1;r<=m;r++){ while(l>0 && rmin[r]<=lmin[l-1] || l+r>m) l--; if(rmin[r]>lmin[l]) continue; int nowmax = max(lmax[l],rmax[r]); ans = min(ans,nowmax-rmin[r]); } cout<<ans; }
|