Tag Archives:

hihoCoder 1050-树中的最长路

hihoCoder 1050-树中的最长路 #1050 : 树中的最长路 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 上回说到,小Ho得到了一棵二叉树玩具,这个玩具是由小球和木棍连接起来的,而在拆拼它的过程中,小Ho发现他不仅仅可以拼凑成一棵二叉树!还可以拼凑成一棵多叉树——好吧,其实就是更为平常的树而已。 但是不管怎么说,小Ho喜爱的玩具又升级换代了,于是他更加爱不释手(其实说起来小球和木棍有什么好玩的是吧= =)。小Ho手中的这棵玩具树现在由N个小球和N-1根木棍拼凑而成,这N个小球都被小Ho标上了不同的数字,并且这些数字都是出于1..N的范围之内,每根木棍都连接着两个不同的小球,并且保证任意两个小球间都不存在两条不同的路径可以互相到达。总而言之,是一个相当好玩的玩具啦! 但是小Hi瞧见小Ho这个样子,觉得他这样沉迷其中并不是一件好事,于是寻思着再找点问题让他来思考思考——不过以小Hi的水准,自然是手到擒来啦! 于是这天食过早饭后,小Hi便对着又拿着树玩具玩的不亦乐乎的小Ho道:“你说你天天玩这个东西,我就问你一个问题,看看你可否知道?” “不好!”小Ho想都不想的拒绝了。 “那你就继续玩吧,一会回国的时候我不叫上你了~”小Hi严肃道。 “诶!别别别,你说你说,我听着呢。”一向习惯于开启跟随模式的小Ho忍不住了,马上喊道。 小Hi满意的点了点头,随即说道:“这才对嘛,我的问题很简单,就是——你这棵树中哪两个结点之间的距离最长?当然,这里的距离是指从一个结点走到另一个结点经过的木棍数。”。 “啊?”小Ho低头看了看手里的玩具树,困惑了。 提示一:路总有折点,路径也不例外! 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第一行为一个整数N,意义如前文所述。 每组测试数据的第2~N行,每行分别描述一根木棍,其中第i+1行为两个整数Ai,Bi,表示第i根木棍连接的两个小球的编号。 对于20%的数据,满足N<=10。 对于50%的数据,满足N<=10^3。 对于100%的数据,满足N<=10^5,1<=Ai<=N, 1<=Bi<=N 小Hi的Tip:那些用数组存储树边的记得要开两倍大小哦! 输出 对于每组测试数据,输出一个整数Ans,表示给出的这棵树中距离最远的两个结点之间相隔的距离。 样例输入 8 1 2 1 3 1 4 4 5 3 6 6 7 7 8 样例输出 6


