强连通分支及其应用(2-SAT)总结

从寒假一开始,到如今也学习了两个多星期的图论中dfs的相关算法,也作了一些题目。在这里先把强连通分支及其应用作一个第一阶段总结,巩固一下也便于开始下一步学习。在这里我也会列出我总结的一套模版。算法

首先咱们要明确下面的这些算法都是针对有向图而言的,先笼统的说一下强连通分支是什么?其实就是有向图中的一部分,在这部分里任意两个节点都相互可达。虽然表述可能不规范,可是应该比较形象吧。数组

1、强连通分支(scc)学习

接下来咱们先来学习一下如何在一张给定的图中求出强连通分支,咱们须要介绍两个算法:spa

(1)Kosaraju算法code

这个算法的思想很简单,也比较好写。前提是咱们已经熟练掌握了dfs的写法及思想。下面是算法流程:blog

  • 首先咱们对原图先进性一边dfs获得原图中各结点的拓扑序把他存在一个数组里。
  • 在有了拓扑序后,咱们再对原图反向后的图按照逆拓扑序进行dfs每次dfs就获得一个强连通分支。

整个算法就描述完了看起来很简单吧,接下来咱们说一下具体到程序中咱们该如何实现。ci

  • 准备 有上面两步咱们看到不只须要原图咱们还须要原图中全部边都反向的图,因此咱们在处理输入时必须同时获得Map,rMap。
  • 初始化 很简单,vis数组置零(dfs标记用),vs数组清空(记录拓扑序用)。
  • 开始第一遍dfs 没遍历一个节点在回溯时将其加入拓扑序数组。
  • 开始第二遍dfs 记得以前清空vis数组。而且此次咱们要在参数中加入一项就是f标记dfs到的结点是属于那个强连通分支的(记录在sccn数组中)。

下面是个人模版:it

 

 1 /****************************************
 2      强连通分支 kosaraju算法        
 3      By 小豪                
 4 *****************************************/
 5 const int LEN = 10000+10;
 6 vector<int> Map[LEN], rMap[LEN], vs;
 7 int vis[LEN], sccn[LEN], n, m;
 8  
 9 void dfs(int v)
10 {
11     vis[v] = 1;
12     for(int i=0; i<Map[v].size(); i++)
13         if(!vis[Map[v][i]])dfs(Map[v][i]);
14     vs.PB(v);
15 }
16  
17 void rdfs(int v, int k)
18 {
19     vis[v] = 1;
20     sccn[v] = k;
21     for(int i=0; i<rMap[v].size(); i++)
22         if(!vis[rMap[v][i]])rdfs(rMap[v][i], k);
23 }
24  
25 int scc()
26 {
27     memset(vis, 0, sizeof vis);
28     vs.clear();
29     for(int i=1; i<=n; i++) if(!vis[i])dfs(i);
30     memset(vis, 0, sizeof vis);
31     int k = 0;
32     for(int i=vs.size()-1; i>=0; i--) if(!vis[vs[i]]) rdfs(vs[i], k++);
33     return k;
34 }

 

算法复杂度是线性的, 返回的k记录了有几个强连通分支。sccn记录了强连通分支的拓扑序(这颇有用)。io

(2)Tarjan算法class

tarjan是一个神奇的人,他提出了许多算法,而这个只是其中的一种。tarjan的复杂度也是线性的,并且比上一种更快由于他只要dfs一遍。tarjan是经过在搜索树中发现第一个属于这个强连通分支的结点,而后因为只要和这个结点在一个强连通分支内的必定是他的后代,那么如何判断一点是否是强连通分支内第一个被发现的点呢?

这个问题咱们又要用到了low值与反向边(和割点割边的求法都很是相似)咱们在整个搜索过程当中设置一个dclock(称为时间戳)每搜过一个点就加一,这样咱们就能在确认搜索树中祖先与孩子的关系。比较明显最早搜到的必定是根节点dfn(搜索标号)值为1,稍微想一下就可发现dfn越小的节点越靠近根。

那么low值又是什么?简单来讲就是这个点经过反向边能连到dfn最小的点(换句话说就是最靠近根节点的点)这样如果一个点u经过他的孩子节点v能连会(注意这里的连回指的是只经过本身强连通份量内的点)u的祖先x,那么咱们就能够确认u,v,x在一个强连通分支内。如果u经过v最多只能连回本身那么咱们就能知道u是咱们第一个找到的点,在回溯是咱们就要把栈中结点取出(并非全取出具体参考代码)记录为同一个强连通分支。

接线来问题又来了,咱们如何获得一个节点dfn和low的值。先说dfn吧,很简单只要在dfs开始让他等于时间戳就ok。而后是low首先咱们在dfs重要判断接下来要走的即是属于树边仍是反向边,如果树边则说明是孩子节点,那必须用孩子节点的low值来更新当前节点的low值。如果反向边,那说明是祖先,那只要用祖先节点的dfn值来更新当前节点的low值。这样low值就搞定的。

说了那么多下面看代码:

 1 /****************************************
 2      强连通分支 tarjan算法        
 3      By 小豪                
 4 *****************************************/
 5 const int LEN = 100000 + 10;
 6 vector<int> Map[LEN];
 7 int dfn[LEN], low[LEN], dclock, scc_cnt, sccn[LEN], n, m;
 8 stack<int> s;
 9 
