看了poj 1236后没啥思路,网上搜了下,才大概知道了解法。要用到缩点,入度,出度的概念。打算先按照《数据结构与算法分析》里的算法写一个,要用两次DFS,估计性能比较差吧,先试试看。妈蛋,这一试写了2天,o(╯□╰)o,还好一次AC。
题目大意:有n个学校,学校之间可以传递信息,例如学校 a 可以传达信息给 b , 即a ——> b , 但 b 不一定 能传递信息给 a 。 告诉你每个学校能够向哪些学校传递信息,然后有两个问题:
问题一:至少要向几个学校传递原始信息,才能保证所有学校都能收到信息。
问题二:至少要添加多少组关系(每组关系类型如右:a 可以向 b 传递信息),才能保证给任意一个学校原始信息后,其他所有学校都能收到信息,也即使得整个图变成是强连通的。
解题思路:这道题其实就是一个有n个顶点的有向图,先计算出强连通分量, 然后分别统计出入度为 0 和出度为 0 的强连通分量个数ansA 和 ansB,那么, 问题一的答案就是ansA , 问题二的答案就是max(ansA , ansB),但是有特例:当只有一个强连通分量时,问题二的答案就是0 。(其实原理还不懂o(╯□╰)o)
使用两次DFS,采用并查集存储强连通分量。刚刚通过了,一次AC,好开心!!就是代码有点长。
/* poj 1236 time : 0ms memory: 228k 此程序完全是借鉴《数据结构与算法分析》这本书的思路,数据结构是自己想的,用邻接表存储图(主要考虑到图为稀疏图),用最大堆完成第二次DFS的节点选取顺序,用并查集存储强连通分量。 本以为用了两次DFS,时间会很差,没想到时间还挺快的,试了下从网上down下来的用Tarjan算法,时间和内存都差不多。。可能与图的存储方式以及该题的样例大小有关吧 */ #include <stdio.h> const int MaxNode = 101; const int MaxEdge = 10001; typedef int PriorityQueue[MaxNode]; //优先队列(最大堆)用来第二次DFS的节点选取 typedef int DisjSet[MaxNode]; //用并查集存储强连通分量 struct NodeEntry { int e, next; }; int Index[MaxNode]; //通过第一次的DFS计算每个节点的序号, int index; int Vis[MaxNode]; //是否已被访问 int Ptr[MaxNode]; //每个节点链表当前访问到的地方,避免重复扫描 int in[MaxNode]; //强连通分量入度(实际只有每个强连通分量的根节点有值) int out[MaxNode]; //出度 NodeEntry G[MaxNode + MaxEdge]; //原图 NodeEntry G_r[MaxNode + MaxEdge]; //翻转后的图 PriorityQueue H; DisjSet s; int max_degree(int a, int b) { return a > b ? a : b; } void Init(int N) { int i, k, id; index = 1; for(k = 1; k <= N; ++k) { G[k].next = 0; G_r[k].next = 0; Vis[k] = 0; Ptr[k] = k; //指针初始化 H[k] = k; //堆初始化 s[k] = -1; //并查集初始化 in[k] = 0; //出入度初始化为0,表示出入度分别为0,当不为0时,用1来表示 out[k] = 0; } for(i = 1; i <= N; ++i) { while(1) { scanf("%d", &id); if(!id) break; G[k].e = id; G[k].next = G[i].next; G[i].next = k; G_r[k].e = i; G_r[k].next = G_r[id].next; G_r[id].next = k++; } } } int Find(int v) { if(s[v] < 0) return v; else return s[v] = Find(s[v]); } void Union(int root1, int root2) { if(s[root1] < s[root2]) { s[root1] += s[root2]; s[root2] = root1; } else { s[root2] += s[root1]; s[root1] = root2; } } void PercolateDown(int N, int i) { int FirstElement = H[i]; int child; for(; 2 * i <= N; i = child) { child = 2 * i; if(2 * i + 1 <= N && Index[H[2 * i + 1]] > Index[H[2 * i]]) ++child; if(Index[H[child]] > Index[FirstElement]) H[i] = H[child]; else break; } H[i] = FirstElement; } int DeleteMax(int N) { int i, child; int MaxElement = H[1]; int LastElement = H[N]; for(i = 1; i * 2 <= N; i = child) { child = 2 * i; if(2 * i + 1 <= N && Index[H[2 * i + 1]] > Index[H[i * 2]]) ++child; if(Index[LastElement] < Index[H[child]]) H[i] = H[child]; else break; } H[i] = LastElement; return MaxElement; } void DFS_index(int N, int i) //第一次DFS,计算每个节点的index { Vis[i] = 1; while(G[Ptr[i]].next) { Ptr[i] = G[Ptr[i]].next; //改变指针位置 if(!Vis[G[Ptr[i]].e]) //光这个函数我写加调试用了一天,起初直接 i = G[Ptr[i]].e,结果啊。。。 { //在一个子函数中i是不变的,表示在第i个节点链表的遍历 DFS_index(N, G[Ptr[i]].e); Index[G[Ptr[i]].e] = index++; } } } void DFS(int N, int i) //第二次DFS,合并强连通节点 { int root1, root2; Vis[i] = 1; while(G_r[Ptr[i]].next) { Ptr[i] = G_r[Ptr[i]].next; if(!Vis[G_r[Ptr[i]].e]) { root1 = Find(i); root2 = Find(G_r[Ptr[i]].e); Union(root1, root2); DFS(N, G_r[Ptr[i]].e); } } } int main() { int i, j, N, root1, root2, k = 0; //k用来计强连通分量的个数 int Non_in_cnt = 0, Non_out_cnt = 0, //Non_in_cnt,Non_out_cnt分别用来计出入度为0的连通分量的个数 scanf("%d", &N); Init(N); for(i = 1; i <= N; ++i) { if(!Vis[i]) { DFS_index(N, i); Index[i] = index++; } } for(i = N / 2; i > 0; --i) PercolateDown(N, i); for(i = 1; i <= N; ++i) //一定注意在第二次DFS之前再初始化一下 { Vis[i] = 0; Ptr[i] = i; } j = N; while(j) { i = DeleteMax(j--); if(!Vis[i]) { DFS(N, i); } } for(i = 1; i <= N; ++i) //将出入度不为0的分量对应的in[],out[]改为1,遍历了所有点和边,感觉可以再优化下 { Ptr[i] = i; root1 = Find(i); while(G[Ptr[i]].next) { Ptr[i] = G[Ptr[i]].next; root2 = Find(G[Ptr[i]].e); if(root1 != root2) { in[root2] = 1; out[root1] = 1; } } } for(i = 1; i <= N; ++i) //计算出入度为0的分量的个数 { if(s[i] < 0) { ++k; if(in[i] == 0) ++Non_in_cnt; if(out[i] == 0) ++Non_out_cnt; } } if(k == 1) //注意当整个图为强连通时,第二个问题的答案为0,这是个特殊情况 printf("%d\n%d\n", Non_in_cnt, 0); else printf("%d\n%d\n", Non_in_cnt, max_degree(Non_in_cnt, Non_out_cnt)); return 0; }心得:这道题花了我2天时间,那个DFS是无参考手写,花了一天时间,感觉自己对递归的理解还是不够,递归先要保证原始函数的完整,再将递归的发生看作是中断,加入到函数的适当位置。
调试的过程还学习到了printf()函数,开始打算用其查看整个过程的当前状态,就因为忽略了其刷新缓冲流的条件,printf()在很多地方并未实际输出到屏幕上,调试过程显得很诡异,哎。。现在论坛里看到如下:
用printf()输出时是先输出到缓冲区,然后再从缓冲区送到屏幕上。
1. 使用fflush(stdout)强制刷新。
2.缓冲区已满。
3.scanf()要在缓冲区里取数据时会先将缓冲区刷新。
4.\n,\r进入缓冲区时。
5.线程结束的时候,如果该线程里也有printf(....);
6. 程序结束时。
用了书上的算法后,打算再学习一下Tarjan算法。百度到一篇博文,感觉还不错,算法学习可以参见BYV的文章。该文真的讲的蛮好的,看了一会儿,发现Num(u),Low(u)等的变量,不就是对书上割点那部分内容的衍生吗(但是Low()的算法貌似略有不同),顿时有了兴趣。再看文章的过程中,我才发现,原来书上的算法叫做Kosaraju算法,也是一种常用的求强连通分量的算法,时间复杂度与Tarjan算法一样,都是O(N + M),且该算法更直观一些,不过Tarjan算法只用一次DFS,而且不用建立逆图,更简洁。在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求最近公共祖先的离线Tarjan算法。
Tarjan算法打算用在poj 2186这道题。
题意分析: 每个Cow都梦想成为牛群里最知名的奶牛,在一个有N(1 <= N <= 10,000) 头牛的牛群里,给出最多M(1 <= M <= 50,000) 个数对(A,B)。
告诉你A认为B是有名的,并且名气可以传递,若果A认为B有名,B认为C有名,则A也会认为C有名,即使在输入的数对中没有给出这段关系。
你的任务是计算出被所有牛认为是有名的牛的个数.
看到Headacher的题解才恍然大悟。
思路分析:
1、假设a和b都是最受欢迎的cow,那么,a欢迎b,而且b欢迎a,于是,a和b是属于同一个连通分量内的点,所有,问题的解集构成一个强连通分量。
2、如果某个强连通分量内的点a到强连通分量外的点b有通路,因为b和a不是同一个强连通分量内的点,所以b到a一定没有通路,那么a不被b欢迎,于是a所在的连通分量一定不是解集的那个连通分量。
3、如果存在两个独立的强连通分量a和b,那么a内的点和b内的点一定不能互相到达,那么,无论是a还是b都不是解集的那个连通分量,问题保证无解。
4、如果图非连通,那么,至少存在两个独立的连通分量,问题一定无解。
原图将会是很大的,我们可以先找到图里的强连通分量,然后使用缩点可以将原图变成DAG,如果出度为 0 的强连通分量的数目只有一个,那么,该强连通分量即是我们所求的(原理可以自己画几个图试一试),就输出这个强连通分量内解的个数,若出度为 0 的强连通分量个数大于1,则无解。另外,出度为 0 的强连通分量个数不可能为 0(若为 0,即每个分量至少一条出边,那么DAG图一定有环,与DAG图自相矛盾)
/* poj 2186 time: 94ms memory:792k */ #include <stdio.h> const int MaxN = 10010; const int MaxM = 50010; struct NodeEntry { int e, next; }; NodeEntry G[MaxN + MaxM]; //存储图 int Index; //用于计算Num int Cnt; //计算强连通分量的个数 int Vis[MaxN]; int Instack[MaxN]; //是否在栈里的flag int Ptr[MaxN]; //当前节点邻接链表的指针位置,避免重复扫描 int Belong[MaxN]; //此处用Belong数组代替了并查集,Belong数组里存得数字是其所在强连通分量的编号 int Num[MaxN]; int Low[MaxN]; int Stack[MaxN]; //用Stack数组进行简单的栈使用 int Ptr_stack; //当前栈顶位置 int min_Low(int a, int b) { return b < a ? b : a; } void Init(int N, int M) { int i, j = N + 1, n1, n2; Ptr_stack = 0; Index = 0; Cnt = 1; for(i = 1; i <= N; ++i) { G[i].next = 0; Vis[i] = 0; Ptr[i] = i; Belong[i] = 0; Instack[i] = 0; } for(i = 1; i <= M; ++i) { scanf("%d%d", &n1, &n2); G[j].e = n2; G[j].next = G[n1].next; G[n1].next = j++; } } void Tarjan(int i) { int j; Vis[i] = 1; Num[i] = Low[i] = ++Index; Stack[++Ptr_stack] = i; Instack[i] = 1; while(G[Ptr[i]].next) { Ptr[i] = G[Ptr[i]].next; if(!Vis[G[Ptr[i]].e]) { Tarjan(G[Ptr[i]].e); Low[i] = min_Low(Low[i], Low[G[Ptr[i]].e]); } else if(Instack[G[Ptr[i]].e]) Low[i] = min_Low(Low[i], Num[G[Ptr[i]].e]); } if(Num[i] == Low[i]) { do { j = Stack[Ptr_stack--]; Instack[j] = 0; Belong[j] = Cnt; } while(i != j); ++Cnt; } } int main() { int N, M, i, k, id; k = 0; scanf("%d%d", &N, &M); Init(N, M); for(i = 1; i <= N; ++i) if(!Vis[i]) Tarjan(i); int *out = new int[Cnt]; for(i = 1; i < Cnt; ++i) out[i] = 0; for(i = 1; i <= N; ++i) { Ptr[i] = i; while(G[Ptr[i]].next) { Ptr[i] = G[Ptr[i]].next; if(Belong[i] != Belong[G[Ptr[i]].e]) { out[Belong[i]] = 1; break; } } } for(i = 1; i < Cnt; ++i) { if(!out[i]) { id = i; ++k; } } if(k > 1) printf("0\n"); else { k = 0; for(i = 1; i <= N; ++i) { if(Belong[i] == id) ++k; } printf("%d\n", k); } delete[] out; return 0; }