本题要求一棵树中相距最远两点之间的距离,也就是`树的直径`。 求树的直径有多种方法,我这里介绍两种。 第一种方法 第一种方法很好理解,我们随机的从某个节点s对树深度优先遍历(广度优先遍历也行),找到深度最大的叶子节点node1;然后再从node1点对树DFS,找到深度最大的叶子节点node2;则node1和node2即为这棵树中相距最远的两个点,它们之间的距离即为树的直径。 关于这种方法的简要证明,可以看这篇博客。 这种方法简单易懂,可以很快的写出代码,如下: [cpp] #include<iostream> #include<vector> using namespace std; const int MAX_N=1e5+2; int n;//节点数 int longest;//最远距离 int node1,node2;//node1,node2分别为为第一、二次DFS找到的最远点 vector<vector<int> > f_s(MAX_N);//father-son和son-father关系,存储双边 //参数分别为当前节点,深度,父亲节点 void dfs(int root,int depth,int father) { if(depth>longest)//更新 { longest=depth; node1=root; } int sons=f_s[root].size(); for(int i=0;i<sons;i++) { if(f_s[root][i]!=father)//不能回溯 dfs(f_s[root][i],depth+1,root); } } int main() { //freopen("input.txt","r",stdin); cin>>n; int a,b; for(int i=0;i<n-1;i++) { cin>>a>>b; f_s[a].push_back(b);//记录双边关系 f_s[b].push_back(a);//记录双边关系 } longest=0; dfs(1,0,-1);//随机选一个点(1)找到第一个最远点node1 longest=0; dfs(node1,0,-1);//以node1找第二个最远点node2,则node1到node2即为整棵树的直径 cout<<longest; return 0; } [/cpp] 本代码提交AC,用时255MS,内存9MB。 有一点需要提醒的是,因为不仅需要从s开始DFS,还需要从node1开始DFS,所以需要存储双向边,即不仅要存储(v,u),还需要存储(u,v);这样存了之后,为了防止DFS时回溯到父节点造成死循环,在DFS时要判断下一个节点是不是父节点,如果是则不继续DFS。 第二种方法 第一种方法虽然简单易懂,但不是hiho题目中给出的方法,那么hiho给出的又是什么方法呢?且看官方提示:
“没错,我枚举每一个点作为转折点t,求出以t为根节点的子树中的‘最长路’以及与‘最长路’不重合的‘次长路’,用这两条路的长度之和去更新答案,那么最终的答案就是这棵树的最长路长度了!”小Hi道。 “原来是这样,也就是说我只要以类似后序遍历的方式依次访问每个结点,从下往上依次计算每个结点的first值和second值,我就能够用O(N)的时间复杂度来解决这个问题咯!”
这提示的第一句话我倒是理解了,但是第二句一直不懂,为什么只要O(N)就可以解决问题呢。不管了,先看看只用第一句话能否AC吧。 我一开始的理解是这样的:尝试枚举每一个点为树的根节点t,然后从t开始DFS,找到以t为根的树的最长路径和次长路径(最长和次长路径不能有重复边),然后把他们加起来就是以t为根节点的树找到最长路径,而且这条最长路径肯定经过了根节点t。 比如样例输入,分别以节点1、2、3为根构成的树如下: 因为我们默认以t为根的树的最长路径一定经过节点t,所以上图最长路径分别为图中红色路线所示,长度分别为6,5,6(因为节点1和3都在最长路径上,所以他们求得的最长路径相等)。同理我们还可以求出以4,5…n为根(路径转折点)的最长路径,最后求这些最长路径中的最大值。 这个版本的代码如下: [cpp] #include<iostream> #include<vector> using namespace std; const int MAX_N=1e5+2; int n;//节点数 vector<vector<int> > f_s(MAX_N);//father-son和son-father关系,存储双边 vector<int> first,second;//first[i]和second[i]分别表示以i节点为根节点时(i节点以下构成的子树)的最大和次大长度 //参数分别为当前节点,父亲节点 void dfs(int root,int father) { int sons=f_s[root].size(); for(int i=0;i<sons;i++) { if(f_s[root][i]!=father)//不能回溯 { dfs(f_s[root][i],root); if(first[f_s[root][i]]+1>first[root]) { second[root]=first[root]; first[root]=first[f_s[root][i]]+1; } else if(first[f_s[root][i]]+1>second[root]) second[root]=first[f_s[root][i]]+1; } } } void init_first_second() { first.clear(); first.assign(n+1,0); second.clear(); second.assign(n+1,0); } int main() { freopen("input.txt","r",stdin); cin>>n; int a,b; for(int i=0;i<n-1;i++) { cin>>a>>b; f_s[a].push_back(b);//记录双边关系 f_s[b].push_back(a);//记录双边关系 } int ans=0; for(int i=1;i<=n;i++)//枚举每一个节点为根节点(转折点) { init_first_second(); dfs(i,-1); if(first[i]+second[i]>ans) ans=first[i]+second[i]; } cout<<ans; return 0; } [/cpp] 但是很遗憾,提交后TLE。 后来我仔细想了想,无论我们从哪个点s开始对树DFS,我们求得的first[i]和second[i]都是以点i为根构成的子树中它的最长和次长距离,那么first[i]+second[i]不就代表了从i开始DFS时得到的最长距离吗?比如图中以2为根的树中,我们在DFS的过程中肯定已经求到了first[1]和second[1],然后才能求first[2]和second[2],而这里的first[1]和second[2]和图中以1为根时求得的first[1]和second[1]是完全一样的。而对于以3为根的树,因为3正好在最长路径上,所以这里的first[1]和second[1]要小于前面两种情况,但是first[3]+second[3]和前面两种情况的first[1]+second[1]是相等的。 说了这么多,不知道同学们有没有发现,不管从哪个点开始DFS,只要一遍DFS结束,最长路径总会出现在某个first[i]+second[i]中。 至此,可以省略上一个版本中的for循环,只要一个DFS即可: [cpp] #include<iostream> #include<vector> using namespace std; const int MAX_N=1e5+2; int n;//节点数 vector<vector<int> > f_s(MAX_N);//father-son和son-father关系,存储双边 vector<int> first,second;//first[i]和second[i]分别表示以i节点为根节点时(i节点以下构成的子树)的最大和次大长度 int longest=0;//最终结果 //参数分别为当前节点,父亲节点 void dfs(int root,int father) { int sons=f_s[root].size(); for(int i=0;i<sons;i++) { if(f_s[root][i]!=father)//不能回溯 { dfs(f_s[root][i],root); if(first[f_s[root][i]]+1>first[root])//更新最大和次大 { second[root]=first[root]; first[root]=first[f_s[root][i]]+1; } else if(first[f_s[root][i]]+1>second[root])//只更新次大 second[root]=first[f_s[root][i]]+1; } } if(first[root]+second[root]>longest)//更新全局最大 longest=first[root]+second[root]; } void init_first_second() { first.assign(n+1,0); second.assign(n+1,0); } int main() { //freopen("input.txt","r",stdin); cin>>n; int a,b; for(int i=0;i<n-1;i++) { cin>>a>>b; f_s[a].push_back(b);//记录双边关系 f_s[b].push_back(a);//记录双边关系 } init_first_second(); dfs(1,-1);//使用任意一个节点DFS,结果都是一样的。 /*int ans=0; for(int i=1;i<=n;i++)//该循环可在DFS时完成 { if(first[i]+second[i]>ans) ans=first[i]+second[i]; }*/ cout<<longest; return 0; } [/cpp] 本代码提交AC,用时224MS,内存9MB。 第二种方法不太好理解,所以还是建议大家用第一种方法。 网上关于树的直径介绍了3种方法,可以参考这篇文章]]>

hihoCoder 1069-最近公共祖先·三

