Tarjan强联通分量学习

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;
 }
  • 18
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

幻听嵩的留香

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值