Tarjan
一、强联通分量tarjan
/*
dfn数组:用来记录dfs深搜的顺序,表示第几个被访问的 low数组:用来记当前节点可以访问的更早的时间戳,不一定是最早的,例如上图中low[9]最早的应该是1,但是却显示3,这个没关 系,因为最终输出一个强联通分量是根据dfn_i == low_i来截止出栈 stk栈:用来记录dfs序列,当某个点所有子孙都深搜过了,并且当前节点low_i == dfn_i,说明找到了以i为根节点的强联通分量,出栈知道遇到i结点,记录下这些出栈的点,属于同一个SCC。 instk数组:用来记录当前节点是否在栈中,因为上图中枚举x的邻点时,如果该点dfn != 0(说明已经深搜过了)并且 instk[i] == 0(0表示不在栈中),这种情况表名i结点属于其他的SCC并且已经出栈了,就没必要考虑i节点了。
*/
代码实现:
#include<bits/stdc++.h> using namespace std; using ll = long long; const int N = 2e5 + 10; int t = 1; // 时间戳,从1开始 vector<int> vec[N]; //用来存图 stack<int> st; //记录深搜中相连点(可以在这个序列中找到SCC,因为强联通分量一定是相连的(联通的)) bool isinst[N]; //记录点是否在栈中 int dfn[N], low[N]; // tarjan必不可少数组,记录时间戳和能回溯的时间戳(SCC根节点low == dfn) int cnt = -1; //cnt记录有多少个SCC vector<int> scc[N]; //记录scc[cnt]前SCC中的结点,总共有cnt个 void tarjan(int x) { cout << "now visit x: " << x << '\n'; // 入 x dfn[x] = low[x] = t++; //每次深搜一个结点时,给他赋上时间戳,low[x]通过遍历完所有子孙来更新low值 isinst[x] = true; // 当前是 入 ,表示在栈中 st.push(x); // 枚举x邻点 for(auto& v : vec[x]) { if(dfn[v] == 0) { cout << "now visit x son v(v isn't visited'): " << v << '\n'; tarjan(v); low[x] = min(low[x], low[v]); cout << "now low[" << x << "] is1: " << low[x] << '\n'; } else if(isinst[v]) { cout << "now visit x son v(v is visited'): " << v << '\n'; low[x] = min(low[x], dfn[v]); // 统一用dfn[v],用low[v]在某种情况下会出错,学完所有再解释 cout << "now low[" << x << "] is2: " << low[x] << '\n'; } } // 离 x if(low[x] == dfn[x]) { //所有的子孙儿子遍历完了都没能回溯到更早的结点,说明该节点是强联通分量 int top; //记录每次栈顶元素 cnt++; do { top = st.top(); scc[cnt].push_back(y); st.pop(); isinst[y] = false; //出栈后记得标记不在栈中 }while(top != x); //用dowhile方便,先存在进栈 } } /*(n, m), 接着输出6条边,这里每条边只输入了两个节点。 5 6 1 2 2 3 3 4 4 5 5 3 3 1 */ int main() { int n, m; cin >> n >> m; for(int i = 1; i <= m; i++) { int a, b; cin >> a >> b; vec[a].push_back(b); } for(int x = 1; x <= n; x++) { //防止图不是联通的,所以对每个点判断一次 if(dfn[x] == 0) tarjan(x); } for(int i = 0; i <= cnt; i++) { //输出所有的SCC for(auto& v : scc[i]) cout << v << ' '; cout << '\n'; } return 0; }
二、tarjan SCC 缩点
[P2812 校园网络【USACO]Network of Schools加强版】 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这里有很多细节需要注意,因为我写这个题wa了一个小时
首先是利用tarjan进行图的化简,将强联通点化成一个点,用cnt来记录有多少个联通块(新图点数)
如何确定加几条边是的x(x > 1)个点互相联通: 遍历x个点,记录当中入度为0的点的数量in0, 和出度为0的点的数量out0,然后取max(in0, out0);
!!!特判:对于cnt == 1的情况,这种时候只有一个联通块,不需要在加边,加边数应该为0
#include<iostream> #include<stack> #include<vector> using namespace std; const int N = 1e6 + 10; vector<int> e[N], ne[N]; int t; bool isinst[N]; int dfn[N], low[N]; stack<int> st; int scc[N], cnt; int in[N], out[N], dp[N]; void tarjan(int x) { isinst[x] = true; st.push(x); dfn[x] = low[x] = ++t; for(auto& v : e[x]) { if(!dfn[v]) { tarjan(v); low[x] = min(low[x], low[v]); } else if(isinst[v]) { low[x] = min(low[x], dfn[v]); } } if(dfn[x] == low[x]) { cnt++; // cout << "x: " << x << "cnt: " << cnt << '\n'; int y; do { y = st.top(); scc[y] = cnt; st.pop(); isinst[y] = false; }while(y != x); } } int main() { int n; cin >> n; for(int i = 1; i <= n; i++) { int a; cin >> a; while(a != 0) { e[i].push_back(a); cin >> a; } } for(int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i); for(int i = 1; i <= n; i++) { int x = scc[i]; for(auto& v : e[i]) { if(scc[v] != x) { ne[x].push_back(scc[v]); out[x]++; in[scc[v]]++; } } } int num = 0; int in0 = 0, out0 = 0; for(int i = 1; i <= cnt; i++) { if(in[i] == 0) in0++; if(out[i] == 0) out0++; // 这里之前加了else一直过不了最后一个点 /* C:\Users\86188\Documents\Tencent Files\1823809284\FileRecv 测试点在我电脑该文件下的P2182 */ } if(cnt == 1) cout << in0 << '\n' << '0'; else cout << in0 << '\n' << max(in0, out0); return 0; }
[P2341 USACO03FALL / HAOI2006] 受欢迎的牛 G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
tarjan缩点后,明星牛牛一定出现在出度为0的点上。because 在tarjan缩点后的新图一定不是联通图,如果存在某个明星出度不为0,则说明该明星牛牛和出度指向的牛牛可以构成联通块,这样不符合tarjan。所以找出度为0的点
#include<bits/stdc++.h> using namespace std; const int N = 1e4 + 10; vector<int> e[N]; int t, cnt, dfn[N], low[N], scc[N], siz[N], dout[N]; bool isinst[N]; stack<int> st; void tarjan(int x) { dfn[x] = low[x] = ++t; st.push(x); isinst[x] = true; for(int v : e[x]) { if(!dfn[v]) { tarjan(v); low[x] = min(low[x], low[v]); } else if(isinst[v]) { low[x] = min(low[x], dfn[v]); } } if(dfn[x] == low[x]) { int y; cnt++; do{ y = st.top(); scc[y] = cnt; st.pop(); isinst[y] = false; siz[cnt]++; }while(y != x); } } int main() { int n, m; cin >> n >> m; for(int i = 1; i <= m; i++) { int a, b; cin >> a >> b; e[a].push_back(b); } for(int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i); for(int x = 1; x <= n; x++) { // 这里是记录tarjan缩点后(cnt个新牛牛)之间的出度 for(int y : e[x]) { if(scc[y] != scc[x]) dout[scc[x]]++; // scc记录的是原牛牛经过缩点后所在的新牛牛(联通块) } } int num = 0, t = 0; for(int i = 1; i <= cnt; i++) { if(dout[i] == 0) { num++; t = siz[i]; } } if(num > 1) t = 0; // 若是有多个出度为0的牛牛,则说明不存在明星牛牛,因为明星牛牛需要所有牛牛能访问到自己,存在2个以上出度为0肯定无法满足都能访问到自己 cout << t; return 0; }