hihoCoder 1069-最近公共祖先·三 #1069 : 最近公共祖先·三 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 上上回说到,小Hi和小Ho使用了Tarjan算法来优化了他们的“最近公共祖先”网站,但是很快这样一个离线算法就出现了问题:如果只有一个人提出了询问,那么小Hi和小Ho很难决定到底是针对这个询问就直接进行计算还是等待一定数量的询问一起计算。毕竟无论是一个询问还是很多个询问,使用离线算法都是只需要做一次深度优先搜索就可以了的。 那么问题就来了,如果每次计算都只针对一个询问进行的话,那么这样的算法事实上还不如使用最开始的朴素算法呢!但是如果每次要等上很多人一起的话,因为说不准什么时候才能够凑够人——所以事实上有可能要等上很久很久才能够进行一次计算,实际上也是很慢的! “那到底要怎么办呢?在等到10分钟,或者凑够一定数量的人两个条件满足一个时就进行运算?”小Ho想出了一个折衷的办法。 “哪有这么麻烦!别忘了和离线算法相对应的可是有一个叫做在线算法的东西呢!”小Hi笑道。 小Ho面临的问题还是和之前一样:假设现在小Ho现在知道了N对父子关系——父亲和儿子的名字,并且这N对父子关系中涉及的所有人都拥有一个共同的祖先(这个祖先出现在这N对父子关系中),他需要对于小Hi的若干次提问——每次提问为两个人的名字(这两个人的名字在之前的父子关系中出现过),告诉小Hi这两个人的所有共同祖先中辈分最低的一个是谁? 提示:最近公共祖先无非就是两点连通路径上高度最小的点嘛! 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第1行为一个整数N,意义如前文所述。 每组测试数据的第2~N+1行,每行分别描述一对父子关系,其中第i+1行为两个由大小写字母组成的字符串Father_i, Son_i,分别表示父亲的名字和儿子的名字。 每组测试数据的第N+2行为一个整数M,表示小Hi总共询问的次数。 每组测试数据的第N+3~N+M+2行,每行分别描述一个询问,其中第N+i+2行为两个由大小写字母组成的字符串Name1_i, Name2_i,分别表示小Hi询问中的两个名字。 对于100%的数据,满足N<=10^5,M<=10^5, 且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人),所有询问中出现过的名字均在之前所描述的N对父子关系中出现过,且每个输入文件中第一个出现的名字所确定的人是其他所有人的公共祖先。 输出 对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:他们的所有共同祖先中辈分最低的一个人的名字。 样例输入 4 Adam Sam Sam Joey Sam Micheal Adam Kevin 3 Sam Sam Adam Sam Micheal Kevin 样例输出 Sam Adam Adam


这应该是最近公共祖先的最后一种算法了吧,hihoCoder还真是循序渐进啊,昨天刚刚学了RMQ-ST算法hihoCoder Problem 1068: RMQ-ST 算法,今天就用上了。 本题的解题思路也是非常的赞,首先把族谱树结构展开成一个数组B,当询问a,b节点的最近公共祖先时,在B中找到a,b最后出现的位置(也就是离开a,b节点的时候)an,bn,在区间[an,bn]上利用RMQ-ST算法求解区间深度最小的节点c,则c即为a,b的最近公共祖先,下面来具体讲解。 将族谱树展开成一个数组 为了利用RMQ-ST算法,需要将树形结构展开成一个数组,展开的方式是深度优先遍历树,将访问的节点顺序记录下来,不管是进入这个节点还是离开这个节点,都需要记录下来。比如下面一棵树: 是样例输入构成的一个家谱树,我们常规DFS这个树的序列是这样的:ASJMK。但是本题在进入和离开某个节点时都需要记录下来,而且每访问完一个子节点,要访问下一个子节点时,也需要记录当前父节点。比如访问完J准备访问M时,需要记录S,由此形成的序列为:ASJSMSAKA。 DFS生成的数组B长度大概为边数的两倍,所以复杂度级别只是O(n)的。 DFS之后就把树转换为了数组B[],因为使用RMQ-ST算法需要求某个区间的最小深度节点,所以在DFS的时候还需要记录每个节点的深度信息,而且还要记录每个节点最后访问的下标,这两点都不难,大家直接看代码即可。 RMQ-ST算法 得到数组B后就好办了,可以利用昨天学习的RMQ-ST算法求解2的幂区间长度间深度最小的节点,不过因为我们这题最终输出的是节点的名字而不是节点的深度,所以RMQ-ST比较的是深度,但存储的是节点名。 观察RMQ-ST算法的二重循环知道其复杂度为O(nlgn)。 在线求解 一切准备就绪后就可以求解了,首先根据字符串名字找到对应数字序号,然后查看两节点最后出现在树中的下标l,r,再讲[l,r]区间分成能覆盖[l,r]的2的幂的长度的半区间[l,t]和[t+1,r],最后求这两个区间最小深度的节点,输出其名称。 完整代码如下: [cpp] #include<iostream> #include<map> #include<string> #include<vector> #include<cmath> using namespace std; const int MAX_N=100010;//最大人数 map<string,int> name_index;//名字转换为数字 vector<string> index_name(MAX_N);//数字转换为名字 int n,m,k;//k全部记录将树转换成数组tree[]是的下标,转换结束时k即为tree[]的长度 vector<vector<int> > f_s(MAX_N);//f_s[i]表示第i个父亲的所有孩子 int tree[2*MAX_N];//DFS将树转换为的数组 int depth[MAX_N];//每个点在树中的深度 int rmq[MAX_N][30];//区间深度最低值 int last_show[MAX_N];//某个元素最后出现在tree中的位置(下标) //保存某个人的信息,并返回其下标 int store_name(string name) { map<string,int>::iterator it=name_index.find(name); if(it==name_index.end())//还不存在 { int curr_index=name_index.size();//用当前已有人数作为其下标,正好是递增的。 name_index.insert(make_pair(name,curr_index)); index_name[curr_index]=name;//记录 return curr_index;//返回下标 } else return it->second;//已经存在,直接返回 } //深度遍历树 void DFS(int root,int deep) { tree[k++]=root;//进入即记录 depth[root]=deep;//记录深度 int sons=f_s[root].size(); for(int i=0;i<sons;i++) { DFS(f_s[root][i],deep+1); tree[k++]=root;//每返回一次都要记录一次根节点 } last_show[root]=k-1;//root节点最后出现的位置 } //求a,b最小值 inline int get_min(int a,int b) { return a<b?a:b; } void init_rmq() { for(int j=0;j<k;j++) //rmq[j][0]=depth[tree[j]]; rmq[j][0]=tree[j];//保存的是这个节点,而不是它的深度,方便最后输出 int q=floor(log((double)k)/log(2.0)); for(int i=1;i<=q;i++) { for(int j=k-1;j>=0;j–) { rmq[j][i]=rmq[j][i-1]; if(j+(1<<(i-1))<k) { if(depth[rmq[j+(1<<(i-1))][i-1]]<depth[rmq[j][i]])//+优先级高于<<,所以改j+1<<(i-1)为j+(1<<(i-1)) rmq[j][i]=rmq[j+(1<<(i-1))][i-1]; } //rmq[j][i]=get_min(rmq[j][i],rmq[j+1<<(i-1)][i-1]); } } } int main() { //freopen("input.txt","r",stdin); cin>>n; string name1,name2; int index1,index2; while(n–) { cin>>name1>>name2; index1=store_name(name1); index2=store_name(name2); f_s[index1].push_back(index2); } k=0;//开始时树的下标为0,DFS完之后正好是tree[]数组的长度,可以作为rmq-st的n DFS(0,0); init_rmq(); cin>>m; int l,r,t; while(m–) { cin>>name1>>name2; if(name1==name2) { cout<<name1<<endl; continue; } index1=store_name(name1); index2=store_name(name2); l=last_show[index1]; r=last_show[index2]; if(l>r)//保证l<=r { int tmp=r; r=l; l=tmp; } t=floor(log(double(r-l+1))/log(2.0));//找到能使[l,r]分为两半的指数 cout<<index_name[get_min(rmq[l][t],rmq[r-(1<<t)+1][t])]<<endl;//r-(1<<t)+1,记得+1 } return 0; } [/cpp] 本代码提交AC,用时202MS,内存8MB。 相比于hihoCoder Problem 1067: 最近公共祖先·二,本题的的DFS+RMQ-ST算法是在线算法,也就是每来一个询问可以立即回答,而且复杂度为O(nlgn)还不错。 至此,hihoCoder的最近公共祖先问题完整结束,共使用三种算法,后两种算法都利用DFS,阅读时可以互相参照:最近公共祖先问题]]>

