Tag Archives: DFS

hihoCoder week 51-1-欧拉路·三

hihoCoder week 51-1-欧拉路·三 题目1 : 欧拉路·三 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi和小Ho破解了一道又一道难题,终于来到了最后一关。只要打开眼前的宝箱就可以通关这个游戏了。 宝箱被一种奇怪的机关锁住: 这个机关是一个圆环,一共有2^N个区域,每个区域都可以改变颜色,在黑白两种颜色之间切换。 小Ho控制主角在周围探索了一下,果然又发现了一个纸片: 机关黑色的部分表示为1,白色的部分表示为0,逆时针连续N个区域表示一个二进制数。打开机关的条件是合理调整圆环黑白两种颜色的分布,使得机关能够表示0~2^N-1所有的数字。 我尝试了很多次,终究没有办法打开,只得在此写下机关破解之法。 ——By 无名的冒险者 小Ho:这什么意思啊? 小Hi:我给你举个例子,假如N=3,我们通过顺时针转动,可以使得正下方的3个区域表示为: 因为黑色表示为1,白色表示为0。则上面三个状态分别对应了二进制(001),(010),(101) 每转动一个区域,可以得到一个新的数字。一共可以转动2^N次,也就是2^N个数字。我们要调整黑白区域的位置,使得这2^N个数字恰好是0~2^N-1 小Ho:我懂了。若N=2,则将环上的黑白色块调整为”黑黑白白”,对应了”1100″。依次是”11″,”10″,”00″,”01″四个数字,正好是0~3。那么这个”黑黑白白”就可以打开机关了咯? 小Hi:我想应该是的。 小Ho:好像不是很难的样子,我来试试! 提示:有向图欧拉回路 输入 第1行:1个正整数,N。1≤N≤15 输出 第1行:1个长度为2^N的01串,表示一种符合要求的分布方案 样例输入 3 样例输出 00010111


本题是欧拉路的判定、无向图欧拉路的求解的更进一步,有向图欧拉路的求解。 在有向图中找欧拉路的算法和无向图中相同,还是Fleury算法,只不过这一题需要自己构造有向图。构造的方法详见题目提示,我一开始根据
对于任意两个点,如果点i,点j满足点i的后n-2个数字和点j的前n-2个数字相同,则我们连接有向边(i,j)。
这句话来构造的,略显麻烦。因为
而我们构造的图,由构造方法可以知道对于任意一个点,其入度一定为2,出度一定为2。所以它必定存在欧拉回路。
所以对于每一个点,它有且只有两个出度,我们只需要求出这两个出度即可,不需要0~n-1一个一个点判断。用二进制表示,对于一个特定的点$$x=a_1a_2…a_n$$,假设x出边指向的点为y和z,则y,z的前n-1位和x的后n-1位是相同的,也即$$y=a_2a_3…a_{n-1}0, z=a_2a_3…a_{n-1}1$$。用公式表示就是y=(x<<1)&(111…1),x左移一位,与上n个1,z=y+1。 把有向图构造好之后,按部就班用Fleury算法求解,再把结果倒序输出,输出的时候只要输出每个点的最后一位就行了。 完整代码如下 [cpp] #include<iostream> #include<cstdio> #include<cstring> using namespace std; const int kMaxN = 16; int edge[1 << kMaxN][2]; int path[1 << kMaxN]; int path_size = 0; int n; void MakeGraph() { memset(edge, -1, sizeof(edge)); int nn = 1 << (n – 1); for (int i = 0; i < nn; i++) { edge[i][0] = (i << 1)&(nn – 1); edge[i][1] = edge[i][0] + 1; } } void DFS(int p) { for (int i = 0; i < 2; i++) { int q = edge[p][i]; if (q != -1) { edge[p][i] = -1; DFS(q); } } path[path_size++] = p; } int main() { scanf("%d", &n); if (n == 1) { printf("01"); return 0; } else if (n == 2) { printf("1100"); return 0; } MakeGraph(); DFS(0); while (–path_size) printf("%d", path[path_size] & 1); return 0; } [/cpp] 本代码提交AC,用时8MS,内存1MB。]]>

hihoCoder week 50-1-欧拉路·二

hihoCoder week 50-1-欧拉路·二 题目1 : 欧拉路·二 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 在上一回中小Hi和小Ho控制着主角收集了分散在各个木桥上的道具,这些道具其实是一块一块骨牌。 主角继续往前走,面前出现了一座石桥,石桥的尽头有一道火焰墙,似乎无法通过。 小Hi注意到在桥头有一张小纸片,于是控制主角捡起了这张纸片,只见上面写着: 将M块骨牌首尾相连放置于石桥的凹糟中,即可关闭火焰墙。切记骨牌需要数字相同才能连接。 ——By 无名的冒险者 小Hi和小Ho打开了主角的道具栏,发现主角恰好拥有M快骨牌。 小Ho:也就是说要把所有骨牌都放在凹槽中才能关闭火焰墙,数字相同是什么意思? 小Hi:你看,每一块骨牌两端各有一个数字,大概是只有当数字相同时才可以相连放置,比如: 小Ho:原来如此,那么我们先看看能不能把所有的骨牌连接起来吧。 提示:Fleury算法求欧拉路径 输入 第1行:2个正整数,N,M。分别表示骨牌上出现的最大数字和骨牌数量。1≤N≤1,000,1≤M≤5,000 第2..M+1行:每行2个整数,u,v。第i+1行表示第i块骨牌两端的数字(u,v),1≤u,v≤N 输出 第1行:m+1个数字,表示骨牌首尾相连后的数字 比如骨牌连接的状态为(1,5)(5,3)(3,2)(2,4)(4,3),则输出”1 5 3 2 4 3″ 你可以输出任意一组合法的解。 样例输入 5 5 3 5 3 2 4 2 3 4 5 1 样例输出 1 5 3 4 2 3


