算法竞赛入门经典——数据结构基础
再谈栈和队列
问题:铁轨(UVa 514)
某城市有一个火车站,有n节车厢从A方向驶入车站,按进站顺序编号为1~n。你的任务是判断是否能让它们按照某种特定的顺序进入B方向的铁轨并驶出车站。例如,出栈顺序(5 4 1 2 3)是不可能的,但(5 4 3 2 1)是可能的。
为了重组车厢,你可以借助中转站C。这是一个可以停放任意多节车厢的车站,但由于末端封顶,驶入C的车厢必须按照相反的顺序驶出C。对于每节车厢,一旦从A移入C,就不能再回到A了;一旦从C移入B,就不能回到C了。换句话说,在任意时刻,只有两种选择:A->C和C->B。
分析
在中转站C中,车厢符合后进先出的原则,因此是一个栈。
#include<cstdio>
#include<stack>
using namespace std;
const int MAXN = 1000 + 10;
int n,target[MAXN];
int main()
{
while(scanf("%d",&n)==1){
stack<int> s;
int A=1,B=1;
for(int i=1;i<=n;i++){
scanf("%d",&target[i]);
}
int ok=1;
while(B<=n){
if(A==target[B]){
A++;
B++;
}
else if(!s.empty()&&s.top()==target[B]){
s.pop();
B++;
}
else if(A<=n){
s.push(A++);
}
else{
ok=0;
break;
}
}
printf("%s\n",ok?"Yes":"No");
}
return 0;
}
链表
问题:破损的键盘(UVa 11988)
你有一个破损的键盘。键盘上的所有键都可以正常工作,但有时Home键或者End键会自动按下。你并不知道键盘存在这一问题,而是专心地打稿子,甚至连显示器都没打开。当你打开显示器之后,展现在你面前的是一段悲剧的文本。你的任务是在打开显示器之前计算出这段悲剧文本。
输入包含多组数据。每组数据占一行,包含不超过100000个字母、下划线、字符“[”或者“]”。其中字符“[”表示Home键,“]”表示End键。输入结束标志为文件结束符(EOF)。输入文件不超过5MB。对于每组数据,输出一行,即屏幕上的悲剧文本。
样例输入:
This_is_a_[Beiju]_text
[[]][][]Happy_Birthday_to_Tsinghua_University
样例输出:
BeijuThis_is_a__text
Happy_Birthday_to_Tsinghua_University
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int const maxn=100000+5;
int last,cur,next[maxn];//光标位于cur号字符的后面
char s[maxn];
int main()
{
while(scanf("%s",s+1)==1)
{
int n=strlen(s+1);//输入保存在s[1],s[2]...中
last=cur=0;
next[0]=0;
for(int i=1;i<=n;i++)
{
char ch=s[i];
if(ch=='[') cur=0;
else if(ch==']') cur=last;
else //插入
{
next[i]=next[cur]; //现在插入的字符的指向的下一个位置(next)变成了cur指向的下一个位置(next)
next[cur]=i; //而cur指向的下一个位置就指向了要插入字符的位置(i),这样就完成了插入,与上一句位置不能颠倒
if(cur==last) last=i;//更新“最后一个字符”编号
cur=i; //移动光标
}
}
for(int i=next[0];i!=0;i=next[i])
printf("%c",s[i]);
printf("\n");
}
return 0;
}
数和二叉树
二叉树的编号
问题:小球下落(UVa 679)
有一棵二叉树,最大深度为D,且所有叶子的深度都相同。所有结点从上到下从左到右编号为1,2,3,…,2的D次方-1。在结点1处放一个小球,它会往下落。每个内结点上都有一个开关,初始全部关闭,当每次有小球落到一个开关上时,状态都会改变。当小球到达一个内结点时,如果该结点上的开关关闭,则往左走,否则往右走,直到走到叶子结点。
一些小球从结点1处依次开始下落,最后一个小球将会落到哪里呢?输入叶子深度D和小球个数I,输出第I个小球最后所在的叶子编号。假设I不超过整棵树的叶子个数。D<=20。输入最多包含1000组数据。
样例输入:
4 2
3 4
10 1
2 2
8 128
16 12345
样例输出:
12
7
512
3
255
36358
分析
不难发现,对于一个结点k,其左子结点、右子结点的编号分别为2k和2k+1.这个结论非常重要,请读者引起重视。
#include<cstdio>
#include<cstring>
const int maxd = 20;
int s[1<<maxd];
int main()
{
int D,I;
while(scanf("%d%d",&D,&I)==2){
memset(s,0,sizeof(s));
int k,n=(1<<D)-1;
for(int i=0;i<I;i++){
k=1;
for( ; ; ){
s[k]=!s[k];
k=s[k]?k*2:k*2+1;
if(k>n){
break;
}
}
}
printf("%d\n",k/2);
}
return 0;
}
每个小球都会落在根结点上,因此前两个小球必然是一个在左子树,一个在右子树。一般地,只需看小球编号的奇偶性,就能知道它是最终在哪颗子树中。对于那些落入根结点左子树的小球来说,只需知道该小球是第几个落在根的左子树里的,就可以知道它下一步往左还是往右了。依此类推,直到小球落到叶子上。
如果使用题目中给出的编号I,则当I是奇数时,它是往左走的第(I+1)/2个小球;当I是偶数时,它是往右走的第I/2个小球。这样,可以直接模拟最后一个小球的路线:
#include<cstdio>
#include<cstring>
const int maxd = 20;
int s[1<<maxd];
int main()
{
int D,I;
while(scanf("%d%d",&D,&I)==2){
int k=1;
for(int i=0;i<D-1;i++){
if(I%2){
k=k*2;
I=(I+1)/2;
}
else{
k=k*2+1;
I/=2;
}
}
printf("%d\n",k);
}
return 0;
}
图
用DFS求连通块
问题:油田(UVa 572)
输入一个m行n列的字符矩阵,统计字符“@”组成多少个八连块。如果两个字符“@”所在的格子相邻(横、竖或者对角线方向),就说它们属于同一个八连块。
分析
和前面的二叉树遍历类似,图也有DFS和BFS遍历。由于DFS更容易编写,一般用DFS找连通块:从每个“@”格子出发,递归遍历它周围的“@”格子。每次访问一个格子时就给它写上一个“连通分量编号”(即下面代码中的idx数组),这样就可以在访问之前检查它是否已经有了编号,从而避免同一个格子访问多次。
#include<cstdio>
#include<cstring>
const int maxn = 100 + 5;
char pic[maxn][maxn];
int m,n,idx[maxn][maxn];
void dfs(int r,int c,int id)
{
if(r<0||r>=m||c<0||c>=n){
return ;
}
if(idx[r][c]>0||pic[r][c]!='@'){
return ;
}
idx[r][c]=id;
for(int dr=-1;dr<=1;dr++){
for(int dc=-1;dc<=1;dc++){
if(dr!=0||dc!=0){
dfs(r+dr,c+dc,id);
}
}
}
}
int main()
{
while(scanf("%d%d",&m,&n)==2&&m&&n){
for(int i=0;i<m;i++){
scanf("%s",pic[i]);
}
memset(idx,0,sizeof(idx));
int cnt=0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(idx[i][j]==0&&pic[i][j]=='@'){
dfs(i,j,++cnt);
}
}
}
printf("%d\n",cnt);
}
return 0;
}