hihoCoder 1067-最近公共祖先·二

hihoCoder 1067-最近公共祖先·二 #1067 : 最近公共祖先·二 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 上上回说到,小Hi和小Ho用非常拙劣——或者说粗糙的手段山寨出了一个神奇的网站,这个网站可以计算出某两个人的所有共同祖先中辈分最低的一个是谁。远在美国的他们利用了一些奇妙的技术获得了国内许多人的相关信息,并且搭建了一个小小的网站来应付来自四面八方的请求。 但正如我们所能想象到的……这样一个简单的算法并不能支撑住非常大的访问量,所以摆在小Hi和小Ho面前的无非两种选择: 其一是购买更为昂贵的服务器,通过提高计算机性能的方式来满足需求——但小Hi和小Ho并没有那么多的钱;其二则是改进他们的算法,通过提高计算机性能的利用率来满足需求——这个主意似乎听起来更加靠谱。 于是为了他们第一个在线产品的顺利运作,小Hi决定对小Ho进行紧急训练——好好的修改一番他们的算法。 而为了更好的向小Ho讲述这个问题,小Hi将这个问题抽象成了这个样子:假设现小Ho现在知道了N对父子关系——父亲和儿子的名字,并且这N对父子关系中涉及的所有人都拥有一个共同的祖先(这个祖先出现在这N对父子关系中),他需要对于小Hi的若干次提问——每次提问为两个人的名字(这两个人的名字在之前的父子关系中出现过),告诉小Hi这两个人的所有共同祖先中辈分最低的一个是谁? 提示一:老老实实分情况讨论就不会出错的啦! 提示二:并查集其实长得很像一棵树你们不觉得么? 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第1行为一个整数N,意义如前文所述。 每组测试数据的第2~N+1行,每行分别描述一对父子关系,其中第i+1行为两个由大小写字母组成的字符串Father_i, Son_i,分别表示父亲的名字和儿子的名字。 每组测试数据的第N+2行为一个整数M,表示小Hi总共询问的次数。 每组测试数据的第N+3~N+M+2行,每行分别描述一个询问,其中第N+i+2行为两个由大小写字母组成的字符串Name1_i, Name2_i,分别表示小Hi询问中的两个名字。 对于100%的数据,满足N<=10^5,M<=10^5, 且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人),所有询问中出现过的名字均在之前所描述的N对父子关系中出现过,第一个出现的名字所确定的人是其他所有人的公共祖先。 输出 对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:他们的所有共同祖先中辈分最低的一个人的名字。 样例输入 4 Adam Sam Sam Joey Sam Micheal Adam Kevin 3 Sam Sam Adam Sam Micheal Kevin 样例输出 Sam Adam Adam