上一周学习了怎样判断欧拉通路,这周要给出一条具体的欧拉通路。 欧拉通路的求解使用Fleury算法,其伪代码如下: [cpp] DFS(u): While (u存在未被删除的边e(u,v)) 删除边e(u,v) DFS(v) End PathSize ← PathSize + 1 Path[ PathSize ] ← u [/cpp] 非常简洁漂亮,本题完整代码如下: [cpp] #include<iostream> #include<cstdio> using namespace std; const int kMaxN = 1005,kMaxM=5005; int n, m,path_size=0; int path[kMaxM];//是kMaxM而非kMaxN int edge[kMaxN][kMaxN]; void dfs(int p) { for (int i = 1; i <= n; i++) { if (edge[p][i]>0) { edge[p][i]–; edge[i][p]–; dfs(i); } } path[path_size++] = p; } int main() { //freopen("input.txt", "r", stdin); scanf("%d %d", &n, &m); int u, v; for (int i = 0; i < m; i++) { scanf("%d %d", &u, &v); edge[u][v]++;//防止有重边 edge[v][u]++; } dfs(1); for (int i = 0; i < path_size; i++) printf("%d ", path[i]); return 0; } [/cpp] 本代码提交AC,用时15MS,内存3MB。需要提醒的是,path的实际大小是m+1,而非n;另外为了防止有重边出现,edge用于记录边的数量,而非是否有边存在。 AC之后我还尝试了先找出欧拉通路的起点,再从起点开始dfs的方法,这种方法并没有更快,因为不论是从哪个点开始dfs,每个点的for循环都要完整执行,所以时间并不会减少,反而会由于寻找起点而增加时间。]]>

POJ 1386-Play on Words

POJ 1386-Play on Words Play on Words Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 10039 Accepted: 3441 Description Some of the secret doors contain a very interesting word puzzle. The team of archaeologists has to solve it to open that doors. Because there is no other way to open the doors, the puzzle is very important for us. There is a large number of magnetic plates on every door. Every plate has one word written on it. The plates must be arranged into a sequence in such a way that every word begins with the same letter as the previous word ends. For example, the word “acm” can be followed by the word “motorola”. Your task is to write a computer program that will read the list of words and determine whether it is possible to arrange all of the plates in a sequence (according to the given rule) and consequently to open the door. Input The input consists of T test cases. The number of them (T) is given on the first line of the input file. Each test case begins with a line containing a single integer number Nthat indicates the number of plates (1 <= N <= 100000). Then exactly Nlines follow, each containing a single word. Each word contains at least two and at most 1000 lowercase characters, that means only letters ‘a’ through ‘z’ will appear in the word. The same word may appear several times in the list. Output Your program has to determine whether it is possible to arrange all the plates in a sequence such that the first letter of each word is equal to the last letter of the previous word. All the plates from the list must be used, each exactly once. The words mentioned several times must be used that number of times. If there exists such an ordering of plates, your program should print the sentence “Ordering is possible.”. Otherwise, output the sentence “The door cannot be opened.”. Sample Input 3 2 acm ibm 3 acm malform mouse 2 ok ok Sample Output The door cannot be opened. Ordering is possible. The door cannot be opened. Source Central Europe 1999


给定一系列字符串,问能否将这些字符串依次首尾相连成一个长串,字符串a和b能够相连的条件就是a的最后一个字母和b的第一个字母相同。 这个题目很有意思,像小时候玩的扑克牌游戏“钓鱼”。我一开始想到的是方法是深度优先搜索DFS,在输入的时候将字符串按首字母不同分到不同的队列里面,然后枚举每一个字符串为第一个字符串,进行DFS,如果DFS结束之后遍历了所有字符串,则成功,否则失败。 用枚举+DFS的方法代码如下: [cpp] #include<iostream> #include<vector> #include<string> using namespace std; const int M=26; int n,used; vector<vector<string> > s; //vector<string> rs;//rs内字符串的先后顺序就是这个游戏的先后顺序 bool dfs(string last) { //rs.push_back(last);//记录字符串 used++; if(used==n) return true; else { int c=last[last.size()-1]-‘a’; int sz=s[c].size(); for(int i=0;i<sz;i++) { if(dfs(s[c][i])) return true; else used–;//rs.pop_back();//回溯 } return false; } } int main() { //freopen("input.txt","r",stdin); int t; string tmp; cin>>t; while(t–) { cin>>n; s.clear(); s.resize(M); for(int i=0;i<n;i++) { cin>>tmp; s[tmp[0]-‘a’].push_back(tmp); } bool success=false; for(int i=0;i<M;i++) { int sz=s[i].size(); for(int j=0;j<sz;j++) { used=0; success=false; if(dfs(s[i][j])) { success=true; break; } } if(success) break; } if(success) cout<<"Ordering is possible."<<endl; else cout<<"The door cannot be opened."<<endl; } return 0; } [/cpp] 这一版代码提交MLT,估计是DFS递归栈太深导致的,看来只能想其他办法了。 看了讨论后,发现大多数方法是欧拉路+并查集/DFS。我们先来复习一下欧拉路: 通过图(无向图或有向图)中所有边一次且仅一次,行遍图中所有顶点的通路称为欧拉通路,通过图中所有边一次且仅一次行遍所有顶点的回路称为欧拉回路。具有欧拉回路的图称为欧拉图(Euler Graph),具有欧拉通路而无欧拉回路的图称为半欧拉图。 根据定义,欧拉回路肯定是欧拉通路。相关定理有: 1.无向连通图G是欧拉图,当且仅当G不含奇数度结点(G的所有结点度数为偶数); 2.无向连通图G含有欧拉通路,当且仅当G有零个或两个奇数度的结点; 3.有向连通图D是欧拉图,当且仅当D的基图(即其无向图)为连通图且D中每个结点的入度=出度 4.有向连通图D含有欧拉通路,当且仅当D的基图(即其无向图)为连通图且D中除两个结点外,其余每个结点的入度=出度,且此两点满足deg-(u)-deg+(v)=±1。(起始点s的入度=出度-1,结束点t的出度=入度-1 或两个点的入度=出度) 5.一个非平凡连通图是欧拉图当且仅当它的每条边属于奇数个环。 6.如果图G是欧拉图且 H = G – uv,则H有奇数个u,v-迹仅在最后访问v;同时,在这一序列的u,v-迹中,不是路径的迹的条数是偶数。(Todia[1973]) 以上内容摘自百度百科·欧拉图;关于欧拉路,还可以参考这篇文章。对于本题,我们可以把一个单词的首尾字母看成图的一条边,比如增加”acm”这个单词,则在图上增加一条a->m的有向边。要使所有字符串组成一个首尾相连的长串,等价于在a~z这26个字符串为点构成的图中只存在一条欧拉通路。判断是否存在欧拉通路可以用上面的定理3和4;判断是否只有一条欧拉通路(也就是判断基图是否连通)就需要借助并查集或者DFS,如果用并查集,要保证所有有边的点属于同一个集合;如果用DFS,则从图的某个点出发能遍历到所有右边的点。我使用的是并查集。 理清思路之后就可以开始写代码了,欧拉路+并查集版本的代码如下: [cpp] #include<iostream> #include<string> using namespace std; const int M=26; int n; int showed[M];//showed[i]表示点i出现过,也就是有边连在i点 int G[M][M];//图 int father[M];//并查集 int degree[M][2];//degree[i][0]入度;degree[i][1]出度 void init_data() { for(int i=0;i<M;i++) { father[i]=i; showed[i]=0; degree[i][0]=0; degree[i][1]=0; for(int j=0;j<M;j++) G[i][j]=0; } } int find_father(int x) { return x==father[x]?x:(father[x]=find_father(father[x])); } void union_father(int x,int y) { father[find_father(x)]=find_father(y); } //判断有边的点是否属于一个集合 bool is_one_set() { int root=-1; for(int i=0;i<M;i++) { if(showed[i]==1)//有边 { int tmp=find_father(i); if(root==-1) root=tmp; else if(tmp!=root) return false; } } return true; } bool judge() { if(!is_one_set()) return false; else { for(int i=0;i<M;i++) { for(int j=0;j<M;j++) { if(G[i][j]!=0) { degree[i][1]+=G[i][j]; degree[j][0]+=G[i][j]; } } } int a=0,b=0;//a:入度-出度=-1的个数;b:入度-出度=1的个数 for(int i=0;i<M;i++) { int rs=degree[i][0]-degree[i][1]; if(rs==-1) a++; else if(rs==1) b++; else if(rs!=0) return false; } if((a==1&&b==1)||(a==0&&b==0))//欧拉回路或欧拉通路 return true; return false; } } int main() { //freopen("input.txt","r",stdin); int t,a,b; string tmp; cin>>t; while(t–) { cin>>n; init_data(); for(int i=0;i<n;i++) { cin>>tmp; a=tmp[0]-‘a’; b=tmp[tmp.size()-1]-‘a’; showed[a]=1; showed[b]=1; G[a][b]++;//首字母指向尾字母的边 union_father(a,b);//首字母指向尾字母的边 } if(judge()) cout<<"Ordering is possible."<<endl; else cout<<"The door cannot be opened."<<endl; } return 0; } [/cpp] 本代码提交AC,用时782MS,内存244K。]]>

