近期一直在刷这方面的题 由于无法学新知识 但又想写点什么 就水篇博文吧算法
发明者 Robert E.Tarjan 罗伯特·塔扬,美国计算机科学家spa
塔老爷子发明过不少算法,并且大可能是以他的名字命名的,因此 Tarjan算法 也分不少种code
这里主要讲 缩点,割点,割边 和 2-SATget
什么是强连通份量?it
强连通份量的定义是:极大的强连通子图。又叫 SCCclass
简单来讲,在一个有向图中,若全部点之间两两互相直接可达,则将这个图成为强连通份量计算机科学
求一个图中的强连通份量可使用 Tarjan,Kosaraju 或者 Garbow 算法变量
什么是双联通份量?原理
双联通分为 点双联通 与 边双连通 两种搜索
在一张连通的无向图中,对于两个点 \(u\) 和 \(v\),若是不管删去哪一条边都不能使它们不连通,咱们就说 \(u\) 和 \(v\) 边双连通
在一张连通的无向图中,对于两个点 \(u\) 和 \(v\) ,若是不管删去哪个除本身以外的点都不能使它们不连通,咱们就说 \(u\) 和 \(v\) 点双连通
这里有两个结论:
边双连通具备传递性,即若 \(x\),\(y\) 边双连通, \(y\),\(z\) 边双连通,则 \(x\),\(z\) 边双连通
点双连通不具备传递性
手玩几组样例便可证实,比较显然
缩点,简单说就是把一个图中全部的强连通份量缩成一个点,使之造成一个 DAG
缩完点后的图中每一个点会有一个新的编号,同处一个强连通份量中的点编号相同
想要完成这一操做,首先须要知道什么是 DFS序
一个结点 \(x\) 的 DFS序 是指深度优先搜索遍历时改结点被搜索的次序,简记为 \(dfn[x]\)
而后,再维护另外一个变量 \(low[x]\)
\(low[x]\) 表示如下节点的 DFS序 的最小值:以 \(x\) 为根的子树中的结点 和 从该子树经过一条不在搜索树上的边能到达的结点
根据 DFS 的遍历原理能够发现
一个结点的子树内结点的 DFS序 都大于该结点的 DFS序
从根开始的一条路径上的 DFS序 严格递增,low值 严格非降
知道了这些,再来看 Tarjan算法 求强连通份量的具体内容
咱们通常只对尚未肯定其 DFS序 的节点进行 Tarjan 操做,操做主要包括两个部分
以 DFS 的形式,处理出当前点 \(x\) 的 \(dfn[x]\) 和 \(low[x]\)
对当前点打一个标记表示已经遍历过,在以后的 DFS 中根据是否遍历过来进行不一样处理,具体方式以下:
设当前枚举点为 \(fr\),\(fr\) 连出去的点记为 \(to\)
这一部分代码实现以下:
low[fr]=dfn[fr]=++cnt;vis[fr]=1; for(int i=head[fr];i;i=e[i].nxt){ int to=e[i].to; if(!dfn[to]) tarjan(to),low[fr]=min(low[fr],low[to]); else if(vis[to]) low[fr]=min(low[fr],dfn[to]); }
对于一个连通份量图,咱们很容易想到,在该连通图中有且仅有一个 \(dfn[x]=low[x]\)
该结点必定是在深度遍历的过程当中,该连通份量中第一个被访问过的结点,由于它的 DFS序 和 low值 最小,不会被该连通份量中的其余结点所影响
咱们能够维护一个栈,存储全部枚举到的点
所以,在回溯的过程当中,断定 \(dfn[x]=low[x]\) 的条件是否成立,若是成立,则从栈中取出一个点,处理它所在的强连通份量的编号以及大小,也能够处理其余的一些操做,这样直到把全部点处理完为止
这一部分的代码实现以下:
zhan[++top]=u; if(dfn[u]==low[u]){ ++siz[++t]; int pre=zhan[top--]; vis[pre]=0;num[pre]=t; while(pre!=u){ ++siz[t];pre=zhan[top--]; vis[pre]=0;num[pre]=t; } }
至此,即可以处理出一个点所在的强连通份量,时间复杂度为 \(O(n+m)\)
能够处理 割点与桥 以及 双联通份量 相关的一些题
由于是无向图,必须加两条边,而加两条边后跑 Tarjan 会很麻烦
这里有另外一个处理方法:经过 异或 来一次选中两条边
咱们知道 \(0\oplus1=1\),\(1\oplus1=0\) ; \(2\oplus1=3\) , \(3\oplus1=2\) ; \(4\oplus1=3\) , \(3\oplus1=4\) ;
而建边的时候两条边的编号相差 \(1\),因此能够每次处理第 \(i\) 条边的时候处理第 \(i\oplus 1\) 条边,解决这个问题
而有向图和无向图 Tarjan 的写法也差很少,low值 的更新方式和缩点的编号等都相同,只有标记的地方不同
代码实现以下:
void tarjan(int u){ low[u]=dfn[u]=++cnt;zhan[++top]=u; for(int i=head[u];i;i=e[i].nxt){ if(!vis[i]){ vis[i]=vis[i^1]=1; int to=e[i].to; if(!dfn[to]) tarjan(to),low[u]=min(low[u],low[to]); else low[u]=min(low[u],dfn[to]); } } if(dfn[u]==low[u]){ ++siz[++t];int pre=zhan[top--];num[pre]=t; while(pre!=u){++siz[t];pre=zhan[top--];num[pre]=t;} } }
SAT 是适定性(Satisfiability)问题的简称。通常形式为 k-适定性问题,简称 k-SAT。而当 \(k>2\) 时该问题为 NP 彻底的。因此咱们只研究 \(k=2\) 的状况。 —— OI Wiki
我的感受,就是一个实际应用类的知识吧
就是指定 \(n\) 个集合,每一个集合包含两个元素,给出若干个限制条件,每一个条件规定不一样集合中的某两个元素不能同时出现,最后问在这些条件下可否选出 \(n\) 个不在同一集合中的元素
这个问题通常用 Tarjan算法 来求解,也可使用爆搜,能够参考OI Wiki上的说明,这里就只讲用 Tarjan 实现
但这种问题的实现主要不是难在 Tarjan 怎么写,而是难在图怎么建
咱们能够考虑怎么经过图来构造其中的关系
既然给出了条件 \(a\) 和 \(b\),必须只知足其中之一,那么存在两种状况,一是选择 \(a\) 与 \(\lnot b\),二是选择 \(b\) 与 \(\lnot a\)
那咱们就能够将 \(a\) 连向 \(\lnot b\),\(b\) 连向 \(\lnot a\),表示选了 \(a\) 必须选 \(\lnot b\),选了 \(b\) 必须选 \(\lnot a\)
举个例子,假设这里有两个集合 \(A=\{x_1,y_1\}\),\(B=\{x_2,y_2\}\),规定 \(x_1\) 与 \(y_2\) 不可同时出现,那咱们就建两条有向边 \((x_1,y_1)\),\((y_2,x_2)\),表示选了 \(x_1\) 必须选 \(y_1\),,选了 \(y_2\) 必须选 \(x_2\)
这样建完边以后只须要跑一边 Tarjan 缩点判断有无解,如有解就把几个不矛盾的强连通份量拼起来就行了
这里注意,由于跑 Tarjan 用了栈,根据拓扑序的定义和栈的原理,能够获得 跑出来的强连通份量编号是反拓扑序 这一结论
咱们就能够利用这一结论,在输出方案时倒序获得拓扑序,而后肯定变量取值便可
具体形如这样:
\\mem[i] 表示非 i for(int i=1;i<=n;i++)if(num[i]==num[mem[i]]){printf("无解");return 0;}\\若两条件必须同时选,则不成立 for(int i=1;i<=n*2;i++)if(num[i]<num[mem[i]]) printf("%d\n",i);return 0;\\输出其中一种选择方案
时间复杂度为 \(O(n+m)\)
什么是割点?
若是在一个无向图中,删去某个点可使这个图的极大连通份量数增长,那么这个点被称为这个图的割点,也叫割顶。
求割点比较暴力的作法是,对于每一个点尝试删除而后判断图的连通性,不过显然复杂度极高
考虑用 Tarjan 作
同缩点同样,用 Tarjan 求割点也须要处理出点的 DFS序 和 low值,而且是用 DFS 的形式处理
每次枚举一个点,判断这个点是否为割点的依据是:
1.若是它有至少一个儿子的 low值 大于它自己的 DFS序,那么它就是割点
1.若是它自己被搜到且有很多于两个儿子,那么它就是割点
对于第一个依据的说明是:若一个儿子的 low值 大于它自己的 DFS序,说明删去它以后它的这个儿子没法回到祖先点,那么它确定是割点
对于第二个依据的说明是:若它的儿子小于两个,那么删去他不会形成任何影响,因此它不会是割点
更新 low值 的方式与缩点类似,可是约束条件不一样,放伪代码感性理解一下:
若是 v 是 u 的儿子 low[u] = min(low[u], low[v]); 不然 low[u] = min(low[u], num[v]);
其实割点 Tarjan 的所有代码实现有不少别的细节,原理很简单,代码实现以下:
void tarjan(int u,int fa){ vis[u]=1;int chi=0;//统计孩子数量 dfn[u]=low[u]=++cnt; for(int i=head[u];i;i=e[i].nxt){ int to=e[i].to; if(!vis[to]){ chi++;tarjan(to,u); low[u]=min(low[to],low[u]); if(fa!=u&&low[to]>=dfn[u]&&!flag[u]){//第一个依据 flag[u]=1; res++;\\割点数量 } } else if(to!=fa) low[u]=min(low[u],dfn[to]); } if(fa==u&&chi>=2&&!flag[u]){//第二个依据 flag[u]=1; res++; } }
可是这样跑 Tarjan 针对的不是没有肯定 DFS序 的点,而是没有访问过的点,而且每次初始父亲都是本身
也就是这样:
for(int i=1;i<=n;i++) if(!vis[i]) cnt=0,tarjan(i,i);
这样跑一边Tarjan后,带有 \(flag\) 标记的点就是割点
按割点的理解方式,割边应该是删去后能使无向图极大连通份量数量增长的边
没错,就是这样
割边,也叫桥。严谨来讲,假设有连通图 \(G=\{V,E\}\),\(e\) 是其中一条边(即 \(e\in E\)),若是 \(G-e\) 是不连通的,则边 \(e\) 是图 \(G\) 的一条割边(桥)。
原理和割点差很少,实现也差很少,只要改一处:\(low[v]>dfn[u]\) 就能够了,并且不须要考虑根节点的问题。
与判断割点的第一条依据相似,当一条边 \((u,v)\) 的 \(low[v]>dfn[u]\) 时,删去这条边,\(v\) 就没法回到祖先节点,所以知足此条件的边就是图的割边
代码实现以下:
void tarjan(int u,int fat){ fa[u]=fat; low[u]=dfn[u]=++cnt; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(!dfn[v]){ tarjan(v,u); low[u]=min(low[u],low[v]); if(low[v]>dfn[u]){vis[v]=true;++bri;}\\bri 是割边的数量 } else if(dfn[v]<dfn[u]&&v!=fat) low[u]=min(low[u],dfn[v]); } }
其中,当 \(vis[x]=1\) 时,\((fa[x],x)\) 是一条割边
「一本通 3.6 例 1」分离的路径
「一本通 3.6 例 2」矿场搭建
[APIO2009]抢掠计划
[USACO5.3]校园网Network of Schools
[ZJOI2007]最大半连通子图
[POI2001]和平委员会
写了半上午加半下午,好累qaq
不过有一说一,我是真不理解有人只放个题目连接而后放个代码是怎么想的,连思路都不提,是给别人写 std 仍是只为了告诉别人本身作对了
比我还水