hihoCoder确实是个练习算法的好网站,之前我们写过朴素的最近公共祖先问题:hihoCoder Problem 1062: 最近公共祖先·一,但那是一个效率比较低的方法,这一次,hihoCoder详细给我们介绍了最近公共祖先的离线算法,而且讲解非常详细。 网站上的讲解详细,易于理解,我这里简单概括一下。所谓离线算法就是先把询问搜集起来,如果某个询问能先回答,则记录答案,直到把所有询问都解答了;与之相对的是在线算法,也就是每提出一个询问,我都要实时的马上回答。 离线算法综合了DFS和并查集。给定一个族谱(树形结构),我们先把每一个节点染成白色,并且在初始并查集中,每个点都是一个独立的集合,然后从树根开始深度优先遍历,当第一次经过某个节点a的时候,把它由白色改为灰色;如果第二次访问该节点的时候(也就是离开该点的时候),则把它由灰色改为黑色。 当a每次由白变灰之后,查看询问中是否出现过该节点,如果出现了,则查看询问中另一个节点b的颜色,如果b是灰色,则<a,b>的最近公共祖先就是b,因为b是灰色的,所以访问a之前肯定访问了b,所以它们的最近公共祖先是b;如果b是黑色,此时已经离开b了,并不能直接说明a和b的关系,但是<a,b>的最近公共祖先肯定在灰色链上,所以我们只要找出b向上的第一个灰色节点,就是<a,b>的最近公共祖先;如果b是白色,则说明b还没有访问,没有关于b的信息,所以我们暂时不管这个询问,等到访问到b时,a肯定不是白色,这是可以依据前两条规则得出它们的最近公共祖先。 当a每次由灰变黑之后,我们需要在并查集中将a和a的直接父节点合并UNION,因为当a由灰变黑时,a及其子树都访问结束了,但是a的直接父节点还没有访问结束,也就是说a的直接父节点还是灰色的,这样我们就记录了a向上的第一个灰色节点,这为上面的第二种情况判断做准备。 最近公共祖先的离线算法概括起来就是上面的内容,简单易懂也很强大。但是因为算法既包含DFS,又包含并查集,对初学者来说有一定的难度,而且这个题给出的数据是字符串形式的,数据量又很大,有很多细节需要注意。 我先把完整的代码贴出来,然后讲一下主要过程。 [cpp] #include<iostream> #include<string> #include<map> #include<vector> using namespace std; const int MAX_N=100010;//最大人数 map<string,int> name_index;//名字转换为数字 vector<string> index_name(MAX_N);//数字转换为名字 int color[MAX_N]={0};//0:白;1:灰;2:黑,初始颜色都是白色 int s_f[MAX_N];//son_father。s_f[i]表示第i个儿子的直接父亲 vector<vector<int> > f_s(MAX_N);//f_s[i]表示第i个父亲的所有孩子 vector<vector<int> > show_at(MAX_N);//show_at[i]表示第i个人在哪些询问里出现过 int q_l[MAX_N];//第i个询问的左边 int q_r[MAX_N];//第i个询问的右边 int father[MAX_N];//用于并查集,和s_f不一样 vector<string> ans;//最终结果 //保存某个人的信息,并返回其下标 int store_name(string name) { map<string,int>::iterator it=name_index.find(name); if(it==name_index.end())//还不存在 { int curr_index=name_index.size();//用当前已有人数作为其下标,正好是递增的。 name_index.insert(make_pair(name,curr_index)); index_name[curr_index]=name;//记录 father[curr_index]=curr_index;//初始的时候,并查集中每个元素都是一个独立的集合 return curr_index;//返回下标 } else return it->second;//已经存在,直接返回 } //并查集FIND操作 int find_father(int name) { return name==father[name]?name:(father[name]=find_father(father[name])); } //并查集UNION操作 void union_father(int name1,int name2) { father[find_father(name1)]=find_father(name2); } //深度遍历族谱树 void DFS(int root) { color[root]=1;//首先修改颜色为灰色 int show_time=show_at[root].size();//查看该元素是否在询问里出现 if(show_time!=0) { for(int i=0;i<show_time;i++)//判断所有询问 { int column=show_at[root][i]; int left=q_l[column];//找到该询问的左右元素 int right=q_r[column]; if(left==right)//如果是左右相同,则该自身即是其最近公共祖先 ans[column]=index_name[left]; else { if(left==root)//判断当前访问的是左边的还是右边的 { if(color[right]==1)//另一个点为灰色 ans[column]=index_name[right];//结果即为该灰色点 else if(color[right]=2)//另一个点为黑色 ans[column]=index_name[find_father(right)];//找其向上最近的灰色节点 } else { if(color[left]==1)//另一个点为灰色 ans[column]=index_name[left]; else if(color[left]=2)//另一个点为黑色 ans[column]=index_name[find_father(left)]; } } } } int sons=f_s[root].size();//查看是否有孩子节点 if(sons!=0) { for(int i=0;i<sons;i++) DFS(f_s[root][i]);//深度遍历其所有孩子 } color[root]=2;//改颜色为黑色 union_father(root,s_f[root]);//和直接父节点UNION } int main() { ios_base::sync_with_stdio(false);//取消同步,加速 cin.tie(0); //freopen("input.txt","r",stdin); int n,m; string name1,name2; int index1,index2; cin>>n; while(n–) { cin>>name1>>name2; index1=store_name(name1); index2=store_name(name2); f_s[index1].push_back(index2); s_f[index2]=index1; } cin>>m; ans.resize(m); for(int i=0;i<m;i++) { cin>>name1>>name2; index1=store_name(name1); index2=store_name(name2); q_l[i]=index1; q_r[i]=index2; show_at[index1].push_back(i); show_at[index2].push_back(i); } DFS(0); for(int i=0;i<m;i++) cout<<ans[i]<<endl; return 0; } [/cpp] 摆在我们面前的第一个问题就是数据的输入问题,题目给的每个人名都是字符串,当然我们可以直接用字符串构建一个树结构,然后进行查找、比较、赋值等操作,但是这样的时空效率都比较低,所以我们可以先用map<string,int> name_index和vector<string> index_name来记录人名字符串和人名数字下标,这样我们在DFS+并查集的时候可以只使用数字下标,到最后输出的时候再转换为字符串,提高效率。记录并返回人名数字下标的操作在函数int store_name(string name);中。 我们还需要记录父子关系,因为一个父亲可以有多个孩子,所以父亲-孩子关系需要用二维数组vector<vector<int> > f_s(MAX_N);来表示;而一个孩子只可能有一个父亲,所以孩子-父亲关系可以用一维数组int s_f[MAX_N];来表示。 还有两个数组需要初始化,每个节点的颜色初始化为白色,在并查集中每个节点单独为一个集合。 然后就是存储询问了。正如前面对LCA离线算法的描述,每当一个节点从白色变为灰色时,我们都要查看询问中是否出现过该节点。输入的询问中有很多节点,怎样来快速判断所有询问中是否有某个节点以及该询问中的另一个节点呢?如果这个问题处理不好,很可能就是TLE,反正我前两个版本都是因为这个原因TLE的。 这个问题可以通过两个步骤来解决,都是用空间来换时间。因为每一个询问都是由左右两个人名组成的,那么我们先把每次询问的左右两个人名记录在int q_l[MAX_N];和int q_r[MAX_N];中。然后还用一个二维数组vector<vector<int> > show_at(MAX_N);来记录某一个节点在哪些询问里出现过。这样当我们DFS到某个节点a时,首先查看show_at[a]这个数组是否为空,如果为空,说明a没有出现在任何一次询问中;如果不为空,则show_at[a]记录了a出现的所有询问的序号,根据这个序号,我们再去查q_l和q_r,这样就可以取出a出现时的那个询问的左右两个人名是什么了。所以整个过程只需要O(1)的时间,大大提高了时间效率。 因为题目中说明了
第一个出现的名字所确定的人是其他所有人的公共祖先。
所以我们以第一个人名下标0来DFS树结构。DFS的过程和上面的算法描述一致,我这里就不多说了,代码中都有详细的注释。 还有一个小问题需要注意,因为编译器不一样,在用vector声明二维数组的时候,我在VS2010下可以这样: [cpp] vector<vector<int>> a; //sth [/cpp] 但是提交之后会报Compile Error的错误:error: ‘>>’ should be ‘> >’ within a nested template argument list,也就是说这个编译器把>>看成了移位操作符,所以我们在两个箭头之间加一个空格> >这样就没问题了。 我上面的代码版本提交后AC,用时711MS,内存30MB。这是我迄今为止内存消耗最大的一次AC。 总的来说,这个题是好题,很考验选手的综合能力,特别是本题用到的各种数据结构较多,需要全盘考虑,不断优化。]]>