hihoCoder 1070-RMQ问题再临

hihoCoder 1070-RMQ问题再临 #1070 : RMQ问题再临 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 终于,小Hi和小Ho踏上了回国的旅程。在飞机上,望着采购来的特产——小Hi陷入了沉思:还记得在上上周他们去超市的时候,前前后后挑了那么多的东西,都幸运的没有任何其他人(售货员/其他顾客)来打搅他们的采购过程。但是如果发生了这样的事情,他们的采购又会变得如何呢? 于是小Hi便向小Ho提出了这个问题:假设整个货架上从左到右摆放了N种商品,并且依次标号为1到N,每次小Hi都给出一段区间[L, R],小Ho要做的是选出标号在这个区间内的所有商品重量最轻的一种,并且告诉小Hi这个商品的重量。但是在这个过程中,可能会因为其他人的各种行为,对某些位置上的商品的重量产生改变(如更换了其他种类的商品),面对这样一个问题,小Ho又该如何解决呢? 提示:平衡乃和谐之理 输入 每个测试点(输入文件)有且仅有一组测试数据。 每组测试数据的第1行为一个整数N,意义如前文所述。 每组测试数据的第2行为N个整数,分别描述每种商品的重量,其中第i个整数表示标号为i的商品的重量weight_i。 每组测试数据的第3行为一个整数Q,表示小Hi总共询问的次数与商品的重量被更改的次数之和。 每组测试数据的第N+4~N+Q+3行,每行分别描述一次操作,每行的开头均为一个属于0或1的数字,分别表示该行描述一个询问和描述一次商品的重量的更改两种情况。对于第N+i+3行,如果该行描述一个询问,则接下来为两个整数Li, Ri,表示小Hi询问的一个区间[Li, Ri];如果该行描述一次商品的重量的更改,则接下来为两个整数Pi,Wi,表示位置编号为Pi的商品的重量变更为Wi 对于100%的数据,满足N<=10^4,Q<=10^4, 1<=Li<=Ri<=N,1<=Pi<=N, 0<weight_i, Wi<=10^4。 输出 对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:标号在区间[Li, Ri]中的所有商品中重量最轻的商品的重量。 样例输入 10 618 5122 1923 8934 2518 6024 5406 1020 8291 2647 6 0 3 6 1 2 2009 0 2 2 0 2 10 1 1 5284 0 2 5 样例输出 1923 2009 1020 1923


