期末上机的意义毋庸置疑。签到一题,简单题四选三,中等题三选二,难题三选一,我最开始估计6题会有20人左右,7题会在5人上下,但最终的结果依然有些低于我们的期望。简单题能过掉3题的就很少,使得以送分为目的设置的简单题变得一点都不简单;做到难题的人更是寥寥无几。成绩上助教们自然会有一定的考虑和调整,但综合整个学期的上机和练习来看,童鞋们的数据结构实践能力并没有达到一个很好的水平,这也是我比较遗憾的事情吧。
我必须指出,因为要保证数据的绝对正确,我的标程不一定按照上机的要求来做,比如使用了STL,比如题目要求用递归但我没有用之类的。
0-签到题. A+B
呵呵。
A-简单题(20). 水水的链表
链表排序,涉及到链表的移动。数据量给得略微有点迷惑性,其实O(n^2)的排序就能过。
#include<cstdio>
#include<list>
using namespace std;
list<int> data;
int main()
{
int n,x;
while(~scanf("%d",&n))
{
while(n--)
{
scanf("%d",&x);
data.push_back(x);
}
data.sort();
while(!data.empty())
{
printf("%d ",data.back());
data.pop_back();
}
putchar('\n');
}
}
B-简单题(20). TXTEditor
这个题坑到了很多人让我和渣诚表示十分费解……我们也在比赛中一再提示,就差直接说在TYPE的时候要清空栈了。事实上除了这个也就没别的可说的了,简单的栈。
#include<cstdio>
#include<deque>
using namespace std;
deque<char> str,st;
int main()
{
int m;
char op[10],c;
while(~scanf("%d",&m))
{
while(m--)
{
scanf("%s",op);
switch(op[0])
{
case 'T':
scanf(" %c",&c);
str.push_back(c);
st.clear();
break;
case 'C':
if(!str.empty())
{
st.push_back(str.back());
str.pop_back();
}
break;
case 'R':
if(!st.empty())
{
str.push_back(st.back());
st.pop_back();
}
break;
}
}
while(!str.empty())
{
putchar(str.front());
str.pop_front();
}
putchar('\n');
st.clear();
}
}
C-简单题(20). 斐波那契
注意到每一次调用函数f(),只要n大于1就进行了一次加法,所以只要用一个全局变量统计f()函数在n大于1时运行的次数就可以了。我则直接递推出来。
#include<cstdio>
using namespace std;
int main()
{
int n,ans[32];
ans[0]=ans[1]=0;
for(int i=2; i<32; ++i)
ans[i]=ans[i-1]+ans[i-2]+1;
while(~scanf("%d",&n))
printf("%d\n",ans[n]);
}
D-简单题(20). 凯撒加密
小时候一度很喜欢玩各种初级加密,比如埃特巴什码、凯撒加密、维吉尼亚密表等。但我其实想吐个槽,渣诚出的这个题并不是标准的凯撒加密法……这道题就是要依次计算明文字母与密钥字母的差值,再在明文字母上加上差值得到密文字母,循环往复,跳过标点。要稍微注意密文字母ASCII码小于'a'和大于'b'的情况,以及差值为负时用int的转换。
#include<cstdio>
#include<cstring>
#include<cctype>
using namespace std;
const int MAXN=10005;
char str[MAXN],key[12];
int main()
{
while(~scanf("%s%s",str,key))
{
int stl=strlen(str),kl=strlen(key);
for(int i=0,k=0; i<stl&&k<kl; ++i)
if(isalpha(str[i]))
{
key[k]=str[i]-key[k];
++k;
}
for(int i=0,k=0; i<stl; ++i)
if(isalpha(str[i]))
{
int tmp=str[i]+key[k++];
if(k==kl)
k=0;
if(tmp<'a')
tmp+=26;
else if(tmp>'z')
tmp-=26;
str[i]=tmp;
}
puts(str);
}
}
E-中等题(15). 反转二叉树
这道题的难度其实很小,只是因为考虑到代码量才放到中等题里。所谓反转,也就是做一个镜像,也就是左右子树交换。所以从根节点开始交换左右子节点指针,递归下去就可以了。
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=1005;
struct BTNode
{
char data;
BTNode *lchild,*rchild;
};
char str[MAXN];
void CreateBTNode(BTNode *&b)
{
BTNode *St[MAXN],*p;
int top=-1,k,l=strlen(str);
b=NULL;
for(int i=0; i<l; ++i)
switch(str[i])
{
case '(':
St[++top]=p;
k=1;
break;
case ')':
--top;
break;
case ',':
k=2;
break;
default:
p=(BTNode *)malloc(sizeof(BTNode));
p->data=str[i];
p->lchild=p->rchild=NULL;
if(!b)
b=p;
else
switch(k)
{
case 1:
St[top]->lchild=p;
break;
case 2:
St[top]->rchild=p;
break;
}
}
}
void ReverseBT(BTNode *b)
{
if(b)
{
swap(b->lchild,b->rchild);
ReverseBT(b->lchild);
ReverseBT(b->rchild);
}
}
void DispBTNode(BTNode *b)
{
if(b)
{
printf("%c",b->data);
if(b->lchild||b->rchild)
{
putchar('(');
DispBTNode(b->lchild);
if(b->rchild)
putchar(',');
DispBTNode(b->rchild);
putchar(')');
}
}
}
void DestroyBT(BTNode *&b)
{
if(b->lchild)
DestroyBT(b->lchild);
if(b->rchild)
DestroyBT(b->rchild);
free(b);
}
int main()
{
while(~scanf("%s",str))
{
BTNode *b;
CreateBTNode(b);
ReverseBT(b);
DispBTNode(b);
putchar('\n');
DestroyBT(b);
}
}
F-中等题(15). 王po买瓜
DS大半夜给了我这道题的idea,虽然后来他又吐槽我出水了,但过题的人数也确实没办法给我以出更难题的自信。这道题是一个双端队列,支持右端压入以及双端弹出,每次左端弹出时统计编号1的个数。
#include<cstdio>
#include<deque>
using namespace std;
deque<int> data;
int main()
{
int m,op,n,x,cnt;
while(~scanf("%d",&m))
{
while(m--)
{
scanf("%d%d",&op,&n);
switch(op)
{
case 1:
while(n--)
{
scanf("%d",&x);
data.push_back(x);
}
break;
case 2:
while(!data.empty()&&n--)
data.pop_back();
break;
case 3:
cnt=0;
while(!data.empty()&&n--)
{
if(data.front()==1)
++cnt;
data.pop_front();
}
printf("%d\n",cnt);
break;
}
}
data.clear();
}
}
G-中等题(15). 证券交易
因为消息是同步扩散的,所以一个人把消息扩散到其他人的时间就是最长的那个人的用时。所以只需要跑一遍floyd,然后寻找到其他人的最长用时的最小值就可以了。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=305;
const int INF=0x3f3f3f3f;
int g[MAXN][MAXN],n;
void floyd()
{
for(int k=1; k<=n; ++k)
for(int i=1; i<=n; ++i)
for(int j=1; j<=n; ++j)
g[i][j]=min(g[i][j],g[i][k]+g[k][j]);
}
int main()
{
int m,v,l;
while(~scanf("%d",&n)&&n)
{
memset(g,0x3f,sizeof(g));
for(int i=1; i<=n; ++i)
g[i][i]=0;
for(int u=1; u<=n; ++u)
{
scanf("%d",&m);
while(m--)
{
scanf("%d%d",&v,&l);
g[u][v]=min(g[u][v],l);
}
}
floyd();
int idx=0,ans=INF;
for(int i=1; i<=n; ++i)
{
int tmp=0;
for(int j=1; j<=n; ++j)
tmp=max(tmp,g[i][j]);
if(tmp<ans)
{
ans=tmp;
idx=i;
}
}
if(idx==0)
puts("disjoint");
else
printf("%d %d\n",idx,ans);
}
}
H-难题(10). Bi to Tri
我一开始并不是很喜欢渣诚出的这道题,但写的时候感觉到这道题其实考查了对二叉树建树和遍历过程的理解,只有对二叉树的相关操作理解透彻了,才可以很轻松地将二叉树结构改写成三叉树。只要加一个中孩子节点指针,在建树时增加一个栈来记录节点状态就可以了,具体见代码。
#include<cstdio>
#include<cstdlib>
#include<cstring>
using namespace std;
const int MAXN=1005;
struct TriTNode
{
char data;
TriTNode *lchild,*mchild,*rchild;
};
char str[MAXN];
void CreateTriTNode(TriTNode *&b)
{
TriTNode *St[MAXN],*p;
int k[MAXN],top=-1,l=strlen(str);
bool isr=false;
b=NULL;
for(int i=0; i<l; ++i)
switch(str[i])
{
case '(':
St[++top]=p;
k[top]=1;
isr=false;
break;
case ')':
--top;
break;
case ',':
++k[top];
break;
default:
p=(TriTNode *)malloc(sizeof(TriTNode));
p->data=str[i];
p->lchild=p->mchild=p->rchild=NULL;
if(!b)
b=p;
else
switch(k[top])
{
case 1:
St[top]->lchild=p;
break;
case 2:
St[top]->mchild=p;
break;
case 3:
St[top]->rchild=p;
break;
}
}
}
void PreOrder(TriTNode *b)
{
if(b)
{
printf("%c",b->data);
PreOrder(b->lchild);
PreOrder(b->mchild);
PreOrder(b->rchild);
}
}
void PreInOrder(TriTNode *b)
{
if(b)
{
PreInOrder(b->lchild);
printf("%c",b->data);
PreInOrder(b->mchild);
PreInOrder(b->rchild);
}
}
void PostInOrder(TriTNode *b)
{
if(b)
{
PostInOrder(b->lchild);
PostInOrder(b->mchild);
printf("%c",b->data);
PostInOrder(b->rchild);
}
}
void PostOrder(TriTNode *b)
{
if(b)
{
PostOrder(b->lchild);
PostOrder(b->mchild);
PostOrder(b->rchild);
printf("%c",b->data);
}
}
void DestroyTriT(TriTNode *&b)
{
if(b->lchild)
DestroyTriT(b->lchild);
if(b->mchild)
DestroyTriT(b->mchild);
if(b->rchild)
DestroyTriT(b->rchild);
free(b);
}
int main()
{
while(~scanf("%s",str))
{
TriTNode *b;
CreateTriTNode(b);
printf("PRE: ");
PreOrder(b);
printf("\nPREMID: ");
PreInOrder(b);
printf("\nPOSTMID: ");
PostInOrder(b);
printf("\nPOST: ");
PostOrder(b);
putchar('\n');
DestroyTriT(b);
}
}
I-难题(10). 中世界的Thor
求不严格次小生成树,数据保证有环意味着数据不是一棵树,换言之一定存在次小生成树。所谓不严格,是指次小生成树权值和可以等于(即不严格大于)最小生成树权值和,换言之此时最小生成树不唯一;这样便与一道陈题联系起来。从数据量上可以看出一个O(n^3)的算法就可以。我们可以轻易地做出一个猜想并证明出,不严格次小生成树一定可以从最小生成树中修改一条边获得。这样可以做一遍prim求出最小生成树,然后每次去掉最小生成树中的一条边再跑prim,找其中的最小值。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=305;
const int INF=0x3f3f3f3f;
bool vis[MAXN];
int g[MAXN][MAXN],n,lowc[MAXN];
int pre[MAXN],tmp[MAXN];
int prim(int cst[])
{
memset(vis,false,sizeof(vis));
for(int i=1; i<=n; ++i)
{
lowc[i]=g[1][i];
cst[i]=1;
}
vis[1]=true;
int ret=0;
for(int i=1; i<n; ++i)
{
int mark=-1,minc=INF;
for(int j=1; j<=n; ++j)
if(!vis[j]&&minc>lowc[j])
{
minc=lowc[j];
mark=j;
}
if(!~mark)
return -1;
ret+=minc;
vis[mark]=true;
for(int j=1; j<=n; ++j)
if(!vis[j]&&lowc[j]>g[mark][j])
{
lowc[j]=g[mark][j];
cst[j]=mark;
}
}
return ret;
}
int main()
{
int m,x,y,w;
while(~scanf("%d%d",&n,&m))
{
memset(g,0x3f,sizeof(g));
for(int i=1; i<=n; ++i)
g[i][i]=0;
while(m--)
{
scanf("%d%d%d",&x,&y,&w);
g[x][y]=g[y][x]=w;
}
prim(pre);
int ans=INF;
for(int i=2; i<=n; ++i)
{
int len=g[i][pre[i]];
g[i][pre[i]]=g[pre[i]][i]=INF;
int cnt=prim(tmp);
if(~cnt&&cnt<ans)
ans=cnt;
g[i][pre[i]]=g[pre[i]][i]=len;
}
printf("%d\n",ans);
}
}
当然,不严格次小生成树是有O(n^2)算法的,需要在prim的过程中dp出在最小生成树内连接两点的最长边的长度,然后枚举图中每条不属于最小生成树的边来替换对应两点在前面dp出的长度。这个做法超出课程要求了,不再多讲,放一下代码。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=305;
const int INF=0x3f3f3f3f;
bool vis[MAXN],used[MAXN][MAXN];
int g[MAXN][MAXN],n,lowc[MAXN],pre[MAXN],maxd[MAXN][MAXN];
int prim()
{
memset(vis,false,sizeof(vis));
memset(maxd,0,sizeof(maxd));
memset(used,false,sizeof(used));
for(int i=1; i<=n; ++i)
{
lowc[i]=g[1][i];
pre[i]=1;
}
pre[1]=0;
vis[1]=true;
int ret=0;
for(int i=1; i<n; ++i)
{
int mark=-1,minc=INF;
for(int j=1; j<=n; ++j)
if(!vis[j]&&minc>lowc[j])
{
minc=lowc[j];
mark=j;
}
if(!~mark)
return -1;
ret+=minc;
for(int j=1; j<=n; ++j)
if(vis[j])
maxd[j][mark]=maxd[mark][j]=max(maxd[j][pre[mark]],lowc[mark]);
vis[mark]=true;
used[mark][pre[mark]]=used[pre[mark]][mark]=true;
for(int j=1; j<=n; ++j)
if(!vis[j]&&lowc[j]>g[mark][j])
{
lowc[j]=g[mark][j];
pre[j]=mark;
}
}
return ret;
}
int second_MST()
{
int MST=prim(),ret=INF;
for(int i=1; i<=n; ++i)
for(int j=1; j<=n; ++j)
if(i!=j&&g[i][j]!=INF&&!used[i][j])
ret=min(ret,MST+g[i][j]-maxd[i][j]);
return ret;
}
int main()
{
int m,x,y,w;
while(~scanf("%d%d",&n,&m))
{
memset(g,0x3f,sizeof(g));
for(int i=1; i<=n; ++i)
g[i][i]=0;
while(m--)
{
scanf("%d%d%d",&x,&y,&w);
g[x][y]=g[y][x]=w;
}
printf("%d\n",second_MST());
}
}
J-难题(10). 拼接字符串
这道题要将两个串按最小长度和最小字典序进行拼接,对KMP稍作修改使其返回匹配到目标串末尾时模式串的匹配长度,然后分别做两次KMP,找匹配长度最长的拼接方式,如果长度一致则找字典序最小的即可。
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN=100005;
char a[MAXN],b[MAXN];
int next[MAXN];
void GetNext(const char t[],int l)
{
int j=0,k=-1;
next[0]=-1;
while(j<l)
if(k==-1||t[j]==t[k])
next[++j]=++k;
else
k=next[k];
}
int KMP(const char str[],const char t[],int sl,int tl)
{
GetNext(t,tl);
int i=0,j=0;
while(i<sl)
if(j==-1||str[i]==t[j])
{
++i;
++j;
}
else
j=next[j];
return j;
}
int main()
{
while(~scanf("%s%s",a,b))
{
int la=strlen(a),lb=strlen(b);
int p=KMP(a,b,la,lb),q=KMP(b,a,lb,la);
if(p>q||(p==q&&strcmp(a,b)<0))
{
printf("%s",a);
while(p<lb)
putchar(b[p++]);
}
else
{
printf("%s",b);
while(q<la)
putchar(a[q++]);
}
putchar('\n');
}
}
最后我想说一下自己对STL的看法吧,这是一个比较有争议的地方。首先不得不说的是,我们的C++课程极度缺乏对STL的介绍,以至于很多人连sort都不会用,更不要说在C++编程中利用率非常高的各类容器了;其次数据结构课程一直以来都禁用STL,因为手动实现各类结构必然是比捡现成的更利于理解数据结构的精髓。
然后从一个ACMer的角度来说,数据结构应当成为帮助解决问题的有力工具,而不是bug频出的阻碍;换言之,一个高水平码农,应当更多把精力放在对问题解决方式的设计上,而不是具体细节的实现和调节上。而在这个意义上,STL要比自行实现的东西要靠谱得多。从另一个角度上,作为一个暂时以C++为主要编程语言的码农,不了解基础STL简直是不可想象的严重的知识面缺失。
所以我对STL并不抱以排斥的目的。然而我们的数据结构课程最终目的依然是夯实基础,并且考虑到对惯例的遵循,所以期末上机依然沿用了以往的要求禁用了STL;但从历次上机和练习的标程中,想必童鞋们可以看出我对使用STL的态度是不反对的。这就好像小学刚学乘法的时候,老师要求写麻烦的竖式乘法,但乘法计算纯熟后就无所谓了;初中刚学方程时,老师要求合并同类项要一步步地写出来,后来熟能生巧再长的方程式也都一步就合并完了。而现在,你们在学习数据结构的原理和实现方式,手写的链表、栈、队列等等就是竖式乘法。所以我的态度是,只要你能保证在禁用STL时你自己可以手写出各类结构,STL随你用。
我想,对原理的透彻理解,以及对功能的简洁实现,合起来才是编程之美吧。