hihoCoder 1062-最近公共祖先·一

hihoCoder 1062-最近公共祖先·一 #1062 : 最近公共祖先·一 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Ho最近发现了一个神奇的网站!虽然还不够像58同城那样神奇,但这个网站仍然让小Ho乐在其中,但这是为什么呢? “为什么呢?”小Hi如是问道,在他的观察中小Ho已经沉迷这个网站一周之久了,甚至连他心爱的树玩具都弃置一边。 “嘿嘿,小Hi,你快过来看!”小Ho招呼道。 “你看,在这个对话框里输入我的名字,在另一个对话框里,输入你的名字,再点这个查询按钮,就可以查出来……什么!我们居然有同一个祖祖祖祖祖爷爷?” “诶,真是诶……这个网站有点厉害啊。”小Hi不由感叹道。 “是啊,这是什么算法啊,这么厉害!”小Ho也附和道。 “别2,我说的是他能弄到这些数据很厉害,而人类的繁殖树这种层数比较浅的树对这类算法的要求可是简单的不得了,你都能写出来呢!”小Hi道。 “啊?我也能写出来?可是……该从哪开始呢?”小Ho困惑了。 小Ho要面临的问题是这样的,假设现在他知道了N个人的信息——他们的父亲是谁,他需要对于小Hi的每一次提问——两个人的名字,告诉小Hi这两个人的是否存在同一个祖先,如果存在,那么他们的所有共同祖先中辈分最低的一个是谁? 提示:不着急,慢慢来,另外我有一个问题:挖掘机技术哪家强?! 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第1行为一个整数N,意义如前文所述。 每组测试数据的第2~N+1行,每行分别描述一对父子关系,其中第i+1行为两个由大小写字母组成的字符串Father_i, Son_i,分别表示父亲的名字和儿子的名字。 每组测试数据的第N+2行为一个整数M,表示小Hi总共询问的次数。 每组测试数据的第N+3~N+M+2行,每行分别描述一个询问,其中第N+i+2行为两个由大小写字母组成的字符串Name1_i, Name2_i,分别表示小Hi询问中的两个名字。 对于100%的数据,满足N<=10^2,M<=10^2, 且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人)。 输出 对于每组测试数据,对于每个小Hi的询问,输出一行,表示查询的结果:如果根据已知信息,可以判定询问中的两个人存在共同的祖先,则输出他们的所有共同祖先中辈分最低的一个人的名字,否则输出-1。 样例输入 11 JiaYan JiaDaihua JiaDaihua JiaFu JiaDaihua JiaJing JiaJing JiaZhen JiaZhen JiaRong JiaYuan JiaDaishan JiaDaishan JiaShe JiaDaishan JiaZheng JiaShe JiaLian JiaZheng JiaZhu JiaZheng JiaBaoyu 3 JiaBaoyu JiaLian JiaBaoyu JiaZheng JiaBaoyu LinDaiyu 样例输出 JiaDaishan JiaZheng -1