本题的数据量比hihoCoder Problem 1068: RMQ-ST 算法这题要小,虽然
用O(N)的时间进行计算——扫描一遍整个区间找到最小值,然后对于每一个更改操作,使用O(1)的时间直接进行修改,这样也就是O(NQ)的总时间复杂度!在这种数据量下完全是可以通过的!
但是本题希望我们用线段树来解决。 我曾在《数据结构》的改造红黑树中看到过区间树,但是本题的线段树和书中的区间树有所区别,区间树由红黑树改造而来,结构更复杂些,这里只需要使用线段树即可。 常规的线段树如下图所示: 从图中可以看到构造线段树的过程就是一个二分的过程,不断将区间分成两半,直到只有一个元素。图中的线段树每一个节点是一个区间[l,r],本题我稍微改造了一下,改成了数组int seg_tree[left][length],比如seg_tree[i][j]表示从下标i开始,长度为j的这样一个区间上的最小值,这样就可以利用线段树来解决RMQ问题了。比如改造后的线段树就成了下面的样子: 因为树形这种特殊的结构,我们可以用一个DFS来对树实现二分构造,当DFS到某个节点长度为1时,其最小值就是w[i]本身,在回溯到父节点时,父节那个区间的最小值又是所有子节点最小值中的最小值。因为树的总节点数大约为2*n,所以复杂度O(n)。 当需要查询区间[l,r]的最小值时,只需对数组seg_tree二分搜索。具体来说,假设我们搜索到了节点[s_l,s_len],如果r<(s_l+s_len/2),说明区间[l,r]全在[s_l,s_len]的左边,我们递归在[s_l,s_len/2]区间找;如果l>=(s_l+s_len/2),说明区间[l,r]全在[s_l,s_len]的右边,我们递归在[s_l+s_len/2,s_len-s_len/2]区间找;如果以上两者都不是,说明[l,r]跨界了,而且中点下标一定是s_l+s_len/2,所以我们分别在二两半区间找,然后求这两者的最小值。复杂度O(lgn)。 当需要更新某个下标为pos的值为value时,也是DFS查找线段树,直到找到叶子seg_tree[pos][1],更新它的值,以及所有我们在查找过程经过的父节点的值。复杂度O(lgn)。 所以线段是的性质使得无论是构造、查询、更新操作,复杂度都只要O(lgn),这就是题目中所说的把总的复杂度平均分配到不同操作:平衡乃和谐之理。 完整代码如下: [cpp] #include<iostream> using namespace std; const int MAX_N=1e4+2; int w[MAX_N];//每个商品重量 int n,m; int seg_tree[MAX_N][MAX_N];//seg_tree[i][j]:起点为i,长度为j的区间的最小值 inline int get_min(int a,int b) { return a<b?a:b; } //深度优先遍历以构造线段树 void dfs(int left,int length) { if(length==1) { seg_tree[left][1]=w[left]; return; } dfs(left,length/2); dfs(left+length/2,length-length/2); seg_tree[left][length]=get_min(seg_tree[left][length/2],seg_tree[left+length/2][length-length/2]);//取最小值 } //在区间[s_left,s_len]搜索区间[left,length]的最小值 int search_min(int s_left,int s_len,int left,int length) { if((s_left==left)&&(s_len==length)) return seg_tree[s_left][s_len]; if((left+length-1)<(s_left+s_len/2))//全在左半部分 { return search_min(s_left,s_len/2,left,length); } else if(left>=(s_left+s_len/2))//全在右半部分 { return search_min(s_left+s_len/2,s_len-s_len/2,left,length); } else//左右分开搜索 { int left_len=s_left+s_len/2-left; int right_len=length-left_len; int min_left=search_min(s_left,s_len/2,left,left_len); int min_right=search_min(s_left+s_len/2,s_len-s_len/2,s_left+s_len/2,right_len); return get_min(min_left,min_right); } } //从区间[s_left,s_len]开始更新下标pos的值为value void update(int s_left,int s_len,int pos,int value) { if((s_left==pos)&&(s_len==1)) { seg_tree[s_left][1]=value; return ; } int mid=s_left+s_len/2; if(pos<mid) update(s_left,s_len/2,pos,value); else update(mid,s_len-s_len/2,pos,value); seg_tree[s_left][s_len]=get_min(seg_tree[s_left][s_len/2],seg_tree[mid][s_len-s_len/2]);//更新父节点 } int main() { //freopen("input.txt","r",stdin); cin>>n; for(int i=1;i<=n;i++) cin>>w[i]; dfs(1,n); cin>>m; int p,l,r; for(int i=0;i<m;i++) { cin>>p>>l>>r; if(p==0)//查询 { cout<<search_min(1,n,l,r-l+1)<<endl; } else//修改 { update(1,n,l,r); } } return 0; } [/cpp] 本代码提交AC,用时151MS,内存42MB。 ]]>

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。 总的来说,这个题是好题,很考验选手的综合能力,特别是本题用到的各种数据结构较多,需要全盘考虑,不断优化。]]>

POJ 1094-Sorting It All Out

POJ 1094-Sorting It All Out Sorting It All Out Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 28397 Accepted: 9814 Description An ascending sorted sequence of distinct values is one in which some form of a less-than operator is used to order the elements from smallest to largest. For example, the sorted sequence A, B, C, D implies that A < B, B < C and C < D. in this problem, we will give you a set of relations of the form A < B and ask you to determine whether a sorted order has been specified or not. Input Input consists of multiple problem instances. Each instance starts with a line containing two positive integers n and m. the first value indicated the number of objects to sort, where 2 <= n <= 26. The objects to be sorted will be the first n characters of the uppercase alphabet. The second value m indicates the number of relations of the form A < B which will be given in this problem instance. Next will be m lines, each containing one such relation consisting of three characters: an uppercase letter, the character “<” and a second uppercase letter. No letter will be outside the range of the first n letters of the alphabet. Values of n = m = 0 indicate end of input. Output For each problem instance, output consists of one line. This line should be one of the following three: Sorted sequence determined after xxx relations: yyy…y. Sorted sequence cannot be determined. Inconsistency found after xxx relations. where xxx is the number of relations processed at the time either a sorted sequence is determined or an inconsistency is found, whichever comes first, and yyy…y is the sorted, ascending sequence. Sample Input 4 6 A<B A<C B<C C<D B<D A<B 3 2 A<B B<A 26 1 A<Z 0 0 Sample Output Sorted sequence determined after 4 relations: ABCD. Inconsistency found after 2 relations. Sorted sequence cannot be determined. Source East Central North America 2001


