From 848904bfb92a0cd48f5c138a20fd16035210a259 Mon Sep 17 00:00:00 2001 From: Neurotical <1602175681@qq.com> Date: Thu, 23 May 2024 19:11:05 +0800 Subject: [PATCH] 2024/5/23 --- "5-\345\233\276\350\256\272.md" | 413 ++++++++++++++++++++++++++++++++ 1 file changed, 413 insertions(+) diff --git "a/5-\345\233\276\350\256\272.md" "b/5-\345\233\276\350\256\272.md" index 8d12998..d06be41 100644 --- "a/5-\345\233\276\350\256\272.md" +++ "b/5-\345\233\276\350\256\272.md" @@ -259,3 +259,416 @@ int main(){ return 0; } ``` + +## 有向图强联通分量 + +有向非强连通图中的**极大**强连通子图我们称为**强连通分量** + +如果一个有向图不是强连通图但是将**所有有向边换成无向边**变成了强连通图,那么该图就是**弱连通图**。 + +Kosaraju算法 + +首先对原图 𝐺 进行遍历,记录节点访问**完**的顺序 𝑑𝑖 , 𝑑𝑖 表示第 𝑖 个访问完的节点编号。 + +我们选择最晚**访问完**的节点,对 𝐺 的反向图进行遍历,它能够遍历到的顶点和它组成了一个 SCC,把该过程所遍历到的节点打标记,接下来继续找最晚**访问完**且未被打上标记的节点进行遍历操作。 + +``` +#include +#define ll long long +using namespace std; +const int Maxn=1e5+7; +int n,m; +vectore[Maxn],e1[Maxn]; +// e 存正向边,e1 存反向边 +bool vis[Maxn]; +int d[Maxn],cnt,col[Maxn],cnt1; +void dfs1(int u){ + vis[u]=1; + for(auto v:e[u]) if(!vis[v]) dfs1(v); + d[++cnt]=u; +} +vectorans[Maxn]; +void dfs2(int u){ + col[u]=cnt1; + ans[cnt1].push_back(u); + for(auto v:e1[u]) if(!col[v]) dfs2(v); +} +int main(){ + scanf("%d%d",&n,&m); + for(int i=1,u,v;i<=m;i++){ + scanf("%d%d",&u,&v); + e[u].push_back(v); + e1[v].push_back(u); + } + for(int i=1;i<=n;i++) if(!vis[i]) dfs1(i); + for(int i=n;i;i--) if(!col[d[i]]) ++cnt1,dfs2(d[i]); + for(int i=1;i<=cnt1;i++) sort(ans[i].begin(),ans[i].end()); + printf("%d\n",cnt1); + for(int i=1;i<=n;i++){ + if(ans[col[i]].size()){ + for(auto j:ans[col[i]]) printf("%d ",j); + puts(""); + ans[col[i]].resize(0); + } + } + return 0; +} +``` + +Tarjan算法 + +在 DFS 过程中,我们会遇到如下 4 种边: + +- 树枝边:DFS 过程中经过的边,即 DFS 搜索树上的边。 + +- 前向边:从祖先节点指向后代节点的非树枝边,我们称为前向边。 + +- 返祖边(后向边):从后代节点指向祖先节点的非树枝边,我们称为返祖边(后向边)。 + +- 横叉边:两端无祖先关系的非树枝边,我们称为横叉边。 + + 每个强连通分量都是 DFS 树的一颗子树,搜索时,把当前 DFS 树种未处理的节点加入一个栈,回溯时可以判断栈顶到栈中的节点是否构成一个强连通分量。 + +我们不妨定义 𝑑𝑓𝑛(𝑢) 表示节点 𝑢 在 DFS 中的遍历编号(**时间戳**), 𝑙𝑜𝑤(𝑢) 表示 𝑢 或 𝑢 的子树能够最多只通过**一条非树枝边(不包含树边)**回溯的最早的 𝑑𝑓𝑛 值,用一个栈记录经过的节点,那么我们可以得出: + +- 初始情况有 𝑙𝑜𝑤(𝑢)=𝑑𝑓𝑛(𝑢) 。 +- 对于边 (𝑢,𝑣) ,如果 𝑢 为 𝑣 的父亲节点,则有 𝑙𝑜𝑤(𝑢)=min{𝑙𝑜𝑤(𝑢),𝑙𝑜𝑤(𝑣)} 。 +- 对于边 (𝑢,𝑣) 为返祖边或者指向非其他强连通的横叉边,则有 𝑙𝑜𝑤(𝑢)=min{𝑙𝑜𝑤(𝑢),𝑑𝑓𝑛(𝑣)} 。 + +在节点 𝑢 搜索完毕之后,如果 𝑙𝑜𝑤(𝑢)=𝑑𝑓𝑛(𝑢) ,那么说明以 𝑢 为**根节点**的搜索子树上及栈中在 𝑢 内的元素组成了一个强连通分量,然后删除栈内的这些元素,不断重复该操作直到找到所有的强连通分量。 + +例 [P3387 【模板】缩点 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)](https://www.luogu.com.cn/problem/P3387) + +``` +#include +#define ll long long +using namespace std; +const int Maxn=1e5+7; +int n,m; +vectore[Maxn]; +int col[Maxn],dfn[Maxn],low[Maxn],cnt; +int stk[Maxn],top,_cnt; +vectorans[Maxn]; +void Tarjan(int u){ + dfn[u]=low[u]=++cnt; + stk[++top]=u; + for(auto v:e[u]){ + if(!dfn[v]) Tarjan(v),low[u]=min(low[u],low[v]); + else if(!col[v]) low[u]=min(low[u],dfn[v]); + } + if(low[u]==dfn[u]){ + col[u]=++_cnt; + ans[_cnt].push_back(u); + while(stk[top]!=u) + ans[_cnt].push_back(stk[top]),col[stk[top--]]=_cnt; + --top; + } +} +int main(){ + scanf("%d%d",&n,&m); + for(int i=1,u,v;i<=m;i++){ + scanf("%d%d",&u,&v); + e[u].push_back(v); + } + for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i); + for(int i=1;i<=_cnt;i++) sort(ans[i].begin(),ans[i].end()); + printf("%d\n",_cnt); + for(int i=1;i<=n;i++){ + if(ans[col[i]].size()){ + for(auto j:ans[col[i]]) printf("%d ",j); + puts(""); + ans[col[i]].resize(0); + } + } + return 0; +} +``` + +## 割点割边 + +- 割点:在**无向图**中,删去该点后使得连通块数增加的结点称为 **割点**。 +- 割边(桥):在**无向图**中,删去该边后使得连通块数增加的边称为 **割边(桥)**。 + +一个图可能会有多个割点或者割边,但是有割点的图不一定存在割边,有割边的图不一定存在割点。 + + + +#### 割点 + +和有向图求 SCC 一样,我们会在搜索过程中遇到两种搜索树上的边: + +- 树枝边:DFS 过程中经过的边,即 DFS 搜索树上的边。 +- 返祖边(后向边):从后代节点指向祖先节点的非树枝边,我们称为返祖边(后向边)。 + +不包含横叉边和前向边,因为这是**无向图**。 + +我们不妨定义 𝑑𝑓𝑛(𝑢) 表示节点 𝑢 在 DFS 中的遍历编号(**时间戳**), 𝑙𝑜𝑤(𝑢) 表示 𝑢 或 𝑢 的子树能够最多只通过**一条非树枝边(不包含树边)**回溯的最早的 𝑑𝑓𝑛 值,那么我们可以得出: + +- 初始情况有 𝑙𝑜𝑤(𝑢)=𝑑𝑓𝑛(𝑢) 。 +- 对于边 (𝑢,𝑣) ,如果 𝑣 没有被搜索到,那么这条边就是树枝边,则有 𝑙𝑜𝑤(𝑢)=min{𝑙𝑜𝑤(𝑢),𝑙𝑜𝑤(𝑣)} 。 +- 对于边 (𝑢,𝑣) ,如果 𝑣 被搜索到了,那么这条边就是返祖边,则有 𝑙𝑜𝑤(𝑢)=min{𝑙𝑜𝑤(𝑢),𝑑𝑓𝑛(𝑣)} 。 + +对于一个节点 𝑢 ,它的子节点 𝑣 ,若 𝑙𝑜𝑤(𝑣)≥𝑑𝑓𝑛(𝑢) ,说明 𝑣 无法通过它子树的节点到达 𝑑𝑓𝑛 更小的节点,也就是 𝑣 无法不经过点 𝑢 到达比 𝑢 的 𝑑𝑓𝑛 值更小的节点,显然 𝑢 是一个割点,反之 𝑢 就不是一个割点。 + +但是对于根节点 𝑢 ,它的 𝑑𝑓𝑛 值一定是整个序列的最小值,因此上述方法不管用,如果它存在两个及以上的子节点,那么把 𝑢 删除后绝对会把 𝑢 子节点的子树分割开来,此时 𝑢 是割点。 + +```cpp +#include +#define ll long long +using namespace std; +const ll Maxn=1e5+7; +struct edge1{ + ll v,Next; +}Edge[Maxn<<1]; +ll n,m,tot,low[Maxn],dfn[Maxn],cnt,root,ans,head[Maxn]; +bool flg[Maxn]; +inline void add(ll u,ll v){ + Edge[++tot]=(edge1){v,head[u]},head[u]=tot; +} +void tarjan(ll u){ + dfn[u]=low[u]=++cnt; + ll ch=0; + for(ll i=head[u];i;i=Edge[i].Next){ + ll v=Edge[i].v; + if(!dfn[v]){ + ch++; + tarjan(v); + low[u]=min(low[u],low[v]); + if(low[v]>=dfn[u]&&u!=root) flg[u]=1; + + } + else low[u]=min(low[u],dfn[v]); + } + if(ch>=2&&root==u) flg[u]=1; +} +int main(){ + scanf("%lld%lld",&n,&m); + for(ll i=1,u,v;i<=m;i++) + scanf("%lld%lld",&u,&v),add(u,v),add(v,u); + for(ll i=1;i<=n;i++) + if(!dfn[i]) + root=i,tarjan(i); + for(ll i=1;i<=n;i++) + if(flg[i]) + ++ans; + printf("%lld\n",ans); + for(ll i=1;i<=n;i++) + if(flg[i]) + printf("%lld ",i); + return 0; +} +``` + +#### 割边 + +割边的求法和割点的求法类似,我们继续使用相同定义的 𝑙𝑜𝑤 和 𝑑𝑓𝑛 。 + +很显然,树枝边使得整个图连通,而非树边删除后并不影响图的连通性,因此**割边一定是树枝边**。 + +假设当前节点为 𝑢 ,它有子节点 𝑣 ,那么边 (𝑢,𝑣) 为割边时当且仅当 𝑙𝑜𝑤(𝑣)>𝑑𝑓𝑛(𝑢) ,只要 𝑣 的节点能通过非树边来到比 𝑢 的 𝑑𝑓𝑛 更小的节点,那么 (𝑢,𝑣) 就不是割边,不取等号的原因是它是一条边而非一个节点。 + +现在唯一的问题就是判断一条边是否为非树边,我们不能直接将 𝑣→𝑢 ( 𝑢 是 𝑣 的父亲)识别成非树边,这样可能将树边也识别成非树边。 + +tarjan + +```cpp +#include +#define int long long +const int N=1e6+1; +using namespace std; +struct fy +{ + int v,next; +}edge[N]; +struct fy_ +{ + int from,to; +}E[N]; +int dfn[N],low[N],n,m,x,y,idx,head[N],res,cnt,IDX; +bool g[N]; +inline void add(int x,int y) +{ + edge[++cnt].v=y,edge[cnt].next=head[x],head[x]=cnt; +} +inline void dfs(int u,int fa) +{ + dfn[u]=low[u]=++idx; + for(int i=head[u];i;i=edge[i].next) + { + int v=edge[i].v; + if(!dfn[v]) + { + dfs(v,u); + low[u]=min(low[u],low[v]); + if(low[v]>dfn[u]) + E[++IDX].from=min(u,v),E[IDX].to=max(u,v); + } + else if(v!=fa&&dfn[v] +#define ll long long +using namespace std; +const ll Maxn=4e6+7; +struct edge1{ + ll u,v,Next; +}Edge[Maxn<<1]; +ll n,m,tot=1,f[Maxn]; +ll low[Maxn],dfn[Maxn],cnt,head[Maxn]; +ll cl; +vectorans[Maxn]; +bool flg[Maxn],vis[Maxn]; +inline void add(ll u,ll v){ + Edge[++tot]=(edge1){u,v,head[u]},head[u]=tot; +} +void tarjan(ll u){ + dfn[u]=low[u]=++cnt; + for(ll i=head[u];i;i=Edge[i].Next){ + if(i==(f[u]^1)) continue; + ll v=Edge[i].v; + if(!dfn[v]){ + f[v]=i; + tarjan(v); + low[u]=min(low[u],low[v]); + if(low[v]>dfn[u]) flg[i]=flg[i^1]=1; + } + else low[u]=min(low[u],dfn[v]); + } +} +void DFS(ll u){ + vis[u]=1; + ans[cl].push_back(u); + for(ll i=head[u];i;i=Edge[i].Next){ + if(!flg[i]&&!vis[Edge[i].v]){ + DFS(Edge[i].v); + } + } +} +int main(){ + scanf("%lld%lld",&n,&m); + for(ll i=1,u,v;i<=m;i++) + scanf("%lld%lld",&u,&v),add(u,v),add(v,u); + for(ll i=1;i<=n;i++) if(!dfn[i]) tarjan(i); + for(ll i=1;i<=n;i++) if(!vis[i]) ++cl,DFS(i); + printf("%lld\n",cl); + for(ll i=1;i<=cl;i++,puts("")){ + printf("%lld ",ans[i].size()); + for(auto j:ans[i]) printf("%lld ",j); + } + return 0; +} +``` + +### 点双连通分量 + +对于一个点双,它在 DFS 搜索树中 𝑑𝑓𝑛*d**f**n* 值最小的点一定是割点或者树根。 + +当这个点是割点时,它所属的点双必定不可以向它的父亲方向包括更多点,因为一旦回溯,它就成为了新的子图的一个割点,不是点双。所以它应该归到其中一个或多个子树里的点双中。 + +当这个点是树根时,它的 𝑑𝑓𝑛*d**f**n* 值是整棵树里最小的。它若有两个以上子树,那么它是一个割点;它若只有一个子树,它一定属于它的直系儿子的点双,因为包括它;它若是一个独立点,视作一个单独的点双。 + +换句话说,一个点双一定在这两类点的子树中。 + +我们用栈维护点,当遇到这两类点时,将子树内目前不属于其它点双的非割点或在子树中的割点归到一个新的点双。注意这个点可能还是与其它点双的公共点,所以不能将其出栈。 + +```cpp +#include +using namespace std; +const int N = 5e5 + 5, M = 4e6 + 5; +int cnt = 1, fir[N], nxt[M], to[M]; +int s[M], top, bcc, low[N], dfn[N], idx, n, m; +vector ans[N]; +inline void tarjan(int u, int fa) { + int son = 0; + low[u] = dfn[u] = ++idx; + s[++top] = u; + for(int i = fir[u]; i; i = nxt[i]) { + int v = to[i]; + if(!dfn[v]) { + son++; + tarjan(v, u); + low[u] = min(low[u], low[v]); + if(low[v] >= dfn[u]) { + bcc++; + while(s[top + 1] != v) ans[bcc].push_back(s[top--]);//将子树出栈 + ans[bcc].push_back(u);//把割点/树根也丢到点双里 + } + } else if(v != fa) low[u] = min(low[u], dfn[v]); + } + if(fa == 0 && son == 0) ans[++bcc].push_back(u);//特判独立点 +} +inline void add(int u, int v) { + to[++cnt] = v; + nxt[cnt] = fir[u]; + fir[u] = cnt; +} +int main() { + scanf("%d%d", &n, &m); + for(int i = 1; i <= m; i++) { + int u, v; + scanf("%d%d", &u, &v); + add(u, v), add(v, u); + } + for(int i = 1; i <= n; i++) { + if(dfn[i]) continue; + top = 0; + tarjan(i, 0); + } + printf("%d\n", bcc); + for(int i = 1; i <= bcc; i++) { + printf("%d ", ans[i].size()); + for(int j : ans[i]) printf("%d ", j); + printf("\n"); + } + return 0; +} +``` +