最近公共祖先问题:给定一个家谱图,问某两个人的最近的公共祖先是谁,如果没有公共祖先,输出-1。最简单的方法题目提示得很清楚了:
当然你也先将一个人的祖先全都标记出来,然后顺着另一个的父亲一直向上找,直到找到第一个被标记过的结点,便是它们的最近公共祖先结点了。
所以我就简单粗暴的使用STL的MAP和SET来解决问题了。假设需要判断的两个人名分别为son1和son2,那么①首先把*son1*及其所有祖先都找到并加入到一个set中,然后②首先判断son2在不在前面的set中,③如果不在,再依次判断son2的祖先在不在set中。 这里需要注意几点: 1. son1的祖先包括son1本身,比如第二个样例中的JiaBaoyu和JiaZheng,JiaZheng的祖先就包括JiaZheng自己,所以需要上面的第①和②步。 2. 为什么使用set而不是vector呢?因为②③步需要在son1的祖先中搜索,set内部用红黑树实现,搜索的效率高于vector。 知道了以上几点,就可以很快的写出代码了,完整代码如下: [cpp] #include<iostream> #include<map> #include<string> #include <set> using namespace std; int main() { //freopen("input.txt","r",stdin); int n,t; cin>>n; string son,father; map<string,string> son_father;//map[son]=father for(int i=0;i<n;i++) { cin>>father>>son; son_father.insert(make_pair(son,father)); } cin>>t; string son1,son2,comm_father; while(t–) { comm_father=""; cin>>son1>>son2; //vector<string> grand_fathers; set<string> grand_fathers;//使用set的查找效率高于vector grand_fathers.insert(son1);//注意,son1,son2本身也是一个祖先,一定要加进去,否则错! while(son_father.find(son1)!=son_father.end()) { grand_fathers.insert(son_father[son1]); son1=son_father[son1]; } if(grand_fathers.find(son2)!=grand_fathers.end())//首先son2本身也是一个祖先,所以先判断一下//使用set的查找效率高于vector { if(t!=0) cout<<son2<<endl; else cout<<son2; continue; } while(son_father.find(son2)!=son_father.end()) { if(grand_fathers.find(son_father[son2])!=grand_fathers.end()) { comm_father=son_father[son2];//因为是son2从下往上找,所以第一个出下的肯定是最近的公共祖先 break; } else son2=son_father[son2]; } if(t!=0) { if(comm_father=="") cout<<"-1"<<endl; else cout<<comm_father<<endl; } else { if(comm_father=="") cout<<"-1"; else cout<<comm_father; } } return 0; } [/cpp] 本代码提交AC,用时4MS,内存0MB。 之前说了,我这种解法是非常简单粗暴的,事实上最近公共祖先问题是一个经典问题,有很多经典算法可以解决,我后面会进一步研究。]]>

hihoCoder week10-1-后序遍历

hihoCoder week10-1-后序遍历 题目1 : 后序遍历 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 在参与过了美食节之后,小Hi和小Ho在别的地方又玩耍了一阵子,在这个过程中,小Ho得到了一个非常有意思的玩具——一棵由小球和木棍连接起来的二叉树! 小Ho对这棵二叉树爱不释手,于是给它的每一个节点都标记了一个标号——一个属于A..Z的大写字母,并且没有任意两个节点的标号是一样的。小Hi也瞅准了这个机会,重新巩固了一下小Ho关于二叉树遍历的基础知识~就这样,日子安稳的过了两天。 这天,小Ho正好在求解这棵二叉树的前序、中序和后序遍历的结果,但是却在求出前序遍历和中序遍历之后不小心把二叉树摔到了地上,小球和木棍等零件散落了一地! 小Ho损失了心爱的玩具,正要嚎啕大哭起来,所幸被小Hi发现了,劝说道:“别着急,这不是零件都还在么?拼起来不就是了?” “可是我忘记了二叉树长什么样子了!”小Ho沮丧道。 “这个简单,你不是刚刚求出了这棵二叉树的前序和中序遍历的结果么,利用这两个信息就可以还原出整棵二叉树来哦!” “这样么?!!”小Ho止住了泪水,问道:“那要怎么做呢?” 没错!小Ho在这一周遇到的问题便是:给出一棵二叉树的前序和中序遍历的结果,还原这棵二叉树并输出其后序遍历的结果。 提示:分而治之——化大为小,化小为无 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第一行为一个由大写英文字母组成的字符串,表示该二叉树的前序遍历的结果。 每组测试数据的第二行为一个由大写英文字母组成的字符串,表示该二叉树的中序遍历的结果。 对于100%的数据,满足二叉树的节点数小于等于26。 输出 对于每组测试数据,输出一个由大写英文字母组成的字符串,表示还原出的二叉树的后序遍历的结果。 样例输入 AB BA 样例输出 BA