这一题有一定的难度,有很多细节需要兼顾。题目的意思为:给定一系列小于关系,问能否从中得出一个严格有序的序列。需要明确的是,题目所指的严格有序的序列需要满足如下几个条件:1.包含n个所有字母;2.每个字母都有明确的大小关系。 另外根据测试样例我们可以知道:每输入一个关系都要判断一次,如果可以得到严格有序序列,则输出结果,但这个测试用例后续的输入还是要输入的;如果出现相互依赖(环),也输出结果,同样的,这个测试用例后面的数据还是要输入;如果所有关系都输入了还不能得出一个严格有序的序列,则输入无法判断。 这一题很自然的想到用拓扑排序来做。下面我们先来回顾一下拓扑排序。 如上图是《算法导论》中关于拓扑排序的一个例子,这是某位教授起床后需要穿戴衣物的顺序,比如只有先穿了袜子才能穿鞋、但是穿鞋和戴手表并没有严格的先后顺序。拓扑排序的功能就是根据图(a)的拓扑图,得到图(b)中节点的先后顺序。 常见的拓扑排序算法有两种:Kahn算法和基于DFS的算法,这两种算法都很好理解,详细的解释请看维基百科Topological sorting,下面简要介绍一下这两个算法。 Kahn算法 拓扑排序强调先后顺序,先做的事情排在前面,那么反应到有向图上,最先做的事情就是没有入度的节点(可能 有/没有 出度)。所以Kahn算法很朴素,他就是把所有没有入度的点加入到集合S中,在S不为空的情况下循环:从S中取出一个节点a,把a加入到L链表的尾部,对于每一条a的出度边,如< a,b>,删除它,并把b的入度减一,如果b的入度也为0了,则又把b加入到集合S中。如果S为空了,但是图中还有边未删除,说明图中至少有一个,拓扑排序失败;否则拓扑排序成功,且链表L保存了一条拓扑排序链。(链表L的生成使用尾插法) 维基百科上关于Kahn算法的伪代码描述如下: [cpp] L ← Empty list that will contain the sorted elements S ← Set of all nodes with no incoming edges while S is non-empty do remove a node n from S add n to tail of L for each node m with an edge e from n to m do remove edge e from the graph if m has no other incoming edges then insert m into S if graph has edges then return error (graph has at least one cycle) else return L (a topologically sorted order) [/cpp] 基于DFS的算法 这个算法类似于深度优先搜索,它不断的递归访问下一个节点,直到不能再递归时,把最后一个节点插入到链表的头部,然后返回。 拿上面穿衣物的例子来说,如果我们首先选择了内裤,则可以递归访问裤子->腰带->夹克。到夹克时,无法递归了,则把夹克插入到L的头部,返回到腰带,腰带也无法递归了,则把腰带插入到L的头部,这个时候就形成了L:腰带->夹克的中间结果,也就是腰带要在夹克前面穿。这个算法不太好表述,还请大家看伪代码领会。(链表L的生成使用头插法) [cpp] L ← Empty list that will contain the sorted nodes while there are unmarked nodes do select an unmarked node n visit(n) function visit(node n) if n has a temporary mark then stop (not a DAG) if n is not marked (i.e. has not been visited yet) then mark n temporarily for each node m with an edge from n to m do visit(m) mark n permanently unmark n temporarily add n to head of L [/cpp] 那么问题来了,只使用拓扑排序并不能满足本题的要求。因为本题问的是能否生成一个严格有序的序列,一个拓扑序列并不一定是严格有序的序列,比如上图中的穿衣物生成的拓扑序列中,先穿袜子和先穿衬衫都是可以的,也就是说袜子和衬衫是没有明确的先后顺序的,所以这不是一个严格有序的序列。再比如下图的拓扑序列,也不是一个严格有序的序列,因为C和A,D,E,F无法比较大小。 那么怎么判断生产的拓扑序列是否是严格的有序序列呢?基本原则就是就是任意取序列中的两个点,看能不能比较大小,如果能则是严格有序,否则不是。 我起初想到的是对拓扑序列的第一个节点进行深度遍历,遍历之后如果所有的节点都访问了,那么这是一个严格有序的序列,否则不是。后来证明这是不正确的,比如上图从B点开始DFS,遍历完F之后回溯到B点再访问C点,这样即使它不是严格有序的,但DFS还是访问了所有节点。 后来想到了Floyd算法。对拓扑图进行Floyd算法之后,会得到任意两点之间的最短距离。如果拓扑序列中前面的节点都可以到达后面的节点(最短距离不为无穷),则是严格有序的;否则不是。比如上图的一个拓扑序列为BCADEF(不唯一,还可以是BADEFC),但是C到ADEF的最短距离都是无穷,所以这个序列不是严格有序的。 把这些大的问题搞清楚之后就可以写代码了,一些小细节可以看我代码里的注释。 [cpp] #include<iostream> //#include<set> #include<list> #include<string> #include<vector> using namespace std; int n,m; const int MAX_N=26; const int MAX_DIS=10000; //*******这些都是维基百科关于拓扑排序(DFS版)里的变量含义 int temporary[MAX_N]; int permanent[MAX_N]; int marked[MAX_N]; //******************************* int path[MAX_N][MAX_N]; //int dfs_visited[MAX_N]; list<int> L;//拓扑排序生产的顺序链 bool isDAG;//DAG=directed acyclic graph,无回路有向图 //每一个测试用例都要初始化路径 void init_path() { for(int i=0;i<n;i++) for(int j=0;j<n;j++) path[i][j]=0; } //每一次拓扑排序都要初始化temporary,permanent,marked void init_tpm() { isDAG=true; L.clear(); for(int i=0;i<n;i++) { temporary[i]=0; permanent[i]=0; marked[i]=0; } } //递归访问。具体看维基百科 void visit(int i) { if(temporary[i]==1) { isDAG=false; return; } else { if(marked[i]==0) { marked[i]=1; temporary[i]=1; for(int j=0;j<n;j++) { if(path[i][j]==1) { visit(j); } } permanent[i]=1; temporary[i]=0; L.push_front(i); } } } /* void init_dfs() { for(int i=0;i<n;i++) dfs_visited[i]=0; }*/ /* //DFS有缺陷 void DFS(int v) { if(dfs_visited[v]==0) { dfs_visited[v]=1; for(int i=0;i<n;i++) { if(dfs_visited[i]==0&&path[v][i]==1) { DFS(i); } } } }*/ //使用Floyd算法来判断生产的拓扑排序是否是严格有序的 bool Floyd() { int copy_path[MAX_N][MAX_N]; for(int i=0;i<n;i++)//首先复制一份路径图 for(int j=0;j<n;j++) { copy_path[i][j]=path[i][j]; if(i!=j&&copy_path[i][j]==0)//如果原来两点距离为0,说明他们是不直接连通的 copy_path[i][j]=MAX_DIS;//置为无穷 } //floyd算法 for(int k=0;k<n;k++) for(int i=0;i<n;i++) for(int j=0;j<n;j++) if(copy_path[i][j]>copy_path[i][k]+copy_path[k][j]) copy_path[i][j]=copy_path[i][k]+copy_path[k][j]; vector<int> seq;//把原来用链表的拓扑序列转换成数组,方便后面的操作 list<int>::iterator it=L.begin(); while(it!=L.end()) { seq.push_back(*it); it++; } //如果这个拓扑链是严格有序的话,则前面的元素到后面的元素一定是可达的。 for(int i=0;i<n-1;i++) { for(int j=i+1;j<n;j++) { if(copy_path[seq[i]][seq[j]]>=MAX_DIS)//如果不可达,则不是严格有序的。 return false; } } return true; } //拓扑排序DFS版本。返回0:有回路;1:虽然是拓扑排序,但非连通(不是严格有序);2:是拓扑排序且连通(严格有序)(即任意两个元素都可以比较大小) int topological_sorting() { for(int i=0;i<n;i++) { if(marked[i]==0) { visit(i); } } if(!isDAG) return 0; else { /*init_dfs(); DFS(*L.begin()); for(int i=0;i<n;i++) { if(dfs_visited[i]==0) { return 1; } }*/ if(Floyd()) return 2; else return 1; } } int main() { //freopen("input.txt","r",stdin); string tmp; while(cin>>n>>m&&n&&m) { init_path(); vector<string> relations(m); int i; for(i=0;i<m;i++)//一次性把所有输入都存起来,免得后续麻烦 { cin>>relations[i]; } int rs=-1; for(i=0;i<m;i++)//每增加一个关系,都要重新拓扑排序一次 { init_tpm();//每次都要初始化 tmp=relations[i]; path[tmp[0]-‘A’][tmp[2]-‘A’]=1; rs=topological_sorting(); if(rs==0) { cout<<"Inconsistency found after "<<i+1<<" relations."<<endl; break;//如果是回路的话,后续的输入可以跳过 } else if(rs==2)//成功 { cout<<"Sorted sequence determined after "<<i+1<<" relations: "; list<int>::iterator it=L.begin(); while(it!=L.end()) { char c=’A’+*it; cout<<c; it++; } cout<<"."<<endl; break;//后续输入跳过 } } if(i==m&&rs==1)//如果处理完所有输入都没有形成严格有序的拓扑序列 cout<<"Sorted sequence cannot be determined."<<endl; } return 0; } [/cpp] 我原本以为又是DFS,又是Floyd,算法时空效率会很低,但是提交后AC,用时125MS,内存244K,也不是很差。 代码中的拓扑排序算法使用的是基于DFS的版本,大家也可以改成Kahn算法。 如果觉得自己的代码是对的,但是提交WA的,可以使用这两个测试数据:数据1数据2]]>

