Catenyms解题报告by John.docx

上传人:b****4 文档编号:3692757 上传时间:2022-11-24 格式:DOCX 页数:17 大小:49.34KB
下载 相关 举报
Catenyms解题报告by John.docx_第1页
第1页 / 共17页
Catenyms解题报告by John.docx_第2页
第2页 / 共17页
Catenyms解题报告by John.docx_第3页
第3页 / 共17页
Catenyms解题报告by John.docx_第4页
第4页 / 共17页
Catenyms解题报告by John.docx_第5页
第5页 / 共17页
点击查看更多>>
下载资源
资源描述

Catenyms解题报告by John.docx

《Catenyms解题报告by John.docx》由会员分享,可在线阅读,更多相关《Catenyms解题报告by John.docx(17页珍藏版)》请在冰豆网上搜索。

Catenyms解题报告by John.docx

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.本文详述了为什么其他方法不行,一定要用欧拉图

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 初中教育 > 数学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1