Catenyms解题报告by John.docx
《Catenyms解题报告by John.docx》由会员分享,可在线阅读,更多相关《Catenyms解题报告by John.docx(17页珍藏版)》请在冰豆网上搜索。
Catenyms解题报告byJohn
Catenyms解题报告
学号:
不详姓名:
钟鏸日期:
2011.6.10-2011.6.20
题目
http:
//172.21.81.30/ZQUOJ/problem/Problem.jsp?
id=1394
题意
catenym是用一个连接符号(.)串接起来的两个单词,其中,前一个单词的最后一个字母,与后一个单词的第一个字母,是一样的。
如题干所示,下面就是两个catenym:
dog.gopher或gopher.rat
然后,题干定义一个“组合catenym”就是若干个前后相连起来的catenym,如:
aloha.aloha.arachnid.dog.gopher.rat.tiger
(前一个单词的最后一个字母=下一个单词的第一个字母)
现题目给出若干个单词,让我们构造一个“组合catenym”。
构造时,要求每个单词都出现一次,且仅一次。
题意弄懂之后,还有几个事项是需要注意的:
1.给出的单词,不一定是按照字典序从小到大的顺序给出的,因此有可能需要排序
2.问题的规模是1000个单词
3.所给的若干个单词,存在构造不出一个“组合catenym”的可能性,即无解,此时输出"***"
4.关于“thelexicographicallyleastcompoundcatenym”,为什么提出要“字典序最小”?
如何去满足这个“字典序最小”的条件?
分析
题目要求把所有单词前后连接在一起,每个单词出现一次且仅一次,看起来就是一个排列问题,用穷举法貌似可以解决。
但问题规模达到1000个单词,那么,就有1000!
种排列,要从这1000!
种排列中判断是否存在满足“组合catenym”条件的排列,如存在,则寻找字典序最小的排列。
总的计算量非常巨大。
“超时”是必然结果。
题目所给的第一个样例太简单了,一眼就能看出可以构造出“组合catenym”,所以不具有什么代表性。
因此,需要我们自己设计一些具有代表性的例子:
例1:
ejjjcasssebfffadkkkbebbdbnnnadcccbayyye
由于catenym涉及的只是单词的首字母以及最后一个字母,对于单词中间的字母以及长度,是根本不敏感的,所以,在设计这些单词的时候,中间的字母都是随意设计。
经过尝试,对于例1,我们可以构造出多个“组合catenym”:
dkkkb.bfffa.assse.ebbd.dcccb.bnnna.ayyye.ejjjc——①
dcccb.bnnna.ayyye.ebbd.dkkkb.bfffa.assse.ejjjc——②
dcccb.bfffa.assse.ebbd.dkkkb.bnnna.ayyye.ejjjc——③
……
因此,如果不给出限制条件,答案是不唯一的。
正因为如此,题目才给出“字典序最小”这个条件,使得答案唯一。
对于例1,正解是③。
例1告诉我们答案的多样性,以及如何根据题目条件选择合适的答案。
那么,什么情况下会出现无解呢?
题目所给的第二个样例同样是太简单,一眼就看出不能构造出“组合catenym”:
因为单词oak无法与任何其他单词前后连接起来。
有没有具有代表性的无解的例子呢?
我们可以思考设计出这样的例子:
例2:
akkkbannncbfffc
例3:
akkkbacccbbfffcbjjjc
例2和例3都是无法满足“使用全部单词一次且仅一次”这个条件来构造出“组合catenym”的。
通过以上分析,自然而然打算采用“搜索”策略来解题,希望通过搜索解空间,寻找符合题目条件的正解。
(如果我们没有这样的想法,说明我们没接触过“搜索”法,或者还没有建立“搜索”策略的思维。
)
如果没有接触过“欧拉图”或“半欧拉图”的概念,自然打算用朴素的“深度优先搜索DFS”来求解本题。
(广度优先搜索BFS在理论上可以求解本题,但BFS多用在求最短路径,用于本题速度很慢)
如果没有时间上的限制,或者问题规模较小,朴素的DFS是可以求得正解的。
对例1画出搜索过程的示意图如下:
说明:
DFS搜索过程中,为了尽快搜到正解,每一步在有多个单词可供选择时,先选择字典序小的单词,这样这样可以尽快搜到正解(满足字典序最小的“组合catenym”),避免遍历整棵搜索树。
以上做法虽然看似可以求解问题,但是,考虑到问题的数据规模:
1000个单词,那么,搜索树高度最多可达1000层,第一层有1000个节点,第二层有1000*999个节点,第三层有1000*999*998各节点,依此类推,那么,这棵搜索树是极为庞大的。
此外,由例1的搜索图解,必须想到的是,按字典序选择最小的单词作为第一个单词,往往求不出一个可能解。
根据经验,这些题目的大多数测试数据的正解往往不在搜索树的左下角,平均几率是在搜索树的中间底部位置,最坏的情况是在搜索树的右下角(这也是ICPC题目喜欢出的数据)。
再考虑到无解的情况,必定是搜完整棵树之后才知道无解。
那么,搜索所花的时间就会很长,耗费的空间也很多。
根据经验,朴素的BFS搜索策略的结果必然是TLE。
因此,朴素搜索策略在本题是不适用的,必须要加上剪枝。
本题所考的知识点是“欧拉图”和“半欧拉图”,如果没接触过,就不能AC本题。
因此,我们只能老老实实学一学“欧拉图”、“半欧拉图”、“欧拉回路”和“欧拉通路”等概念。
先看定义:
定义1通过图(无向图或有向图)中所有边一次且仅一次,行遍图中所有顶点的通路称为欧拉通路。
定义2通过图中所有边一次并且仅一次,行遍所有顶点的回路称为欧拉回路。
定义3具有欧拉回路的图称为欧拉图,具有欧拉通路而无欧拉回路的图称为半欧拉图。
学习了以上定义,我们可以把每个单词看作一条弧,单词的首字母看作弧的始点,单词的最后一个字母看作是弧的终点。
因此,本题对应的是有向图。
题目所给第一个样例的图如下:
例1的图如下:
学习了以上定义,我们也可以想到,题干所说的“每个单词都使用一次,且仅一次”,与欧拉路径的性质是完全吻合的。
题目本质
此时,本题就从表面上的字符串连接问题,首先否定了朴素的穷举思想(排列问题模型),然后建立搜索问题模型,最终归结为有向图的欧拉图问题——求欧拉回路或欧拉通路。
利用欧拉图的知识建立强有力的剪枝条件,再加上搜索的优先条件:
按字典序从小到大选用单词(也就是说,我们要对所有单词进行排序),就可以大大缩小搜索空间。
解题思路
有了以上概念之后,再看欧拉图的相关定理:
定理1无向图G是欧拉图当且仅当G是连通图,且G中没有奇度顶点。
(意思就是:
每个顶点的度都是偶数。
)
定理2无向图G是半欧拉图当且仅当G是连通的,且G中恰有两个奇度顶点。
定理3有向图D是欧拉图当且仅当D是强连通的且每个顶点的入度都等于出度。
定理4有向图D是半欧拉图当且仅当D是单向连通的,且D中恰有两个奇度顶点,其中一个的入度比出度大1,另一个的出度比入度大1,而其余顶点的入度都等于出度。
定理5G是非平凡的欧拉图当且仅当G是连通的且为若干个边不重的圈的并。
(“非平凡的”指的是图中大于一个顶点)
其中定理1和2是针对无向图的,定理3是针对欧拉图的,定理4是针对半欧拉图的。
定理3和定理4都适用于本题。
(请问定理5适用于什么问题?
)
有了定理3和定理4,在本题判断是否存在“组合catenym”,也就是判断是否存在可能解,就是轻而易举的事情了,时间复杂度大大降低。
首先,要满足“连通”条件,像题目所给的第二个样例,由于它是非连通图(就更不用说强连通了),所以可以马上判定它无解。
示意图如下:
其次,只要测试数据满足了定理3或定理4的条件,那么它就一定有解,剩下的问题就是怎样尽快找到正解。
经思考,本题的数据可分为3种情况:
1.不存在欧拉通路(此时当然也不存在欧拉回路),此时无解
2.存在欧拉通路,但不构成欧拉回路
根据定理4,问题有解,且可行解不止一个,但出发点只有一个:
应该选择“出度比入度大1”的顶点作为欧拉通路的出发点,按字典序选择下一个可用的单词(弧)走下去即可得正解。
(顺便说一句,“入度比出度大1”的顶点必然成为欧拉通路的终点。
)这样就可以大大缩小搜索树的第一层,也就是从“源头”就缩小了搜索树。
再举例如下图所示:
在这个图中,欧拉通路的出发点就应该是"s",而不选择字典序最小的以"k"开头的单词,因为s的入度为3,出度为4。
欧拉通路的终点是"p"。
3.存在欧拉回路
此时,问题肯定有解,且可行解不止一个,那么,就选定字典序最小的单词的首字母作为出发点。
然后按字典序选择下一个可用的单词(弧)走下去即可得正解。
可以想象,在第2种和第3种情况下,应该都是一气呵成走完整个搜索过程,无需回溯。
现在,可以写出伪代码如下:
1.检查所给图形是否欧拉图或半欧拉图
如果否,则"***"
如果是,则把单词按字典序排序,备用。
2.如果是欧拉图,则从最小的字母出发
否则就是半欧拉图,则从“出度比入度大1”的字母出发
3.DFS,每次选取一个“未用过”的字典序最小的单词(即未走过的弧),一直走下去,一边走一边输出单词。
(也可以一边走一边记录行走路线,最后一次性输出路径)
其中,1要判断是否连通图,弱连通也可以。
可用并查集来实现。
在这个过程中,可顺便统计每个顶点的入度、出度。
然后,检查每个顶点,看出度是否等于入度;出度比入度大1的顶点是否只有一个;入度比出度大1的顶点是否只有一个。
代码
constintmaxn=10000;
intV,E;//顶点数边数
structedge
{
intt;//s->t=w
charw[30];//存储单词本身
intnext;
};
intp[maxn];//表头节点
edgeG[maxn];//邻接表
//u->v=w,邻接表节点l
voidaddedge(intu,intv,char*w,intl)
{
strcpy(G[l].w,w);
G[l].t=v;
//按照w从小到大排序
if(p[u]==-1)
{
G[l].next=p[u];
p[u]=l;
return;
}
intindex=-1,pre=-1;
for(inti=p[u];i!
=-1;i=G[i].next)
{
if(strcmp(G[i].w,w)>=0)
{
index=i;
break;
}
pre=i;
}
if(pre==-1)
{
G[l].next=p[u];
p[u]=l;
return;
}
G[l].next=G[pre].next;
G[pre].next=l;
}
//并查集判断弱连通
intfath[27];
intfind(intx)
{
returnfath[x]==x?
x:
fath[x]=find(fath[x]);
}
voiduion(intx,inty)
{
x=find(x);
y=find(y);
if(x==y)
return;
fath[x]=y;
}
//欧拉路
intin[27],out[27];//统计每个顶点的入度、出度,只需27就够了
intflag;//标志,1是欧拉路,0不是欧拉图
ints0,t0;//欧拉路出发点终结点
intans[maxn];//保存欧拉路
intcur;//欧拉路长度
boolused[maxn];//记录某一条弧是否走过
///输出欧拉路
voiddfs(intx,intcas)//顶点边
{
for(inti=p[x];i!
=-1;i=G[i].next)
{
if(!
used[i])
{
used[i]=1;
dfs(G[i].t,i);
}
}
if(cas!
=-1)
ans[cur++]=cas;
}
//逆序输出
voidprint()
{
for(inti=cur-1;i>0;i--)
{
printf("%s.",G[ans[i]].w);
}
printf("%s\n",G[ans[0]].w);//输出最后一个
}
intmain()
{
inttestcase;
scanf("%d",&testcase);
while(testcase--)
{
scanf("%d",&E);//边数
V=26;//顶点最多26个,26个英文字母
memset(p,-1,sizeof(p));
for(inti=1;i<=V;i++)//初始化并查集,26个顶点分属26个集合
fath[i]=i;
memset(in,0,sizeof(in));//入度初始化=0
memset(out,0,sizeof(out));//出度初始化=0
memset(used,0,sizeof(used));//边是否访问过初始化=0,未访问过
charch[25];//存储一个单词
intvis[27]={0};
//------------------------------------------------------------------
//读入各条边,顺便做一些处理和统计
for(i=0;i{
scanf("%s",ch);//读入一个单词
//u是单词的第一个字母,v是单词的最后一个字母
//并把字母'a'~'z'转换为数字1~26
intu=ch[0]-'a'+1,v=ch[strlen(ch)-1]-'a'+1;
uion(u,v);//并查集的合并操作
vis[u]=vis[v]=1;//这两个字母出现在图中了
in[v]++,out[u]++;//入度、出度分别加1
addedge(u,v,ch,i);//w按照从小到大排序
}
//------------------------------------------------------------------
//使用并查集来判断是否弱连通
flag=1;//首先假设是连通图
intf1=-1;//是否处理图中第一个顶点的标志而已
for(i=1;i<=V;i++)//最多只需检查26个顶点,所以这个循环是很快的
{
if(vis[i])
{
if(f1==-1)//第一次
f1=find(i);
else
{
if(f1!
=find(i))
{
flag=0;//发现不是连通图
break;
}
}
}
}//题外话:
如果是无向图,BFS或DFS都可以用来判断是否连通图
if(!
flag)//不是连通图,直接收工
{
printf("***\n");
continue;
}
//------------------------------------------------------------------
//是连通图,继续检查是否满足欧拉图/半欧拉图的条件
intns0=0,nt0=0;//ns0和nt0用来累计出入度不等的顶点。
当它们的个数>1时,不存在欧拉路
for(i=1;i<=V;i++)
{
if(!
vis[i])//如果顶点i根本没有边,则跳过,无需检查
continue;
if(in[i]入度
{
if(out[i]-in[i]==1)//出度比入度大1的顶点
ns0++,s0=i;//那么s0就是欧拉通路的起点
else
flag=0;//出度比入度大了超过1,则肯定不是欧拉图/半欧拉图,所以置flag为0
}
else//入度>出度的判断,与上面一样的
if(in[i]>out[i])
{
if(in[i]-out[i]==1)
nt0++,t0=i;
else
flag=0;
}
}
if(!
(ns0<=1&&nt0<=1&&ns0==nt0))//不满足定理2或定理3,置flag为0
flag=0;
if(!
flag)//flag==0,不是欧拉图
{
printf("***\n");
continue;
}
//------------------------------------------------------------------
//是欧拉图
//选定欧拉路径的出发点后,开始DFS
if(ns0==1)//DFS的出发点是S0
{
cur=0;
dfs(s0,-1);
}
else//欧拉回路的情况
{
cur=0;
for(inti=1;i<=V;i++)//既然是回路,那么找到字典序最小的字母,从它开始出发DFS即可
{
if(vis[i])
{
dfs(i,-1);
break;//DFS一次即可
}
}
}
print();
}
return0;
}
时间复杂度的分析
首先记图的边数为e
因为顶点数很少,只有26个,所以,使用并查集来检查是否连通图,所消耗的时间几乎可以忽略不计。
把所有单词排序,采用快速排序,时间为O(e*log2e)
判断是否欧拉图/半欧拉图,对每一条边统计一次,所以时间复杂度是O(e)。
采用DFS去遍历欧拉图的每一条边,由于前面分析,几乎是一气呵成的,所以时间复杂度是O(e)。
因此,总的时间复杂度是O(e*log2e)。
空间复杂度的分析
关于顶点的存储空间:
顶点最多只有26个,加上一些辅助性的数组,也仅仅是k*26,所以,可以忽略不计。
关于边的存储空间:
最多1000个单词,空间复杂度是O(e)。
e<=1000
DFS函数是递归的,但最多1000层,空间复杂度也是O(e)。
因此,总的空间复杂度是O(e)。
另一种解法
对于欧拉图,如何寻找欧拉回路、欧拉通路,存在另一种算法,称“套圈法”。
算法思想的朴素表达
对于欧拉图,从一个节点出发,随便选一条边(弧)往下走(走过之后需要标记这条弧,下次不要重复走),必然也在这个节点终止(因为除了起始节点,其他节点的度数都是偶数,只要能进去就能出来)。
这样就构成了一个圈,但因为是随便走的,所以可能会有些边还没走过就回来了。
我们就从终止节点逆着往前查找,直到找到第一个分叉路口,然后从这个节点出发继续上面的步骤,肯定也是可以找到一条回到这个点的路径的,这时我们把两个圈连在一起。
当你把所有的圈都找出来后,整个欧拉回路的寻找就完成了。
寻找欧拉回路时,起始节点是可以任意选择的。
如果是有奇度顶点要寻找欧拉通路,则从奇度顶点出发即可,上述步骤依然有效。
算法思想的书面表达
一个解决此类问题基本的想法是从某个节点开始,然后查出一个从这个点出发回到这个点的环路径。
现在,环已经建立,这种方法保证每个点都被遍历。
如果有某个点的边没有被遍历就让这个点为起点,这条边为起始边,把它和当前的环衔接上。
这样直至所有的边都被遍历。
这样,整个图就被连接到一起了。
更正式的说,要找出欧拉路径,就要循环地找出出发点。
按以下步骤:
任取一个起点,开始下面的步骤:
如果该点没有相连的点,就将该点加进路径中然后返回。
如果该点有相连的点,就列一张相连点的表然后遍历它们直到该点没有相连的点。
(遍历一个点,删除一个点)
处理当前的点,删除和这个点相连的边,在它相邻的点上重复上面的步骤,把当前这个点加入路径中。
下面是伪代码:
#circuit[]是一个全局的数组
find_euler_circuit
circuitpos=0
find_circuit(node1)
#nextnodeandvisitedisalocalarray
#thepathwillbefoundinreverseorder
find_circuit(nodei)
ifnodeihasnoneighborsthen
circuit(circuitpos)=nodei
circuitpos=circuitpos+1
else
while(nodeihasneighbors)
pickarandomneighbornodejofnodei
delete_edges(nodej,nodei)
find_circuit(nodej)//递归
circuit(circuitpos)=nodei
circuitpos=circuitpos+1
要找欧拉路径,只要简单的找出一个度为奇数的节点,然后调用find_circuit就可以了。
一点点代码(pku2337里面截过来的,具体请看
//主函数中调用下面这个函数
euler(start,-1);//因为直接从start出发,所以第二个参数用-1代替,输出的时候要忽略掉。
//euler函数就是用的传说中的套圈法,是一个迭代的过程
//npath是一个全局变量,记录已经找到的边的数量
//边存储在adj[]中,是用数组实现的链表结构(就跟很多hash那样,首节点的数据域不用,只有指针部分有效)
voideuler(intcur,intedgeN)//cur当前到达的节点edgeN上一被选择的边,即上一个节点通过edgeN到达的cur
{
inti;
while(adj[cur].nxt!
=-1)
{
i=adj[cur].nxt;
adj[cur].nxt=adj[i].nxt;//相当于是删除掉使用了的边
euler(adj[i].end,i);
}
path[npath++]=edgeN;//后序记录,如果要保持搜索时候边的优先级,则逆向输出
}
体会
0.本文详述了为什么其他方法不行,一定要用欧拉图