POJ 2676-Sudoku

POJ 2676-Sudoku Sudoku Time Limit: 2000MS Memory Limit: 65536K Total Submissions: 14104 Accepted: 6963 Special Judge Description Sudoku is a very simple task. A square table with 9 rows and 9 columns is divided to 9 smaller squares 3×3 as shown on the Figure. In some of the cells are written decimal digits from 1 to 9. The other cells are empty. The goal is to fill the empty cells with decimal digits from 1 to 9, one digit per cell, in such way that in each row, in each column and in each marked 3×3 subsquare, all the digits from 1 to 9 to appear. Write a program to solve a given Sudoku-task. Input The input data will start with the number of the test cases. For each test case, 9 lines follow, corresponding to the rows of the table. On each line a string of exactly 9 decimal digits is given, corresponding to the cells in this line. If a cell is empty it is represented by 0. Output For each test case your program should print the solution in the same format as the input data. The empty cells have to be filled according to the rules. If solutions is not unique, then the program may print any one of them. Sample Input 1 103000509 002109400 000704000 300502006 060000050 700803004 000401000 009205800 804000107 Sample Output 143628579 572139468 986754231 391542786 468917352 725863914 237481695 619275843 854396127 Source Southeastern Europe 2005


这一题是练习深度搜索和剪枝的好题目。题意很简答,数独游戏,在每一行、每一列和每一个小正方形填入1-9这9个数字,数字不能重复,求一种可以的方案。 因为只需要求一种可行解,根据POJ Problem 3278: Catch That Cow后面的总结,很快有了深度搜索的思路:从上到下,从左到右依次扫描每一个格子,如果这个格子上的数字不是0,说明原来就有数字了,不用填,测试下一个格子;如果这个格子为0,则求出可填入该格子的候选数字,再依次测试每一个候选数字是否可行,一直这样深度搜索下去,直到将所有格子测试完毕。这个思路相信很多人都能很快想到,完整的代码如下: [cpp] #include<iostream> #include<vector> using namespace std; const int N=9; int board[N][N];//棋盘 int candidates[N];//对于某一个坐标ij,可以填入的候选数字 //给定坐标(row,column),求它所在的小方格的左上角坐标 void get_subquare_start(int row,int column,int& x0,int& y0) { if(row<3&&column<3) { x0=0; y0=0; } else if(row<3&&column<6) { x0=0; y0=3; } else if(row<3&&column<9) { x0=0; y0=6; } else if(row<6&&column<3) { x0=3; y0=0; } else if(row<6&&column<6) { x0=3; y0=3; } else if(row<6&&column<9) { x0=3; y0=6; } else if(row<9&&column<3) { x0=6; y0=0; } else if(row<9&&column<6) { x0=6; y0=3; } else if(row<9&&column<9) { x0=6; y0=6; } } //给定坐标(row,column),将可以填入该坐标的候选数字放入vi //思路就是先把candidates[N]填满1-9,然后将给定坐标的 //横轴、纵轴、小方格出现过的数字去掉,剩下的数字就是 //候选数字 void get_candidates(int row,int column,vector<int>& vi) { for(int i=0;i<N;i++) candidates[i]=i+1; for(int i=0;i<N;i++)//横轴 { if(board[row][i]!=0) candidates[board[row][i]-1]=0; } for(int i=0;i<N;i++)//纵轴 { if(board[i][column]!=0) candidates[board[i][column]-1]=0; } int x0,y0; get_subquare_start(row,column,x0,y0); for(int i=x0;i<x0+3;i++)//小方格 { for(int j=y0;j<y0+3;j++) { if(board[i][j]!=0) candidates[board[i][j]-1]=0; } } for(int i=0;i<N;i++)//重复工作,可优化 if(candidates[i]!=0) vi.push_back(candidates[i]); } //深度搜索 bool dfs(int pos) { if(pos==N*N) { return true; } int row=pos/N; int column=pos%N; if(board[row][column]!=0)//如果有数字 return dfs(pos+1);//则搜下一个格子 else { vector<int> local_candidates;//要使用局部的candidates,因为如果是全局的,那么回溯的时候candidates已经修改了 get_candidates(row,column,local_candidates);//获取可填入该格子的候选数字 int cand_num=local_candidates.size(); for(int i=0;i<cand_num;i++) { board[row][column]=local_candidates[i];//填入 if(dfs(pos+1))//继续搜下一个格子 return true; } board[row][column]=0;//回溯 return false;//回溯 } } int main() { freopen("input.txt","r",stdin); int t; char c; cin>>t; while(t–) { for(int i=0;i<N;i++) { for(int j=0;j<N;j++) { cin>>c; board[i][j]=c-‘0’; } } if(dfs(0)) { for(int i=0;i<N;i++) { for(int j=0;j<N;j++) cout<<board[i][j]; cout<<endl; } } } return 0; } [/cpp] 满心欢喜的提交后发现TLE超时了,此时泪流满面,后来又琢磨了一下,师兄给的分类是简单搜索技巧和剪枝,我上面的代码压根就没有剪枝,TLE也就不足为奇了。 那么怎样剪枝?想一想我们手算解数独的过程,正常情况下,如果A这个格子有5个候选数字可以填入,而B这个格子只有2个候选数字可以填入,我们肯定会先填好B这个格子,再填A,因为B的情况少一些,而填完B之后,A的情况可能又会减少,这样比先填A再填B会快得多。根据这样的思路,我们在每次深度搜索的时候,把每个空格的候选数字求出来,并求候选数字个数最少的那个格子C,填格子C,继续深度搜索,直到所有空格填满为止。 可能有的同学会问,每次深度搜索的时候都要求一遍所有空格的候选数字,这样会不会耗时太多了,这个我也担心过,但是总体来说,剪枝比第一个版本的代码高效多了。下面是剪枝版本的完整代码: [cpp] #include<iostream> #include<vector> using namespace std; const int N=9; int board[N][N];//棋盘 int candidates[N];//对于某一个坐标ij,可以填入的候选数字 //给定坐标(row,column),求它所在的小方格的左上角坐标 void get_subquare_start(int row,int column,int& x0,int& y0) { if(row<3&&column<3) { x0=0; y0=0; } else if(row<3&&column<6) { x0=0; y0=3; } else if(row<3&&column<9) { x0=0; y0=6; } else if(row<6&&column<3) { x0=3; y0=0; } else if(row<6&&column<6) { x0=3; y0=3; } else if(row<6&&column<9) { x0=3; y0=6; } else if(row<9&&column<3) { x0=6; y0=0; } else if(row<9&&column<6) { x0=6; y0=3; } else if(row<9&&column<9) { x0=6; y0=6; } } //给定坐标(row,column),将可以填入该坐标的候选数字放入vi //思路就是先把candidates[N]填满1-9,然后将给定坐标的 //横轴、纵轴、小方格出现过的数字去掉,剩下的数字就是 //候选数字 void get_candidates(int row,int column,vector<int>& vi) { for(int i=0;i<N;i++) candidates[i]=i+1; for(int i=0;i<N;i++)//横轴 { if(board[row][i]!=0) candidates[board[row][i]-1]=0; } for(int i=0;i<N;i++)//纵轴 { if(board[i][column]!=0) candidates[board[i][column]-1]=0; } int x0,y0; get_subquare_start(row,column,x0,y0); for(int i=x0;i<x0+3;i++)//小方格 { for(int j=y0;j<y0+3;j++) { if(board[i][j]!=0) candidates[board[i][j]-1]=0; } } for(int i=0;i<N;i++)//重复工作,可优化 if(candidates[i]!=0) vi.push_back(candidates[i]); } //深度搜索 bool dfs(vector<int>& blank_pos) { int blank_num=blank_pos.size(); if(blank_num==0)//如果空格都填完了,返回true { return true; } int row,column,pos; int min_candidates=N+1; vector<int> global_candidates; for(int i=0;i<blank_num;i++) { row=blank_pos[i]/N; column=blank_pos[i]%N; vector<int> local_candidates; get_candidates(row,column,local_candidates);//对于每一个空格求可填入的候选数字 if(local_candidates.size()<min_candidates)//求到最小值 { pos=blank_pos[i];//记录位置 min_candidates=local_candidates.size(); global_candidates.clear(); global_candidates=local_candidates;//vector可以直接赋值 } } row=pos/N; column=pos%N; vector<int> next_blank_pos; for(int j=0;j<blank_num;j++) if(blank_pos[j]!=pos) next_blank_pos.push_back(blank_pos[j]);//剩余的空格 for(int i=0;i<min_candidates;i++) { board[row][column]=global_candidates[i];//填入 if(dfs(next_blank_pos))//继续搜下一个格子 return true; } board[row][column]=0;//回溯 return false;//回溯 } //剪枝版本,AC,用时282MS,内存240K int main() { //freopen("input.txt","r",stdin); int t; char c; cin>>t; vector<int> blank_pos; while(t–) { blank_pos.clear();//因为可能有多个case,所以每次都要clear!!! for(int i=0;i<N;i++) { for(int j=0;j<N;j++) { cin>>c; board[i][j]=c-‘0’; if(board[i][j]==0) blank_pos.push_back(i*N+j); } } if(dfs(blank_pos)) { for(int i=0;i<N;i++) { for(int j=0;j<N;j++) cout<<board[i][j]; cout<<endl; } } } return 0; } [/cpp] 这个版本的代码需要注意一个小细节,我在每次输入的时候都将空格的位置记录在了blank_pos的vector中,因为题目说有多个case,所以每次输入的时候都要记得blank_pos.clear(),要不然会影响下一个case。这个版本的代码AC,用时282MS,内存240K。 AC之后,又看了一下discuss,发现很多同学谈到如果倒搜,会比顺搜快得多,倒搜就是从最后一个格子开始搜索。闲来无事,我就把第一个版本的代码改成了倒搜索,只需要修改两三个地方,代码如下: [cpp] #include<iostream> #include<vector> using namespace std; const int N=9; int board[N][N];//棋盘 int candidates[N];//对于某一个坐标ij,可以填入的候选数字 //给定坐标(row,column),求它所在的小方格的左上角坐标 void get_subquare_start(int row,int column,int& x0,int& y0) { if(row<3&&column<3) { x0=0; y0=0; } else if(row<3&&column<6) { x0=0; y0=3; } else if(row<3&&column<9) { x0=0; y0=6; } else if(row<6&&column<3) { x0=3; y0=0; } else if(row<6&&column<6) { x0=3; y0=3; } else if(row<6&&column<9) { x0=3; y0=6; } else if(row<9&&column<3) { x0=6; y0=0; } else if(row<9&&column<6) { x0=6; y0=3; } else if(row<9&&column<9) { x0=6; y0=6; } } //给定坐标(row,column),将可以填入该坐标的候选数字放入vi //思路就是先把candidates[N]填满1-9,然后将给定坐标的 //横轴、纵轴、小方格出现过的数字去掉,剩下的数字就是 //候选数字 void get_candidates(int row,int column,vector<int>& vi) { for(int i=0;i<N;i++) candidates[i]=i+1; for(int i=0;i<N;i++)//横轴 { if(board[row][i]!=0) candidates[board[row][i]-1]=0; } for(int i=0;i<N;i++)//纵轴 { if(board[i][column]!=0) candidates[board[i][column]-1]=0; } int x0,y0; get_subquare_start(row,column,x0,y0); for(int i=x0;i<x0+3;i++)//小方格 { for(int j=y0;j<y0+3;j++) { if(board[i][j]!=0) candidates[board[i][j]-1]=0; } } for(int i=0;i<N;i++)//重复工作,可优化 if(candidates[i]!=0) vi.push_back(candidates[i]); } //深度搜索 bool dfs(int pos) { if(pos==-1)//倒搜到-1结束 { return true; } int row=pos/N; int column=pos%N; if(board[row][column]!=0)//如果有数字 return dfs(pos-1);//则搜下一个格子 else { vector<int> local_candidates;//要使用局部的candidates,因为如果是全局的,那么回溯的时候candidates已经修改了 get_candidates(row,column,local_candidates);//获取可填入该格子的候选数字 int cand_num=local_candidates.size(); for(int i=0;i<cand_num;i++) { board[row][column]=local_candidates[i];//填入 if(dfs(pos-1))//继续搜下一个格子 return true; } board[row][column]=0;//回溯 return false;//回溯 } } //倒搜版本,AC,用时125MS,内存224K int main() { //freopen("input.txt","r",stdin); int t; char c; cin>>t; while(t–) { for(int i=0;i<N;i++) { for(int j=0;j<N;j++) { cin>>c; board[i][j]=c-‘0’; } } if(dfs(N*N-1))//倒搜 { for(int i=0;i<N;i++) { for(int j=0;j<N;j++) cout<<board[i][j]; cout<<endl; } } } return 0; } [/cpp] 令我吃惊的是,倒搜这个版本的代码AC了,而且用时125MS,内存224K,居然比剪枝还要快,内存占用也更少。到现在我还没有搞清楚更快的原因。]]>

