• 【题解】cf1381c Mastermind


    (一道很考验思维质量的构造好题,而且需要注意的细节也很多。)

    本题解主体使用的是简洁且小常数的(O(nlogn))时间复杂度代码,并且包含其他方法的分析留给读者自行实现(其实是自己不会写或者写崩了)。

    后记有(O(n))时间复杂度的反向优化。

    题意

    共t组数据,每组数据第一行是n,x,y,其中n表示数列规模。接下来一行是n个数表示数列,要求输出任意一个数列,它与原数列满足:

    1. 有x个数字位置相同且大小相同

    2. 打乱原数列顺序后,最多能有y个元素位置相同且大小相同。

    转义

    为方便分析,这里对题目进行转义,我们认为新数列是由原数列变换位置后得到的,并重新定义下列字母:

    • (x)表示位置不变的数字个数
    • (y)表示新旧位置在原数列的对应位置上,颜色不同的数字个数
    • (z)表示舍弃的数字个数。
    	n=read();
    	x=read();
    	y=read()-x;
    	z=n-x-y;
    

    解释一下舍弃:存在可以舍弃掉的数字,其原因在于颜色总值域为[1,n+1],故必然存在原数组颜色值域以外的颜色(记为(off)),(x)(y)按规则变换位置后,剩余位置填上(off)即可。

    体现在原数组里面就相当于有 (z)个元素被舍弃。

    分析

    (x)是位置不变,(z)是直接舍弃,显然这二者都不涉及到有无解的问题。

    (y)要求新旧位置必须颜色不同,这一点可能无法满足,所以我们重点分析这(y)个数字的选取和转移。

    而且非常容易被错误理解的一点是:在选定(y)个数字后,我们的转移终点并不一定必须也在这(y)个数字的位置上。显然(x)的位置是无法作为终点的,但(z)反正被舍弃,其留下的空位置便可以作为终点

    定性选取

    不难发现:在所选的(y+z)个数字中,频数最大的颜色频数越小,越容易有解。

    通俗来讲就是颜色分布越多样、均匀,越容易有解。

    因为如果某一种颜色过多,就可能导致(y)没有足够的转移终点。

    因此第一步就是贪心地去均匀选数,可以用堆选取(x)个数字使得剩余数字的颜色的最大频数尽可能小((nlogn)),也可以直接开个二维vector如下均匀选取(排序也要(nlogn)

    struct color
    {
    	int col,id;
    	color(int C=0,int I=0):col(C),id(I){}
    };
    
    struct cmp1
    {
    	bool operator () (const vector<color> &x,const vector<color> &y)
    	{
    		return x.size()>y.size();
    	}
    };
    
    	n=read();
    	x=read();
    	y=read()-x;
    	z=n-x-y;
    	
    	//将输入存储于二维vector中 
    	for(ri i=1;i<=n;++i)a[i]=read();
    	
    	vector<vector<color> > vec(n+1);
    	
    	for(ri i=1;i<=n;++i)
    		vec[a[i]-1].push_back(color(a[i],i));
    	
    	sort(vec.begin(),vec.end(),cmp1());
    	while(!vec.back().size())vec.pop_back();
    	
    	//尽可能多样地选取y+z个col 
    	vector<color> arr;
    	
    	for(ri i=1,p=0;i<=y+z;++i)
    	{
    		arr.push_back(vec[p].back());
    		vec[p].pop_back();
    		if(++p==vec.size())
    		{
    			while(vec.size() && !vec.back().size())vec.pop_back();
    			p=0;
    		}
    	}
    

    定量判断

    可对于选出的(y+z)个数字,我们如何定量判断是否无解?

    记其中颜色的最大频数为(maxcnt)

    则它们还需要颜色不同的(maxcnt)个终点进行转移

    故若(maxcnt*2-(y+z) le 0)必然有解

    但需要注意的是:

    1. 仅当频数超过(lfloor {{y+z} over 2} floor)上式才有可能(>0),故最多只有有一种颜色存在这样的风险。

    2. 并不是所有(maxcnt)个数字都必须有转移终点 ,我们完全可以舍弃其中的(z)个数字不作转移,但它们留下的空位置并不能作为剩余同颜色数字的转移终点,故若(0 < maxcnt*2-(y+z) le z)则也有解。

      (如果不容易理解可以画图:在长为(y+z)的区间上,(maxcnt*2)相当于顶着区间两端各放一条线段,减去(y+z)的区间总长,得到的是两线段的重叠部分,这便是必须舍弃的部分)

    综上:若(maxcnt*2-(y+z) le z)则有解,反之无解。

    	//统计个数,获得各颜色个数排名
    	int maxcnt=0,off;
    	
    	for(ri i=1;i<=n+1;++i)cnt[i]=0;
    	for(ri i=0;i<arr.size();++i)++cnt[arr[i].col];
    	
    	for(ri i=1;i<=n+1;++i)
    	{
    		if(cnt[i])maxcnt=max(maxcnt,cnt[i]);
    		else off=i;
    	}
    	if(maxcnt*2-(y+z)>z){printf("NO
    ");return;}
    

    构造转移

    这是一个卡了我很久的点(主要是我看了ltao的题解发现他直接一笔带过就很离谱

    我们费尽功夫选好了(y+z)个点,判断出了有无解,可真到了构造转移的时候应该怎么处理。

    显然前面没选的(x)个数字原地tp即可,不在话下。

    对于选好的(y+z)个数字,最初的想法如下:

    1. 颜色集中分布,按颜色频数从小到大排序。((logn)
    2. 对于频数最大的颜色,我们优先处理它的转移终点,若(maxcnt*2-(y+z) le 0)则不必动用舍弃名额,直接 在剩余颜色里面按频数从小到大选就可以;否则就舍弃(maxcnt*2-(y+z))个。
    3. 对于剩余颜色,按频数从大到小处理,每种颜色的转移终点从比频数比它大的里面选(可以用queue存储频数比它大的位置),由于上一步是从小到大选的故这一步必然有解。
    4. 如果有剩余的舍弃名额就随便舍。

    正确性显然,但代码实现繁琐,留给读者自行思考。


    然后在这里分析一下ltao的构造思路(确实很妙,我也加入了一些自己的理解):

    首先注意在上一步的代码中我们已经将arr按颜色集中分布。

    记录两个指针(i,j),分别表示转移的终点和起点。(注意顺序)

    初始化(i=0,j=maxcnt),令它们有(maxcnt)的初始间隔,见下例(没选的(x)个已省略,(y=5,z=2)

    arr.col​ 1 2 2 2 2 3 3
    指针i​ i​
    指针j​ j​
    b​

    (i,j)对应颜色不同,则设置转移并记录在新数列中,由于初始间隔的存在,不难证明当(i)(j)任意一者对应颜色不是频数最大的颜色时,必然可以转移,即(i,j)同时向右移动(注意(j)(arr.end())时的取模)即可;

    而需要特殊处理的是(i,j)对应颜色相同的情况,如下例:

    arr.col​ 1 2 2 2 2 3 3
    指针i​ i​
    指针j​ j​
    b​ 2 3 3 1

    此时我们保持起点(j)不动,继续寻找终点(i)直至可行(可见,途径不可行的都是被舍弃的终点):

    arr.col​ 1 2 2 2 2 3 3
    指针i​ i​
    指针j j​
    b​ 2 3 3 1 4​ 2

    直到每一个终点都被尝试过,结束算法。可见由于(j)的停滞,同样存在一些被舍弃的起点

    arr.col​ 1 2 2 2 2 3 3
    指针i​ i​
    指针j​ j​
    b​ 2 3 3 1 4​ 2 2

    该算法核心就在于初始间隔(maxcnt)的设置能够很好地处理非最大频数颜色的转移;而面对最大频数颜色的冲突,我们也可以通过(j)的停滞来舍弃最大频数颜色末尾的部分数字,以保证有解。(啥也别说了都快去%ltao

    upd[2020.7.23]:如果还是不太懂(有可能是因为luogu表格炸了看不清点击以获取更好的阅读体验),就体会一下ltao的原话:

    前面你判了一定有解对吧

    所以之后只需要排序 然后 肯定会有解的

    所以 你只要把这个排好序的序列平移 一半(即maxcnt)

    注意 这个平移还是要留下之前的索引

    然后可以保证排好序的数组至少ok

    那么,按照你存的索引直接找回去就OK

    这应该是ltao最初从宏观角度思考的算法思路了,还不明白可以看代码

    struct cmp2
    {
    	bool operator () (const color &x,const color &y)
    	{
    		return cnt[x.col]!=cnt[y.col] ? cnt[x.col]>cnt[y.col] : x.col>y.col;
    	}
    };
    
    	//填入y+z个元素的b
    	for(ri i=1;i<=n;++i)b[i]=0;
    	sort(arr.begin(),arr.end(),cmp2());
    	
    	for(ri i=0,j=maxcnt,sz=arr.size();i<sz;++i)
    	{
    		if(j<maxcnt+y && arr[i].col!=arr[j%sz].col)
    			b[arr[i].id]=arr[(j++)%sz].col;
    		else
    			b[arr[i].id]=off;
    	}
    	
    	//输出b
    	printf("YES
    ");
    	for(ri i=1;i<=n;++i)
    	{
    		put(b[i] ? b[i] : a[i]);
    		pc(' ');
    	}
    	pc('
    ');
    

    后记

    在实现了上述代码并成功(AC)后,你发现代码中有两个(sort)格外显眼:

    1. 给二维vector按照一维的size排序(选数阶段)
    2. 给arr按照颜色集中分布的排序(转移阶段)

    这个2很好处理,因为我们转移时,没有必要特意按频数排序,只要颜色集中就可以(看见我上面给的那个样例故意没排序了吗),这一点可以在统计颜色的时候处理。

    	//统计个数,获得maxcnt,并将arr按颜色集中分布 
    	int maxcnt=0,off;
    	
    	vector<vector<color> > tarr(n+1);
    	while(arr.size())
    	{
    		tarr[arr.back().col-1].push_back(arr.back());
    		arr.pop_back();
    	}
    	
    	for(ri i=0;i<n+1;++i)
    	{
    		if(tarr[i].size())
    		{
    			maxcnt=max(maxcnt,(int)tarr[i].size());
    			while(tarr[i].size())
    			{
    				arr.push_back(tarr[i].back());
    				tarr[i].pop_back();
    			}
    		}
    		else off=i+1;
    	}
    	
    	if(maxcnt*2-(y+z)>z){printf("NO
    ");return;}
    

    至于1确实不易实现,因为会有size相同的一维vector,所以我们再给它开个桶(给桶桶排的桶应该叫啥?桶的桶?二阶桶?

    	//将输入存储于二维vector中 
    	for(ri i=1;i<=n;++i)a[i]=read();
    	
    	vector<vector<color> > vec(n+1);//二维vector 桶 
    	vector<vector<vector<color> > >tax(n);//三维vector 桶的桶(笑) 
    	
    	for(ri i=1;i<=n;++i)
    		vec[a[i]-1].push_back(color(a[i],i));
    	
    	while(vec.size())//这桶排写得属实烧脑 
    	{
    		if(vec.back().size())
    			tax[vec.back().size()-1].push_back(vec.back());
    		vec.pop_back();
    	}
    	for(ri i=n-1;i>=0;--i)
    	{
    		while(tax[i].size())
    		{
    			vec.push_back(tax[i].back());
    			tax[i].pop_back();
    		}
    	}
    

    然后踌躇满志地提交,你会发现:

    上文(O(nlogn))提交记录

    线性(O(n))提交记录

    ”喂这什么情况啊这波怎么成了反向优化了“

    “废话分析分析常数不就知道了”

    事实证明sort的常数还是很优的

  • 相关阅读:
    恭喜发财
    狗腿子的一天
    向系统分析员爬进
    解决SqlTransaction用尽的问题
    Localhost与数据库连接
    《成都,今夜请将我遗忘》读后感
    如何快速掌握一门技术
    怎样做一个iOS App的启动分层引导动画?
    怎么去掉Xcode工程中的某种类型的警告
    iOS 8 AutoLayout与Size Class自悟
  • 原文地址:https://www.cnblogs.com/-SingerCoder/p/13367166.html
Copyright © 2020-2023  润新知