【题解】cf1381c Mastermind
序
(一道很考验思维质量的构造好题,而且需要注意的细节也很多。)
本题解主体使用的是简洁且小常数的\(O(nlogn)\)时间复杂度代码,并且包含其他方法的分析留给读者自行实现(其实是自己不会写或者写崩了)。
后记有\(O(n)\)时间复杂度的反向优化。
题意
共t组数据,每组数据第一行是n,x,y,其中n表示数列规模。接下来一行是n个数表示数列,要求输出任意一个数列,它与原数列满足:
有x个数字位置相同且大小相同
打乱原数列顺序后,最多能有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\)必然有解
但需要注意的是:
仅当频数超过\(\lfloor {{y+z} \over 2} \rfloor\)上式才有可能\(>0\),故最多只有有一种颜色存在这样的风险。
并不是所有\(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\n");return;}
构造转移
这是一个卡了我很久的点(主要是我看了ltao的题解发现他直接一笔带过就很离谱)
我们费尽功夫选好了\(y+z\)个点,判断出了有无解,可真到了构造转移的时候应该怎么处理。
显然前面没选的\(x\)个数字原地tp即可,不在话下。
对于选好的\(y+z\)个数字,最初的想法如下:
- 颜色集中分布,按颜色频数从小到大排序。(\(logn\))
- 对于频数最大的颜色,我们优先处理它的转移终点,若\(maxcnt*2-(y+z) \le 0\)则不必动用舍弃名额,直接 在剩余颜色里面按频数从小到大选就可以;否则就舍弃\(maxcnt*2-(y+z)\)个。
- 对于剩余颜色,按频数从大到小处理,每种颜色的转移终点从比频数比它大的里面选(可以用queue存储频数比它大的位置),由于上一步是从小到大选的故这一步必然有解。
- 如果有剩余的舍弃名额就随便舍。
正确性显然,但代码实现繁琐,留给读者自行思考。
然后在这里分析一下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\n");
for(ri i=1;i<=n;++i)
{
put(b[i] ? b[i] : a[i]);
pc(' ');
}
pc('\n');
后记
在实现了上述代码并成功\(AC\)后,你发现代码中有两个\(sort\)格外显眼:
- 给二维vector按照一维的size排序(选数阶段)
- 给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\n");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的常数还是很优的
你之所以看见的,正是因为你想看见。
——萨特《自由之路》
【题解】cf1381c Mastermind的更多相关文章
- 贪心/构造/DP 杂题选做
本博客将会收录一些贪心/构造的我认为较有价值的题目,这样可以有效的避免日后碰到 P7115 或者 P7915 这样的题就束手无策进而垫底的情况/dk 某些题目虽然跟贪心关系不大,但是在 CF 上有个 ...
- 2016 华南师大ACM校赛 SCNUCPC 非官方题解
我要举报本次校赛出题人的消极出题!!! 官方题解请戳:http://3.scnuacm2015.sinaapp.com/?p=89(其实就是一堆代码没有题解) A. 树链剖分数据结构板题 题目大意:我 ...
- noip2016十连测题解
以下代码为了阅读方便,省去以下头文件: #include <iostream> #include <stdio.h> #include <math.h> #incl ...
- BZOJ-2561-最小生成树 题解(最小割)
2561: 最小生成树(题解) Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 1628 Solved: 786 传送门:http://www.lyd ...
- Codeforces Round #353 (Div. 2) ABCDE 题解 python
Problems # Name A Infinite Sequence standard input/output 1 s, 256 MB x3509 B Restoring P ...
- 哈尔滨理工大学ACM全国邀请赛(网络同步赛)题解
题目链接 提交连接:http://acm-software.hrbust.edu.cn/problemset.php?page=5 1470-1482 只做出来四道比较水的题目,还需要加强中等题的训练 ...
- 2016ACM青岛区域赛题解
A.Relic Discovery_hdu5982 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/65536 K (Jav ...
- poj1399 hoj1037 Direct Visibility 题解 (宽搜)
http://poj.org/problem?id=1399 http://acm.hit.edu.cn/hoj/problem/view?id=1037 题意: 在一个最多200*200的minec ...
- 网络流n题 题解
学会了网络流,就经常闲的没事儿刷网络流--于是乎来一发题解. 1. COGS2093 花园的守护之神 题意:给定一个带权无向图,问至少删除多少条边才能使得s-t最短路的长度变长. 用Dijkstra或 ...
随机推荐
- 操作系统识别-python、nmap
识别操作系统主要是用于操作系统漏洞的利用.不管是windows还是linux系统,在安装完毕后都会默认启动一些服务,开启一些端口. 识别目标主机的系统最简单的方法就是发送ping包,windows起始 ...
- 微信小程序之页面跳转(tabbar跳转及页面内跳转)
一.简介 微信小程序页面主要分为tabbar页面和应用内页面,这两种页面的跳转方式不同 二.tabBar页面跳转 tabBar 是底部导航栏页面,如下图 在app.json中的配置如下: 跳转方式如下 ...
- Redis持久化机制,优缺点,如何选择合适方式
一.什么是Redis持久化? 持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失. 二.Redis 的持久化机制是什么?各自的优缺点? Redis 提供两种持久化机制 RDB(默认) 和 ...
- 学写PEP,参与Python语言的设计
如果你为Python写了一篇PEP,这篇PEP成功的被Python指导委员会接受了,那么以后你在吹牛皮的时候你就可以说我主导了Python语言某个特性的设计工作. -- 跬蟒 我就问你主导Python ...
- python计算图像信息熵
import cv2 import numpy as np import math import time def get_entropy(img_): x, y = img_.shape[0:2] ...
- 关于阿里云服务器Linux安装Tomcat后,外网不能访问解决方案
这里需要提及三个方面的问题 第一个方面:Linux上启动防火墙的问题 当下比较流行的Linux镜像是CentOS,所以防火墙也随之变成了firewall,那么怎么操作这个防火墙呢? #停止fi ...
- pycharm一直显示Process finished with exit code 0
后来排查发现原来是解释器的问题我之前使用的解释器是pycharm提供的虚拟解释器#####如何查看解释器点file–>new projects 如果选择的是2就是使用了pycharm提供的虚拟解 ...
- 每日一题 - 剑指 Offer 30. 包含min函数的栈
题目信息 时间: 2019-06-24 题目链接:Leetcode tag:栈 难易程度:简单 题目描述: 定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 m ...
- mysql高可用架构MHA搭建(centos7+mysql5.7.28)
无论是传统行业,还是互联网行业,数据可用性都是至关重要的,虽然现在已经步入大数据时代,nosql比较流行,但是作为数据持久化及事务性的关系型数据库依然是项目首选,比如mysql. 现在几乎所有的公司项 ...
- Java基础笔记01-02-03-04
一.今日内容介绍 1.Java开发环境搭建 2.HelloWorld案例 3.注释.关键字.标识符 4.数据(数据类型.常量) 01java语言概述 * A: java语言概述 * a: Java是s ...