POJ 1321-棋盘问题

POJ 1321-棋盘问题 棋盘问题 Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 23209 Accepted: 11512 Description 在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C。 Input 输入含有多组测试数据。 每组数据的第一行是两个正整数,n k,用一个空格隔开,表示了将在一个n*n的矩阵内描述棋盘,以及摆放棋子的数目。 n <= 8 , k <= n 当为-1 -1时表示输入结束。 随后的n行描述了棋盘的形状:每行有n个字符,其中 # 表示棋盘区域, . 表示空白区域(数据保证不出现多余的空白行或者空白列)。 Output 对于每一组数据,给出一行输出,输出摆放的方案数目C (数据保证C< 2^31)。 Sample Input 2 1 #. .# 4 4 …# ..#. .#.. #… -1 -1 Sample Output 2 1 Source 蔡错@pku


这题可以看成是深度搜索的样题或者经典题。有点类似八皇后问题,题目要求在棋盘上能放棋子的区域放上规定数目的棋子,且保证同行和同列最多放一个棋子,问一共有多少种解法。 本题的限制条件就是同行和同列最多放一个棋子,那么我们在搜索的时候就可以以行为单位搜索,每次测试一行,这样就保证同一行最多放了一个棋子,那么怎样保证同列最多放一个棋子呢?我们可以设置一个数组`int col[]`数组,`col[i]=0`表示该行还没有放棋子;`col[i]=1`表示该行已经放了棋子了。这样我们在搜索的时候就可以这样: [cpp] if(board[i][j]==’#’&&col[j]==0)//如果可以放棋子,且这一列上还没有放过棋子 { col[j]=1;//则在该处放棋子 dfs(i,placed_num+1);//继续往下搜索 col[j]=0;//回溯 } [/cpp] 本题的思路和代码都不难,对于深度搜索的题目,还是要多做,做多了自然会摸索出其中的固定模式。下面是完整代码: [cpp] #include<iostream> using namespace std; int n,k,ans; char board[9][9];//棋盘 int col[9];//每一列是否放置了棋子,0没放;1放了。 //深度搜索,row表示上一次搜索的行号,placed_num表示已经放了几个棋子了 void dfs(int row,int placed_num) { if(placed_num==k)//如果所有棋子都放好了,则找到一个解 { ans++; return; } for(int i=row+1;i<n;i++)//否则的话,从下一行开始搜索每一行 { for(int j=0;j<n;j++)//对该行的所有位置(列) { if(board[i][j]==’#’&&col[j]==0)//如果可以放棋子,且这一列上还没有放过棋子 { col[j]=1;//则在该处放棋子 dfs(i,placed_num+1);//继续往下搜索 col[j]=0;//回溯 } } } } //初始的时候,每一列都没有放棋子 void init_col() { for(int i=0;i<9;i++) col[i]=0; } int main() { while(cin>>n>>k&&n!=-1&&k!=-1) { ans=0; init_col(); for(int i=0;i<n;i++) for(int j=0;j<n;j++) cin>>board[i][j]; dfs(-1,0);//row=-1;placed_num=0 cout<<ans<<endl; } return 0; } [/cpp] 本代码AC,用时32MS,内存216K。]]>