10-2国庆节第四场模拟赛题解
T1 电灯 (light)
Description
有 n 个灯泡排成一列。每个灯泡可能是点亮或熄灭的。有一台操控灯泡的机器,每一次可以选择一段区间,让这段区间中熄灭的灯泡全部点亮,亮着的灯泡全部熄灭。但由于机器已经老化,仅能再使用一次了。
你可以认为点亮的灯泡与熄灭的灯泡交替排列的样子(下面称这样的灯泡列为交替列)很好看。现在,你希望珍惜最后一次操控灯泡的机会,使得操控后这列灯泡中最长的交替列尽可能地长。
例如,这列灯泡若原本如下所示(○ 表示点亮的灯泡,● 为熄灭的灯泡):
○ ○ ● ● ○ ● ○ ○ ○ ●
如果选择第 4 个到第 7 个灯泡,则会变成如下的形式:
○ ○ ● ○ ● ○ ● ○ ○ ●
此时,最长的交替列为第 2 个到第 8 个灯泡,长度为 7。
而如果仅选择第 88 个灯泡,则会变成如下的形式:
○ ○ ● ● ○ ● ○ ● ○ ●
此时,最长的交替列为第 4个到第 10 个灯泡,长度也为 7。
可以发现,此例中没有方法能使得最长交替列长度大于 77,则 77 即为答案。
Input
输入文件第一行一个正整数 n,表示灯泡的数量。第二行包含 n 个数字,每个数字均为 0 或 1,依次代表序列中每个灯泡的初始状态。1 代表点亮,0 代表熄灭。
Output
输出一个整数,表示所有能得到的灯泡列中最长的交替列的长度。
第一道题的意思就是初始有一个01串,约定01相间的区间是美的,并且我们仅有一次机会将某个区间内的所有值翻转,即0变成1,1变成0。问题是找出翻转后的最长美的区间的长度。
elov大佬在讲题的时候是这么说的:对于类似这种给你一个01串,然后让你关于01相间这个要求求一些东西的题,再没读完题的时候就应该想到要把所有偶数位(或者奇数位)的01数字翻转,这样的话,再去求关于01相见的东西时候,就是可以转化成求相同的区间了。
仔细想一想确实这样,记得一道题:棋盘制作,也是可以通过上述方法将问题转化,使之变成更加好求的问题。
这道题的思路:在输入过程中,将所有偶数位上的数字全部翻转,在对转化后的序列进行操作。
先求出这个序列中有多少个连续相同的小区间,只需要统计这些小区间的长度,再去枚举每一个区间。
像这样:110001100011 当枚举到中间1的小区间时,我们将这一整个小区间翻转,那么可以得到的答案就是t[now-1]+t[now]+t[now+1],也就是反转中间区间,会将该区间前面的区间和后面的区间与反转之后的当前区间共同组成一个新的比较大的区间,再通过预处理出的区间长度,可以做到O(1)地查询当前答案并更新。
这样 ,整体的复杂度就是O(n)的。
之后按照这个思路去实现了一遍,忽然发现在考场上感觉挺难的题,立马变水了。
在考场上还是应该尽力沉下心来读题,沉下心来思考。
code:
#include<iostream>
#include<cstdio>
using namespace std;
inline int read(){
int sum=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
sum=(sum<<1)+(sum<<3)+ch-'0';
ch=getchar();
}
return sum*f;
}
int n,ans,tot,flag;
int a[999999],t[999999];
int main(){
n=read();
for(int i=1;i<=n;i++){
if(i&1)a[i]=read();
else {
(a[i]=read())^=1;
}
if(a[i]!=a[i-1])flag=1;
}
if(!flag)ans=n;
for(int i=1;i<=n;i++){
if(a[i]==a[i-1])t[tot]++;
else t[++tot]=1;
}
for(int i=1;i<=tot;i++){
ans=max(ans,t[i-1]+t[i]+t[i+1]);
}
printf("%d\n",ans);
return 0;
}
T2:粘贴 (copy)
Description
从前有一个 \({1, 2, \dots , n}\) 的排列,你希望用剪切/粘贴操作,将这个排列变成 \(1,2, \dots , n\)。
一次剪切/粘贴操作指的是把序列中某段连续的子区间整体向前或者向后平移一段距离。由于你认为同时按 Ctrl+X 很累手指,所以想要知道最少的操作次数。
Input
输入文件包含多组数据。
每组数据有两行,其中第一行包含一个正整数 n,第二行给出一组 \({1, 2, ..., n}\) 的排列。
以一个 0 来结束输入。
Output
对于每组数据,输出一行一个整数,表示最小的操作次数。
第二道题的话,还是要先%%%ZAGER大佬,搜索王实至名归(雾),全场就他一个人A掉了这道题,(好像也就只有他一个人得分?忘记了。。),而且他还是用的非常牛皮的IDA*,他说在当时讲课的时候,他有讲过这道题,而且真的在当时的ppt上找到了这道题,可能是因为当时不认真吧,唉,后悔。。。
这道题的题意是这样的:给你一个乱序的序列,由1到n的数字不重复组成。现在要求最少的步数通过平移某个小区间将这个序列转成严格上升的,也就是1,2,3……n这么排列。
数据范围是很友好的,elov良心啊,本来想着可以开心地乱搞了,可就是因为这道题把自己整的很颓废,整场考试差点垮掉。
先说一下IDA*吧,ZAGER大佬在给我们讲题的时候,提到了自己对IDA *的理解。
ZAGER:IDA*=迭代加深+估价函数h();
个人感觉十分精辟,毕竟搜索菜鸡什么都不懂。
1,迭代加深:就是每一次给出一个深度,作为搜索步数的上界,深度从小到大给出,所以如果答案就是操作次数的话,迭代加深就可以保证第一次搜到的答案就是最小的,因为相当于从小到大枚举答案。
为什么要这么做?
因为针对于一部分搜索题,如果你开始搜到了错误的状态,是非常致命的,也就是说,如果在跑大法师的过程中搜到了一个错误状态并且还很深,那么基本上就GG了。那么对于这种情况,我们用类似广搜的思想来思考。就是在搜索的过程中,相当于我们是在从一棵搜索树的根节点不断向外扩展新的状态,一层一层,越来越多。那么为了避免我们钻进一棵子树再也上不来的情况,可以钦定一个深度,一旦当前的搜索深度到了这个深度并且还没有搜到,那么我们就选择放弃这棵子树,去到另外的子树也就是去搜索别的状态。这样即使刚开始几层的状态我们会重复搜到,也是可以有效解决搜不到底的问题的。
2,估价函数:个人感觉这东西太牛皮了,但是ZAGER大佬教导我的时候说这东西其实就是我们平时做题的一种思想,只不过把它叫做估价函数我就懵逼了。
想想还真是。
估价函数起到一个剪枝的作用。
联系迭代加深,我们可以知道搜索的总步数是给定的。
那么可以试想,当我们搜到一个状态的时候,已经用过的步数是已知的,一共可以用的步数也是已知的(就是迭代加深的最大深度),更重要的一点是,我们知道目标状态是什么,知道当前状态是什么,那么就可以估算出从当前状态到目标状态的代价也就是要用的步数,如果当前步数加上估算出的预期步数大于总步数,那么说明这种情况肯定不优或者说搜不到答案,直接返回就行,相当于一个剪枝。
为什么是估算?
很重要的一点就是,估价函数是我们就题论题分析出的,是指在最优决策下,从当前状态到达目标状态的期望步数,所以并不准确,但必须保证在最优状态下。因为如果在最优状态下都不能从当前状态到目标状态,我们就可以没有顾虑毫不犹豫地进行剪枝了。
对这道题分析,因为数据奇小,所以不用担心复杂度(有了IDA*就不用担心唯一会使我们超时的情况也就是搜不到底了)。又经过分析,我们可以得出只要在(n+1)/2步以内,又因为n<=9,所以答案最多是5.都会是任意一个错序序列变成目标序列,elov大佬说可以用分治证明,不过他并没有证,,,只是举了一个最坏情况下的例子
给出初始序列为:5 4 3 2 1
可以这么变换: 3 2 5 4 1 *1
3 4 1 2 5 *2
1 2 3 4 5 *3 也就是说在n=5的最坏情况下是可以3步解决问题的。
那么我们就可以优化,将迭代加深可以到的最大深度设为5,这里又有一个神奇的地方,就是我们可以将最大深度设为4,这时候如果还没有找到答案,那么不用往下搜也可以知道答案是5了,(分析问题真的很重要。。。,瑟瑟发抖)。
之后每一次变换,就可以先枚举一个小区间(左右端点),之后再枚举这个区间插到哪里就可以了。(真的是有了IDA*随便怎么乱搞都不怕。。。)
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
inline int read(){
int sum=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
sum=(sum<<1)+(sum<<3)+ch-'0';
ch=getchar();
}
return sum*f;
}
const int wx=199;
int n,m;
int a[wx],t[wx];
bool goal(){
for(int i=1;i<n;i++){
if(a[i]!=a[i+1]-1)return false;
}
return true;
}
int h(){
int tot=0;
for(int i=1;i<n;i++){
if(a[i+1]!=a[i]+1)tot++;
}
if(a[n]!=n)tot++;
return tot;
}
bool dfs(int d,int maxd){
if(h()+d*3>maxd*3)return false;//估价函数 大剪枝 很重要的一点:改变一个数最多会消掉3的不和谐值。
if(goal())return true;//判断是否到达目标状态
int b[wx],olda[wx];
memcpy(olda,a,sizeof a);
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){//i,j为枚举的区间左右端点
int cnt=0;
for(int k=1;k<=n;k++){
if(k<i||k>j)b[++cnt]=a[k];//将不在i,j之间的数用中转数组b存一下
}
for(int k=1;k<=cnt;k++){//枚举i,j插在哪一个位置,暴力改变a数组即可
int cnt2=0;
for(int l=1;l<=k;l++)a[++cnt2]=b[l];//k是枚举i,j这个区间插入到第k个数后面。
for(int l=i;l<=j;l++)a[++cnt2]=olda[l];
for(int l=k+1;l<=cnt;l++)a[++cnt2]=b[l];
if(dfs(d+1,maxd))return true;
memcpy(a,olda,sizeof olda);//恢复a数组
}
}
}
return false;
}
int slove(){
if(goal())return 0;
int max_ans=5;
for(int i=1;i<max_ans;i++){
if(dfs(0,i))return i;//迭代步数
}
return max_ans;//分析问题真的很重要。。。
}
int main(){
freopen("copy.in","r",stdin);
freopen("copy.out","w",stdout);
while(1){
n=read();
if(!n)break;
for(int i=1;i<=n;i++){
a[i]=read();
}
int ans=slove();
printf("%d\n",ans);
}
return 0;
}
对于第二题 [UVa 11212 编辑书稿 ,查阅借鉴了一位大佬的思路,表示感谢并挂上链接。
https://blog.csdn.net/jc514984625/article/details/51785439
T3:图 (graph)
Description
如今大大小小的电信运营商如雨后春笋般成立,通信市场变得十分混乱,甚至各运营商的手机卡已经不都支持给所有运营商的号码发送短信了。
好在你拥有一个双卡手机,可以同时使用两个不同电信运营商的手机卡。当你想向某个号码发送短信时,可以通过两家运营商中可用的或收费更少的一家。请你挑选两家运营商,购买他们的手机卡,使手机能向所有运营商的号码发送短信,而且发送一条短信的最大费用最低。
注意,归属于同一家运营商的号码之间不一定能够互发短信。
Input
输入文件第一行包含两个正整数 n 和 k,其中 n 是手机运营商的数目。
接下来 k 行,每行包含 x,y,c 三个整数,表示可以花费c 元通过运营商 x 发送一条短信给归属于运营商 y 的号码。
Output
输出一个整数,表示在最优的购卡方案下发送一条短信的最大费用。
如果不论怎么选择运营商都无法满足条件,输出
No solution
。
这道题严格意义上说,个人感觉并不是一道图论题。挺考思维的,考场上一发\(O(n^3)\)暴力骗到了20分。
其实这道题一定是要二分答案的,考场上也想到了但是没有打出来。。(这话好没用啊喂)
提前声明,鉴于这道题如果用链式前向星实现去重边一系列的操作烦的一批,所以我借鉴了eolv大佬的std。
vector在存边方面,对解决去重边这方面的问题简直有奇效。
具体说一下,对于建一条从x到y的边,写法是
vector<int > v[wx];
v[x].push_back(y);
那么如何去重边?STL是真的棒!
枚举每一个点,对于他的vector进行一次unique去重,注意要进行一遍sort,然后再定义一个迭代器指向去重后第一个不合法元素的位置,直接用erase()将iter到v[i].end()全部去掉就行了。
for(int i=1;i<=n;i++){
sort(v[i].begin(),v[i],end());
vector<pair<int,int> > iterator iter=unique(v[i].begin(),v[i].end());
v[i].erase(iter,v[i].end());
}
在做完上述工作也就是把每个点的出边中的重边全部去掉之后,继续考虑如何解决问题。
因为我们要二分答案,所以二分的是边权。为了满足单调性我们需要把边按照边权排序,所以自然需要将所有的边拿一个东西存起来,这里eolv大佬用的还是vector,我个人认为数组也是可以的。
之后二分哪一条边作为答案,再将其边权传进判断函数,把它当做一个边的上界就可以去判断每条边的合法性了。
这里有两种判断的方法,一种是针对n小的时候,一种是针对n大的时候。
elov大佬是证过的,但是我还是没有记住具体的复杂度。。。
说一下两种判断的思路:
1,因为要选两个点,所以两层枚举点是必须的,那么我们就枚举两个点,判断他们的不超过当前二分的val的边所连到的点的集合是否包含了所有的点即可。
怎么实现呢?我们需要维护每个点在val的限制下满足条件的出边所能连到的点的集合。再去将这两个集合求交集,机房大佬说可以用并查集实现,不过eolv大佬十分不屑,给我们引入了一个超级牛皮的东西-bitset,先来简单介绍一下这东西的一些基础用法:
头文件声明 : #include
变量定义: bitset<2005> b[2005];注意尖括号里面要写的是数字。
bitset只能存储0或1,并且使用一位来存储,因为普通的bool数组存0或者1使用一字节也就是八位存储的,所以和bitset相比较,是浪费了七倍的空间的。
并且这还不是这道题的重点。
bitset有一个非常好用的性质就是我们可以直接把两个bitset进行按位的位运算,如:
b1[5]=00110 b2[5]=11001 那么我们可以直接进行 b1|b2的操作得到一个新的bitset存的是11111
对于这道题,我们就可以直接想到对于每一个点开一个bitset,用0或1来表示这个点对于其它点的到达情况,最后直接求交集里的1的个数就行了。
bitset简单操作:
- b.set(i) 是指将b这个bitset里的第i位变成1
2)b.count()返回的是b这个bitset里面1的个数
那这道题就很神奇的变简单了。
首先我们O(m)地预处理出在当前边权上界的限制下,每个点对于其它点到达的情况。即枚举每一个点,在枚举这个点的出边,判断边权与上界的关系即可。
之后就要开始枚举两个点,这里有一个优化,就是如果当前枚举的点的出边数是小于n/2的,那么我们选择这个点基本上没戏,所以就跳过。
如果当前枚举的两个点对于所有点的到达情况的交集全都是一的话,就说明可行,返回true。
2,基本思路与1差不多,但是1中的复杂度n是在分母位置上的,所以当n很小时是很爆炸的。
#include<iostream>
#include<cstdio>
#include<vector>
#include<bitset>
#include<algorithm>
#include<cstring>
using namespace std;
typedef pair<int,int> prz;
inline int read(){
int sum=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
sum=(sum<<1)+(sum<<3)+ch-'0';
ch=getchar();
}
return sum*f;
}
const int wx=10017;
int n,m,x,y,z;
bitset<2005> b[2005];
vector<pair<int,int> > v[wx];
int vis[wx];
bool ok(int val){
if(n<=2000){
for(int i=1;i<=n;i++){
b[i]=0;
for(int j=0;j<(int)v[i].size();j++){
if(v[i][j].second<=val){
b[i].set(v[i][j].first);
}
}
}
for(int i=1;i<=n;i++){
if((int)v[i].size()*2>=n){
for(int j=1;j<=n;j++){
if((int)(b[i]|b[j]).count()==n){
return true;
}
}
}
}
}
else{
memset(vis,0,sizeof vis);
for(int i=1;i<=n;i++){
if((int)v[i].size()*2>=n){
int cnt=n;
for(int j=0;j<(int)v[i].size();j++){
if(v[i][j].second<=val){
vis[v[i][j].first]=i,cnt--;
}
}
for(int j=1;j<=n;j++){
if((int)(v[i].size()+v[j].size())<n)continue;
int _cnt=cnt;
for(int k=0;k<(int)v[j].size();k++){
if(v[j][k].second<=val&&vis[v[j][k].first]!=i)_cnt--;
}
if(!_cnt)return true;
}
}
}
}
return false;
}
int main(){
// freopen("graph.in","r",stdin);
// freopen("graph.out","w",stdout);
n=read();m=read();
for(int i=1;i<=m;i++){
x=read();y=read();z=read();
v[x].push_back(prz(y,z));
}
for(int i=1;i<=n;i++){
vector<prz>::iterator iter=unique(v[i].begin(),v[i].end());
v[i].erase(iter,v[i].end());
}
vector<int >vals;
for(int i=1;i<=n;i++){
for(int j=0;j<(int)v[i].size();j++){
vals.push_back(v[i][j].second);
}
}
sort(vals.begin(),vals.end());
vals.erase(unique(vals.begin(),vals.end()),vals.end());
int l=0;int r=vals.size()-1,ans=-1;
while(l<=r){
int mid=l+r>>1;
if(ok(vals[mid])){
r=mid-1,ans=mid;
}
else l=mid+1;
}
if(ans==-1)cout<<"No solution\n";
else cout<<vals[ans]<<endl;
return 0;
}