10 void sccinit(){
11     for(int i=0; i<LEN; i++) Map[i].clear();
12     while(!s.empty())s.pop();
13     dclock = scc_cnt = 0;
14     memset(sccn, 0, sizeof sccn);
15     memset(dfn, 0, sizeof dfn);
16 }
17 
18 void dfs(int u){
19     dfn[u] = low[u] = ++dclock;
20     s.push(u);
21     for(int i=0; i<Map[u].size(); i++){
22         int v = Map[u][i];
23         if(!dfn[v]){
24             dfs(v);
25             low[u] = min(low[u], low[v]);
26         }else if(!sccn[v]) low[u] = min(low[u], dfn[v]);
27     }
28     if(low[u] == dfn[u]){
29         scc_cnt++;
30         while(1){
31             int x = s.top();s.pop();
32             sccn[x] = scc_cnt;
33             if(x == u) break;
34         }
35     }
36 }

两端代码长度是不相上下的都不长,在tarjan中咱们新增了scc_cnt来做为全局变量记录强连通分支的个数。

2、2-SAT

强联通份量一个很重要的用途就是解布尔方程可知足性问题(SAT)。须要学习这一部分知识咱们须要一点布尔代数的知识。

下文中咱们约定(^表示交v表示并)

例如:(a v b v …)^(c v d v …)^…

这样的咱们叫作合取范式。其中(a v b v …)这样的叫作子句。相似a,b...叫作文字。

咱们把合取范式中一个子句中包含文字不超过两个的问题成为2-SAT问题。在SAT问题中只有这一类咱们能够用线性时间内得出答案。

最常规的2-SAT题目分为大体两种,一种是让你判断有没有解,另外一种是让你输出一组解。针对这两种给出模版。

在这以前先来介绍一下2-SAT题目的大体解题步骤:

对于2-SAT问题咱们须要构建一张有向图每一个文字拆为两个节点 例如 a 变为 a, !a

首先咱们从题目中总结出来的都是一些比较杂乱的逻辑表达式,不过通常都是两两之间的关系,咱们须要作的第一步是化简成用^链接。而后对于每一个子句建边。

建边的规则是这样的 a -> b那么在有向图中建一条a到b的边

咱们可能获得的子句有:

a v b 咱们能够化简 !a->b ^ !b->a

a -> b 直接连边

a  转化为!a -> a

其中每一个文字及其的非对应相应的结点,如果出如今文字前有非的关系例如 !a v b 那么变通一下 就化成 a -> b ^ !b -> !a就能够了。

到这里咱们要作的事(建图)就完成了,接下来交给模版,咱们来看一下模版作了什么:

首先咱们对建完的有向图求强连通分支,如果出现有一个逻辑变量和他的反在同一个联通分以内就无解,不然有解。

若a所在的强连通分支的拓扑序在!a以后a为真,不然为反。怎么样很简单吧。

下面贴出代码:

 

 1 /****************************************
 2      2-SAT kosaraju算法        
 3      By 小豪                
 4 *****************************************/
 5 const int LEN = 200000+10;
 6 vector<int> Map[LEN], rMap[LEN], vs;
 7 int n, m, vis[LEN], sccn[LEN];
 8 
 9 void dfs(int v){
10     vis[v] = 1;
11     for(int i=0; i<Map[v].size(); i++)
12         if(!vis[Map[v][i]]) dfs(Map[v][i]);
13     vs.PB(v);
14 }
15 
16 void rdfs(int v, int f){
17     vis[v] = 1;
18     sccn[v] = f;
19     for(int i=0; i<rMap[v].size(); i++)
20         if(!vis[rMap[v][i]]) rdfs(rMap[v][i], f);
21 }
22 
23 int scc(){
24      memset(vis, 0, sizeof vis);
25      vs.clear();
26      for(int i=0; i<2*n; i++) if(!vis[i]) dfs(i);
27      memset(vis, 0, sizeof vis);
28      int k = 0;
29      for(int i = vs.size()-1; i>=0; i--) if(!vis[vs[i]]) rdfs(vs[i], k++);
30      return k;
31 }
32 
33 void addedge(int a, int b){
34     Map[a].PB(b);
35     rMap[b].PB(a);
36 }
37 
38 void solve()
39 {
40     scc();
41     for(int i=0; i<2*n; i+=2)
42         if(sccn[i] == sccn[i+1]){
43                 //printf("No solution.\n");
44              //无解
45             return ;
46         }
47     for(int i=0; i<n; i++){
48         if(sccn[i*2] > sccn[i*2+1]) printf("Yes\n");
49         else printf("No\n");
50     }
51 }

 

好了主体部分讲完了,接下来在讲一下再强连通的题目中,咱们每每会用到缩点(就是把同一个强连通分支内的点缩成一个),其实缩点并非都要把几个点缩成一个,须要根据题目的须要,有时候只需判断一下就能够了。在缩点后强连通分支内的点每每具备相同的特性,就赋予这个点一个新的意义。并且原图也变成了DAG,就能够dp等等。

 

水平有限,只但愿把本身所知道的和你们分享一下。若大神发现有什么错误,欢迎留言指正,定当感激涕零! By 张小豪

相关文章
相关标签/搜索