10.4
全国信息学奥林匹克联赛(NOIP2017)复赛模拟
提高组第一试
2017年10月4日 8:10-11:40
(请选手务必仔细阅读本页内容)
题目名称 |
密码 |
独立集 |
益智游戏 |
题目类型 |
传统 |
传统 |
传统 |
目录 |
substring |
bubble |
game |
可执行文件名 |
substring |
bubble |
game |
输入文件名 |
substring.in |
bubble.in |
game.in |
输出文件名 |
substring.out |
bubble.out |
game.out |
每个测试点时限 |
1秒 |
1秒 |
1秒 |
内存限制 |
256M |
256M |
256M |
测试点数目 |
10 |
20 |
10 |
每个测试点分值 |
10 |
5 |
10 |
提交源程序文件名
对于C++语言 |
substring.cpp |
bubble.cpp |
game.cpp |
对于C语言 |
substring.c |
bubble.c |
game.c |
对于pascal语言 |
substring.pas |
bubble.pas |
game.pas |
编译选项
对于C++语言 |
-lm |
-lm |
-lm |
对于C语言 |
-lm |
-lm |
-lm |
对于pascal语言 |
|
|
|
注意事项
1.文件名(程序名和输入输出文件名)必须使用英文小写。
2.除非特殊说明,结果比较方式均为忽略行末空格及文末回车的全文比较。
3.C/C++中函数main()的返回值类型必须是int,程序正常结束时的返回值必须是0。
4.全国统一评测时采用的机器配置为:CPU 2.8GHz,内存4G,上述时限以此配置为准。
5.只提供Linux格式附加样例文件。
6.评测在NOI Linux下进行。
7.编译时不打开任何优化选项。
3781---密码
(substring.cpp/c/pas)
【问题描述】
假发通过了不懈的努力,得到了将军家门锁的密码(一串小写英文字母)。但是假发被十四和猩猩他们盯上了,所以假发需要把密码传递出去。因为假发不想十四他们发现几松门前贴的小纸条就是将军家的密码,所以他加密了密码(新八:听起来有点诡异)。加密方法如下:随机地,在密码中任意位置插入随机长度的小写字符串。
不过,假发相信银桑和他那么多年小学同学,一定能猜中密码是什么的(新八:银桑什么时候成攮夷志士了!!!)。可是,写完了小纸条之后,假发觉得有点长,就想截去头和尾各一段(可以为空),让剩下的中间那一段依然包含真~密码。想着想着,假发就想知道有多少种可行方案。结果在沉迷于稿纸之际,假发被投进了狱门岛(新八:……)。于是,就由你计算了。
【输入】
两行非空字符串,纯小写英文字母,第一行是加密后的密码,第二行是原密码。
第一行长度不超过300000,第二行不超过200。
【输出】
一行,有多少种方案。注意:不剪也是一种方案。
【输入输出样例1】
substring.in |
substring.out |
abcabcabc cba |
9 |
【样例1解释】
用(L,R)表示一种方案,其中L和R分别表示截去头和尾的长度。这9种方案分别是(0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2)。
【输入输出样例2】
substring.in |
substring.out |
abcabcaba cba |
12 |
【输入输出样例3】
substring.in |
substring.out |
abcabcabac cba |
18 |
【数据说明】
30%的数据满足第一行长度不超过1000。
100%的数据满足第一行长度不超过300000,方案总数不超过10^18。
【题目分析】
类型:暴力/暴力
这道题有很多方法可以过。比如预处理每个位置上一个任意字母在什么位置,然后枚举开始位置,跳m下,就能O(nm)过。同样,也可以暴力设f[n][m]递推过。
30%:纯暴力;
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
string s,t;
int i,j,k,lens,lent,x=-1;
long long Ans=0;
int main()
{ cin>>s>>t;
lens=s.length();lent=t.length();
for(i=0;i<lens;i++)
{ j=0;
if(s[i]==t[j])
{ k=i;k++;j++;
while(k<lens&&j<lent)
if(s[k]==t[j]){k++;j++;}
else k++;
if(k<=lens&&j==lent)
{ Ans+=(i-x)*(lens-k+1);
x=i;
}
}
}
cout<<Ans<<endl;
return 0;
}
3782---独立集
(bubble.cpp/c/pas)
【问题描述】
有一天,一个名叫顺旺基的程序员从石头里诞生了。又有一天,他学会了冒泡排序和独立集。在一个图里,独立集就是一个点集,满足任意两个点之间没有边。于是他就想把这两个东西结合在一起。众所周知,独立集是需要一个图的。那么顺旺基同学创造了一个算法,从冒泡排序中产生一个无向图。
这个算法不标准的伪代码如下:
Pascal版本 |
C/C++版本 |
procedure bubblesortgraph(n, a[]) : /*输入:点数n,1到n的全排列a。 输出:一个点数为n的无向图G。*/ 创建一个有n个点,0条边的无向图G。 repeat swapped = false for i 从 1 到 n-1 : if a[i] > a[i + 1] : 在G中连接点a[i]和点a[i + 1] 交换a[i]和a[i + 1] swapped = true until not swapped 输出图G。 //结束。 |
void bubblesortgraph(n,a[]) //输入:点数n,1到n的全排列a //输出:一个点数为n的无向图G { 创建一个有n个点,0条边的无向图G。 do{ swapped=false for i 从1 到n-1 if(a[i]>a[i+1]) { 在G中连接点a[i]和点a[i+1] 交换a[i]和a[i+1] swapped =true } }while(swapped); 输出图G。 } //结束。 |
那么我们要算出这个无向图G最大独立集的大小。但是事情不止于此。顺旺基同学有时候心情会不爽,这个时候他就会要求你再回答多一个问题:最大独立集可能不是唯一的,但有些点是一定要选的,问哪些点一定会在最大独立集里。今天恰好他不爽,被他问到的同学就求助于你了。
【输入】
输入包含两行,第一行为N,第二行为1到N的一个全排列。
【输出】
输出包含两行,第一行输出最大独立集的大小,第二行从小到大输出一定在最大独立集的点的编号。
【输入输出样例】
bubble.in |
bubble.out |
3 3 1 2 |
2 2 3 |
【样例说明】
如上图,顶点1和2一定在最大独立集中,其对应的编号为2和3。
【数据范围】
30%的数据满足 N<=16
60%的数据满足 N<=1,000
100%的数据满足 N<=100,000
【题目分析】
类型:最长上升子序列
观察给出的伪蟒蛇代码可以发现,当且仅当两个数为逆序对的时候有边。那么对于独立集,两两之间互不为逆序对。同样观察发现这就是上升子序列。那么最大独立集就是最长上升子序列。
至于必定会在最长上升子序列里的元素:
首先要满足它能在最长上升子序列里;
‚其次是以它结尾的最长上升子序列的长度独一无二(否则就可以交换);
这样做两遍最长上升子序列就可以了。
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int d[100005]={0},f[100005]={0},g[100005]={0};
int ha[1000005]={0},a[100005]={0};
int main()
{ int n,i,j,k,len,L,R,mid;
scanf("%d",&n);
for(i=1;i<=n;i++)scanf("%d",&a[i]);
len=0;
for(i=1;i<=n;i++)
{ if(a[i]>d[len]){d[++len]=a[i];f[i]=len;}
else
{ L=1;R=len;
while(L<=R)
{ mid=(L+R)/2;
if(d[mid]>=a[i])R=mid-1;
else L=mid+1;
}
f[i]=L;
d[L]=a[i];
}
}
len=0;d[0]=0x7fffffff/2;
for(i=n;i>=1;i--)
{ if(a[i]<d[len]){d[++len]=a[i];g[i]=len;}
else
{ L=1;R=len;
while(L<=R)
{ mid=(L+R)/2;
if(d[mid]<=a[i])R=mid-1;
else L=mid+1;
}
g[i]=L;
d[L]=a[i];
}
}
printf("%d\n",len);
for(i=1;i<=n;i++)if(f[i]+g[i]==len+1)ha[f[i]]++;
for(i=1;i<=n;i++)
if(ha[f[i]]==1&&f[i]+g[i]==len+1)printf("%d ",i);
return 0;
}
4491---益智游戏
(game.cpp/c/pas)
【问题描述】
小P和小R在玩一款益智游戏。游戏在一个正权有向图上进行。
小P控制的角色要从A点走最短路到B点,小R控制的角色要从C点走最短路到D点。
一个玩家每回合可以有两种选择,移动到一个相邻节点或者休息一回合。
假如在某一时刻,小P和小R在相同的节点上,那么可以得到一次特殊奖励,但是在每个节点上最多只能得到一次。
求最多能获得多少次特殊奖励。
【输入格式】
第一行两个整数n,m表示有向图的点数和边数。
接下来m行每行三个整数xi,yi,li,表示从xi到yi有一条长度为li的边。
最后一行四个整数A,B,C,D,描述小P的起终点,小R的起终点。
【输出格式】
输出一个整数表示最多能获得多少次特殊奖励。若小P不能到达B点或者小R不能到达D点则输出-1。
【样例输入输出】
game.in |
game.out |
5 5 1 2 1 2 3 2 3 4 4 5 2 3 5 3 5 1 3 5 4 |
2 |
【数据规模】
对于30%的数据,满足n≤50
对于60%的数据,满足n≤1000,m≤5000
对于100%的数据,满足n≤50000,m≤200000,1≤li≤500000000
【题目分析】
(1)特殊奖励的点一定是连续的
(2)若s-->x+(x,y)+y-->t = s-->t 则(x,y)在s到t的最短路上
(3)所有在A-->B,C-->D的最短路上的边构成一个有向无环图
在正向图上求A,C出发的最短路
在反向图上求到达B,D的最短路
最短路可以用dijkstra+优先队列
筛选出在A-->B,C-->D的最短路上的边作为新图
对新图拓扑排序+DP求最长路
时间复杂度(MlogN)
【算法步骤】
①读入数据建立正向图和反向图;
②在正向图上分别求出以A,C为源点的最短路;在反向图上分别求出以B,D为源点的最短路;
③筛选出在A→B且在C→D的最短路径上的边建立新图
④对新图拓扑排序+DP求最长路(问题变为求一个DAG图的最长路问题)
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
const int INF=0x7fffffff/3;
struct front_star{int ne,to,v;}a[500005],fa[500005],na[500005];
struct Edge{int x,y,v;}e[500005];
struct jgt{int num,V;};
bool operator < (const jgt &x,const jgt &y){return x.V>y.V;}
priority_queue<jgt>q;
int d1[500005]={0},d2[500005]={0},h[500005]={0},cnt=0,Q[500005]={0};
int fh[500005]={0},fcnt=0,n,m,rd[500005]={0},f[500005]={0},vst[500005]={0};
int ha[500005]={0},ncnt=0,nh[500005]={0};
int ans=0,A,B,C,D;
void Addedge(int x,int y,int v)
{ a[++cnt].to=y;a[cnt].ne=h[x];a[cnt].v=v;h[x]=cnt;}
void fAddedge(int x,int y,int v)
{ fa[++fcnt].to=y;fa[fcnt].ne=fh[x];fa[fcnt].v=v;fh[x]=fcnt;}
void nAddedge(int x,int y,int v)
{ na[++ncnt].to=y;na[ncnt].ne=nh[x];na[ncnt].v=v;nh[x]=ncnt;}
void Read()
{ int i;
scanf("%d%d",&n,&m);
for(i=1;i<=m;i++)
{ scanf("%d%d%d",&e[i].x,&e[i].y,&e[i].v);
Addedge(e[i].x,e[i].y,e[i].v);
nAddedge(e[i].y,e[i].x,e[i].v);
}
scanf("%d%d%d%d",&A,&B,&C,&D);
}
void Dijkstra(int x,int d[],front_star a[],int h[])
{ int i,j,k,v;
for(i=1;i<=n;i++){d[i]=INF;vst[i]=0;}
d[x]=0;
q.push((jgt){x,0});
while(!q.empty())
{ i=q.top().num;
q.pop();
if(vst[i])continue;
vst[i]=1;
for(k=h[i];k;k=a[k].ne)
{ j=a[k].to;
if(!vst[j]&&d[j]>d[i]+a[k].v)
{ d[j]=d[i]+a[k].v;
q.push((jgt){j,d[j]});
}
}
}
}
int Pre()
{ int i;
Dijkstra(A,d1,a,h);//以A点为源点在正向图上求最短路
Dijkstra(B,d2,na,nh);//以B点为源点在反向图上求最短路
if(d1[B]==INF||d2[A]==INF)return 0;//无解情况
for(i=1;i<=m;i++)//判断那些边在A-B的最短路径上
if(d1[e[i].x]+e[i].v+d2[e[i].y]==d1[B])ha[i]++;
Dijkstra(C,d1,a,h);//以C点为源点在正向图上求最短路
Dijkstra(D,d2,na,nh);//以D点为源点在反向图上求最短路
if(d1[B]==INF||d2[A]==INF)return 0;//无解
for(i=1;i<=m;i++)//判断那些边在C-D的最短路径上
{ if(d1[e[i].x]+e[i].v+d2[e[i].y]==d1[D])ha[i]++;
if(ha[i]==2)//建立新图
{ fAddedge(e[i].x,e[i].y,e[i].v);
rd[e[i].y]++;
}
}
return 1;
}
void SortDp()
{ int i;
for(i=1;i<=n;i++)
{ f[i]=1;
if(rd[i]==0)Q[++Q[0]]=i;
}
for(i=1;i<=Q[0];i++)
{ int x=Q[i];
for(int k=fh[x];k;k=fa[k].ne)
{ int y=fa[k].to;
f[y]=max(f[y],f[x]+1);
ans=max(ans,f[y]);
rd[y]--;
if(rd[y]==0)Q[++Q[0]]=y;
}
}
printf("%d",ans);
}
int main()
{ int i,j,k,x,y,v;
Read();
if(Pre())SortDp();
else printf("-1");
return 0;
}
3783---道路改建
(rebuild.pas/c/cpp)
【问题描述】
人称不死将军的林登·万,与他的兄弟林登·图两人的足迹踏遍了地球的每一寸土地。他们曾将战火燃遍了世界。即使是lifei888这样的强悍人物也从来没有将他彻底击败。
这一次,林登·万在N个城市做好了暴动的策划。然而,在起事的前一天,将军得知计划已经泄漏,决定更改计划,集中力量掌握一部分城市。
具体来说,有M条单向的道路连接着这N座城市。对于两座城市A,B,如果它们能够通过路径直接或间接的互相到达,那么就林登·万可以同时控制A,B两座城市而不至于分散力量,反之则会被lifei888各个击破。
为了扩大成果,将军还组织了人手改建道路。这些人可以在起事前将其中一条单向道路改变成双向。现在,将军想要知道他最多能通过改建道路控制几座城市,以及通过改建哪些道路,可以使他控制的城市到达最多。
【输入】
第一行为两个正整数N,M。
接下来M行每行两个范围在1~N内的正整数x,y,表示有一条从x到y的单向路径。
输入保证任意两点的任意方向最多只有一条边直接相连。
【输出】
输出共三行。
第一行一个正整数,将军最多能控制的城市数量。
第二行一个正整数L,表示有L种改建方案使得将军能控制最多的城市。
第三行L个按递增顺序给出的正整数ki,表示改建输入中的第ki条边能使将军能控制最多的城市。
【输入输出样例1】
rebuild.in |
rebuild.out |
5 4 1 2 2 3 1 3 4 1 |
3 1 3
|
【输入输出样例2】
rebuild.in |
rebuild.out |
3 4 1 2 2 1 1 3 3 1 |
3 4 1 2 3 4 |
【数据规模】
对于30%的数据,N,M<=10
对于60%的数据,N<=1500,M<=100000
对于100%的数据,N<=2000 M<=N*N
【题目大意】给你N个点M条单向边连接的图,问可以把一条边改成双向边,所得到的最大强连通分量的大小是多少?
【题目考点】
【题目分析】
题目就是要求把一条有向边变为无向边后,求最大的强连通分量的大小。
30%的数据,暴力枚举每一条边,然后求强连通分量即可。因为N,M非常小,所以求强连通分量这一步可以用floyd完成。
void doit() { int i,j,k,num; for(k=1;k<=n;k++) //枚举中间点 for(i=1;i<=n;i++) for(j=1;j<=n;j++)g[i][j]=g[i][j]||(g[i][k]&&g[k][j]); num=0; for(i=1;i<=n;i++) //统计连通分量数 if(vst[i]==0) { num++;vst[i]=num; //顶点i属于新的连通分量 for(j=1;j<=n;j++) if(g[i][j]&&g[j][i])vst[j]=num; //i和j属于同一连通分量 } } |
对于60%的数据,首先对原图求一次强连通分量。然后我们就得到了一个拓扑图。用f1[i]记录i这个节点能到达的节点组成的集合,f2[i]记录能到达i这个节点的节点组成的集合。显然f1,f2可以通过两次DP在O(N*M)的时间内求出来。
接下来我们枚举改变的边(u,v)。显然最大的强连通分量一定包含了(u,v)。那么我们只要求包含了(u,v)的强连通分量即可。这个新的强连通分量就是f1[u]和f2[v]的交集。最大的交集即为答案,这一步同样可以在O(N*M)的时间内解决。
对于100%的数据,因为求f1,f2的过程中用到了and和or操作,把f1,f2用30位的整数压缩,时间复杂度仍然是O(N*M)的,但常数只有1/32,可以通过全部数据。
算法步骤:
读入数据,建立图
‚强连通分量的缩点
ƒ建立新图,拓扑排序,求出f1[i]记录i这个节点能到达的节点组成的集合,f2[i]记录能到达i这个节点的节点组成的集合;
„枚举m条边,对于每条边<x,y>,即新的强连通分量就是f1[fa[x]]与f2[fa[y]]的交集,最大交集即为答案。
【扩展知识】STL模板bitset
#include<bitset>
#include<iostream>
using namespace std;
bitset<2010>a[2010],b[2010],tmp;
int main()
{ int x=1,y=1;
a[x][1]=1;a[x][2]=1;a[x][4]=1;//1 2 4
b[y][1]=1;b[y][3]=1;b[y][4]=1;b[y][5]=1;//1 3 4 5
a[x]|=b[y]; //合并两个集合中的1 1 2 3 4 5
tmp=a[x]&b[y];//求两个集合的交集 1 3 4 5
int Ans=a[x].count();//统计tmp中二进制位为1的个数
cout<<Ans<<endl;
return 0;
}
【方法2】
#include<cstdio>
#include<cstring>
#include<ctime>
#include<vector>
#include<bitset>
using namespace std;
int U[8000005],V[8000005],N,M,Ans[8000005];
bitset<2005> F[2005],G[2005];
void Init()
{ scanf("%d%d",&N,&M);
for(int i=1;i<=N;i++)F[i][i]=1;
for(int i=1;i<=M;i++)
{ scanf("%d%d",&U[i],&V[i]);
F[U[i]][V[i]]=1;
}
for(int k=1;k<=N;k++)//枚举中间点
for(int i=1;i<=N;i++)//枚举起点,省掉终点
if(F[i][k])F[i]|=F[k];//相当于f[i,j]=f[i,j]||(f[i,k]&&f[k,j])
//F[i]表示结点i到达的点的集合
for(int i=1;i<=N;i++)//G[i]表示到达i点的集合
for(int j=1;j<=N;j++)G[j][i]=F[i][j];
}
void Work()
{ for(int i=1;i<=M;i++)//枚举边反向并求交集
{ Ans[i]=((F[U[i]]&G[V[i]])).count();//交集中点的个数
if(Ans[0]<Ans[i])Ans[0]=Ans[i];//求最大值
}
}
void Output()
{ int Cnt=0;
printf("%d\n",Ans[0]);
for(int i=1;i<=M;i++)
if(Ans[i]==Ans[0])Cnt++;
printf("%d\n",Cnt);
for(int i=1;i<=M;i++)
if(Ans[i]==Ans[0])printf("%d ",i);
}
int main()
{ Init();
Work();
Output();
return 0;
}