解法一(已更新简洁版解法二) 简单来说就是根据前序遍历和中序遍历的结果求得后序遍历。这个题目如果用手算的话还简单些,但是要用程序实现,还真让我无从下手。不过可以明确的思路是:依次取前序遍历中的一个元素,找它在中序遍历中的位置,然后把中序遍历分成左右两部分,再在左右两部分递归求解。 递归的思路虽然简单,具体代码实现时有很多细节需要注意,我参考了这篇博文里面的用前序遍历和中序遍历重建二叉树的代码。但是代码好像有一点小问题,指针横飞,我根据《数据结构》P161整理如下: [cpp] #include<iostream> #include<string> #include<cstdlib> using namespace std; typedef struct node_t { char data; struct node_t* lchild; struct node_t* rchild; }node;//树的定义 //根据先序遍历和中序遍历还原树结构 void rebuild_tree_from_pre_in_order(string &pre_order,string in_order,int tree_len,node*& root)//注意node*&要带上&,表示node*root的引用,看数据结构P162 { if(pre_order==""||in_order=="") return; node *tmp=(node*)malloc(sizeof(node)); tmp->data=pre_order[0]; tmp->lchild=NULL; tmp->rchild=NULL; if(root==NULL) root=tmp;//这里改变了root指针的指向,所以传进来的时候要node*& root使用引用! if(tree_len==1) { pre_order=pre_order.substr(1); //每次取前序遍历的元素之后都要将其删除,并且要将结果带出去 return; } int i=0,left_len=0,right_len=0; while(pre_order[0]!=in_order[i])//找到先序遍历中的根在中序遍历中的位置,将中序遍历一分为二 i++; left_len=i; right_len=in_order.size()-i-1; pre_order=pre_order.substr(1);//每次取先序遍历的元素之后都要将其删除,如果tree_len!=1,则要先计算了left_len和right_len之后再删除 if(left_len>0) rebuild_tree_from_pre_in_order(pre_order,in_order.substr(0,left_len),left_len,root->lchild);//递归左子树 if(right_len>0) rebuild_tree_from_pre_in_order(pre_order,in_order.substr(i+1,right_len),right_len,root->rchild);//递归右子树 } //后序遍历 void post_order_print(node* root) { if(root!=NULL) { post_order_print(root->lchild); post_order_print(root->rchild); cout<<root->data; } } int main() { string pre_order,in_order; cin>>pre_order>>in_order; node* root=NULL; rebuild_tree_from_pre_in_order(pre_order,in_order,pre_order.size(),root); post_order_print(root); return 0; } [/cpp] 首先利用递归的思想重建二叉树,然后后序遍历输出。需要注意的是我把二叉树节点统一定义为node,重建函数 [cpp] void rebuild_tree_from_pre_in_order(string &pre_order,string in_order,int tree_len,node*& root) [/cpp] 传入的参数需要写成node*& root,也就是node* root指针的引用,如果是C语言里可以用二级指针,因为在这个函数内部对指针node* root进行了修改(if(root==NULL) root=tmp;),也就是改变了上一个node里的 node_t* lchild和node_t* rchild的指向,这是改变了指针的指向(指针本身的内容),所以需要传入指针的引用,如果只是想改变指针指向的内容node->data,可以只用node*,总之一句话,想改变什么就传入什么的指针,想改变int就传入int*或者int&,想改变node*就传入node**或者node*&,这个在链表里也有类似的应用。 还有一个通俗易懂的例子: [cpp] #include<stdio.h> #include<stdlib.h> #include<string.h> void get_memory(char*& buffer,int n) { buffer=(char*)malloc(sizeof(char)*n); } int main() { char *str=NULL; get_memory(str,20); strcpy(str,"Hello World!"); printf("%s",str); return 0; } [/cpp] 如果void get_memory(char*& buffer,int n);的形参buffer不是传入指针的引用的话,函数里malloc分配的内存只是分配给了char*的副本,并不会传出去,这样会导致内存泄露。 解法二(2015.4.16更新) 今天回头来看解法一真是笨拙,解法一先恢复了树的结构,然后求树的后序遍历。其实可以不用这么复杂,题目的提示已经很明确了,不知道当时为什么没有注意到。 具体解法是这样的,假设s,t分别为前序遍历和中序遍历,则s=根+左+右;t=左+根+右;根据s可以知道根节点,在t中查找根节点,可以把t分成左和右,因为s和t的左右孩子长度是一样的,所以也可以把s分成左和右。这样我们就分别有了左孩子和右孩子的前序和中序遍历,又可以递归上述过程。 通过这种方法,我们可以由前序和中序遍历直接得到后序遍历。具体代码如下: [cpp] #include<iostream> #include<string> using namespace std; //s前序,t中序 string get_post_order(string s,string t) { if(s=="") return ""; if(s.size()==1) return s; int i; for(i=0;i<t.size();i++) if(t[i]==s[0]) break; string s_left=s.substr(1,i); string s_right=s.substr(i+1);//如果i+1==长度,则返回空串,当i+1>长度时才抛出异常。 string t_left=t.substr(0,i); string t_right=t.substr(i+1); return get_post_order(s_left,t_left)+get_post_order(s_right,t_right)+s[0]; } int main() { string s,t; while(cin>>s>>t) cout<<get_post_order(s,t)<<endl; return 0; } [/cpp] 由于题目已经过期,没办法提交到hiho,我简单测试了几个例子,得到了正确的结果。]]>