Codeforces Round 1031 (Div. 2)

A是简单题,但是太久没写还是写的太慢

B其实简单摸一下就能够发现是怎么样的了,但是我vp的时候还是想的过于复杂,其实按照最简单的画图的思路来就可以最快速的搞定。

C和A一个难度的贪心。

重要的是这个D。说实话,非常夸张。这题除了二分没有任何的数据结构和算法知识,纯粹依靠思维难度达到了2200 。
我看到题解只有4行的时候挺不敢相信的。这也是说明了这题确实可以说是一个好题。

题意:

你在玩一种新的纸牌游戏,规则如下:

  1. 用一副有 2n 张不同点数卡牌的牌组。
  2. 牌组均分给玩家和庄家,各得 n 张。
  3. 进行 n 轮,每轮双方同时出一张顶牌,点数大的一方得 1 分,赢的牌移除游戏,输的牌放回出这张牌的玩家手中并置于其手牌顶。

已知庄家手牌的卡牌顺序(从上到下),你最多能交换自己手牌里的两张牌(不超过一次),要算出能拿到的最大分数 。 简单说就是:基于规则,利用最多一次换牌,求玩家对庄家能拿到的最高得分。

我vp的时候得到的一个最重要的思路就是,手上最小的牌会一直卡住。说是比较所有牌的大小,其实我们可以直接比较前缀最小值的大小。
(其实我考场想到的是单调栈,因为维护前缀最小值可以用单调栈。也侧面说明我掌握的知识还是过于套路狭隘了)

而单调其实是没错的。单调意味着我们其实有一些更快的手段求出答案,能用二分。
我们可以二分:cherk(x)表示我当前情况下第x张牌是否能够得到属于我自己的分数。
由于我们可以很快的二分找出这个cherk(x)的结果,所以是成立的。

这里插入一个部分:那么从这里来看,其实还有一个更重要的部分需要想到。也就是我们其实能够通过直接比较当前位置前缀最小值是上面大还是下面大来直接得到某一张确定的牌会成为谁的分数。这一点其实非常非常奇妙(也就是难以发现,这里需要后续的总结分析)。因为假如我们模拟这个过程,可以发现我们其实是没法确定某一张确定的牌是会和哪些牌比较的。它可能会和相对位置很前面的比较,也可能会和相对位置很后面的比较。

那么我们现在应该是能够完成一个\(O(log_n)\)的求解答案。只需要去二分卡片得分的位置即可。

接下来要处理的其实就是怎么去考虑我们的那一次操作。那其实已经非常非常明显了。我们肯定是去找我们后面那些没有机会得分的比较大的数字去替换到前面。
替换的目标就是前缀最小值里面的变化点。

但是还是不对。因为当我们替换了数字之后,我们并不知道替换后的数字是否会再次影响答案。而且前缀最小值的转折点是\(O(n)\)级别的,直接枚举很可能会Tle。

正解还是延续二分答案。我们直接钦定我们的得分。这样我们就确定了我们卡牌的使用范围,同时也知道了使用范围里面的最小值,和使用范围外的最大值。

其实能够发现,这样的情况下,后面的最大值和前面的最小值交换100%最优秀。否则就一定可以通过调整范围来达到可能存在的更优秀的解,这是可以反证法证明的。

所以这题的思路也就完全出现了。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline int read(){
	int a=0,b=1;char c=getchar();
	for(;c<'0'||c>'9';c=getchar())if(c=='-')b=-1;
	for(;c>='0'&&c<='9';c=getchar())a=a*10+c-'0';
	return a*b;
}
int n,a[400001],b[400001],prea[400001],preb[400001];
bool cherk(int x)
{
	int i,j,cnt;
	for(i=1,j=1,cnt=0;cnt<n;cnt++)
	{
		// cout<<a[i]<<' '<<b[j]<<' ';
		if(a[i]<b[j])
		{
			// cout<<i<<' '<<j<<endl;
			j++;
		}
		else 
		{
			// cout<<i<<' '<<j<<endl;
			i++;
		}
	}
	// cout<<i<<endl;
	if(i<=x)return 0;
	else return 1;
}
int main()
{
	int T=read();
	while(T--)
	{
		n=read();
		prea[0]=preb[0]=2*n;
		for(int i=1;i<=n;i++)
		{
			a[i]=read();
			prea[i]=min(prea[i-1],a[i]);
		}
		for(int i=1;i<=n;i++)
		{
			b[i]=read();
			preb[i]=min(preb[i-1],b[i]);
		}
		// cout<<cherk(n)<<endl;
		// read();
		int l=1,r=n+1;
		while(l<r)
		{
			int mid=l+r>>1;
			int Min=2*n,Minid;
			int Max=0,Maxid;
			for(int i=1;i<=mid;i++)
			{
				if(a[i]<Min)
				{
					Min=a[i];
					Minid=i;
				}
			}
			for(int i=mid+1;i<=n;i++)
			{
				if(a[i]>Max)
				{
					Max=a[i];
					Maxid=i;
				}
			}
			if(Max>Min)
			swap(a[Minid],a[Maxid]);
			// cout<<mid<<' ';
			if(cherk(mid)) l=mid+1;
			else r=mid;
			if(Max>Min)
			swap(a[Minid],a[Maxid]);
		}
		cout<<l-1<<endl;
	}
	return 0;
}

对于D的反思和总结

我对于这题主要想要分析两个部分,也是我认为设计的极为巧妙的部分。

1.我们能够通过直接比较当前位置前缀最小值是上面大还是下面大来直接得到某一张确定的牌会成为谁的分数

我认为这一点主要来自于这个特殊的博弈设计,我也可以尝试设计一点类似的

2.这个钦定答案范围的二分。

我已经遇到很多次这种类型的二分答案了,这应该是二分答案特长之一,只要把这个特点隐藏好,就可以让题目难度直接飙升。就比如这题。这题就是单调性找到了,但是并不就能意识到需要用二分。因为直接模拟就能够得到答案,我为什么要花费精力从这个不那么明显和好写的二分上面去吧计算答案使用的\(O(n)\)模拟优化为\(O(log_n)\)的二分呢?有什么明显收益吗?没有啊。所以自然不会去做。但是结果就是思路可以从这里来。这也是为什么说这题好,设计的太特喵巧妙了,把找到这些思路的端倪全部藏起来了。如果不够敏锐,就做不出来。

相较之下,我觉得这题的思路,需要先意识到用二分。二分主要也就是来自于单调性。

其实二分答案能够做到的一件挺特殊的事情,我也一直都没有分析过,就是钦定答案范围来凭空创造条件。其实和很多证明的方法很像,本质上来说就是反证法。做到这件事情需要一个特殊的单调性,这个单调性和答案范围的变化相关。我们先假设答案是这个范围内的,然后以此为条件计算答案。如果最后计算出来的答案不满足这个条件,那就说明这不是答案。而此时就需要用到这个特殊的单调性,来调整我们初始的假设和答案的范围。以此为基础形成了一个二分答案。其实把后面的二分答案的部分去掉,直接设计的时候就不加入这个特殊的单调性,本身也能成为一个比较麻烦的题目。因为这个反证法类似的钦定答案范围后验证的思想还没有成为一个常见的思路,可以作为一个trick来出题甚至。记录一下。

唉。我好想采访一下星丝老师做这题怎么做的。

我要到什么时候才能仅凭感觉秒这题啊。

posted @ 2025-06-24 05:19  Tracer_w  阅读(52)  评论(0)    收藏  举报