一.强联通分量及缩点
算法学习
1.定义
- 强连通分量: 给定一张有向图,若存在一个点集,该点集内的点可通过路径任意到达其他所有点,则称该点集为一个强联通分量。(一个点也可为一个强联通分量)
2.算法tarjan
- 结果: 将强联通分量缩成一个点,让一个有环图变成一张无环图
- 实现方式:记 l o w [ u ] low[u] low[u] 为 u u u 点所能到达的最小次序点,记 d f n [ u ] dfn[u] dfn[u] 为 u u u 点的次序, s t k stk stk 存储经过的点,在 d f s dfs dfs 的过程中不断的维护 l o w low low。回溯时,若点 u u u 的 d f n = = l o w dfn==low dfn==low,即点 u u u 的子结点不会到达 u u u 上层的点了,则从 u u u 开始跑经过的点均在 u u u 所在的强联通分量里,就将其染色
#include<bits/stdc++.h>
using namespace std;
const int N=20005;
const int M=1e5+5;
struct ppp {
int u,v,next;
} e[2*M];
int vex[N],k;
int dfn[N],low[N],cnt;
int stk[N],top,color[N],co,ans[N],instk[N];
int n,m;
//dfn为次序,low为该点所能遍历的次序最早的点
void add(int u,int v) {
k++;
e[k].u=u;
e[k].v=v;
e[k].next=vex[u];
vex[u]=k;
return;
}
void tarjan(int u) {
dfn[u]=low[u]=++cnt;
instk[u]=1;
stk[++top]=u;//用栈储存强连通,u点入栈
for(int i=vex[u]; i; i=e[i].next) {
int v=e[i].v;
if(!dfn[v]) {
tarjan(v,root);
low[u]=min(low[v],low[u]);
//如果v能遍历到的点比u能遍历到的最小点小,就改值
}
else if(instk[v])low[u]=min(low[u],dfn[v]);
//如果v的次序比u的遍历到的最小点小,就改值
}
if(dfn[u]==low[u]){//则该点为强连通的最早点
co++;
while(1){
int t=stk[top--];//从栈顶到这个点的所有点,都是该强联通分量的点集
color[t]=co;//属于一个强联通分量的点颜色相同
instk[t]=0;//出栈
if(t==u)break;
}
}
}
void solve(){
co=n; //如果需要对缩完后的点建图,则co要从n+1开始
for(int i=1; i<=n; i++) {
if(!dfn[i])tarjan(i);
}
for(int u=1;u<=n;u++){
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(color[u]!=color[v])add(color[u],color[v]);
//后来是以缩点后的强联通分量颜色来建无向图
}
}
}
int main() {
int u,v;
cin>>n>>m;
for(int i=1; i<=m; i++) {
cin>>u>>v;
add(u,v);
add(v,u);
}
solve();
}
例题练习
例题1:
只需要在tarjan完之后,求入度为0的点的数量即可
void xxks(){
int ans=0;
for(int u=1;u<=n;u++){
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(color[u]!=color[v])in[color[v]]++;
//求入度为0的点为扩散源
}
}
for(int i=1;i<=co;i++){
if(in[i]==0)ans++;
}
cout<<ans;
}
例题2:缩点模板
- 题目描述: 给定一个有向图,有带点权 a [ ] a[] a[]。让你从中选择若干个回路。使得这些回路经过的点的并集是 V 。若选择的一个回路的起点为 u ,则该条回路的花费为 a [ u ] a[u] a[u] 。求最小花费及最小花费点方案数。 n , m < = 2 e 5 n,m<=2e5 n,m<=2e5
- 问题分析: 选择每个强连通分量中花费最小的点作为回路的起点即可。而方案数就是所有强连通分量中花费最小的点数量的乘积。
例题3:缩点模板
- 题目描述: 给定一个有向图,有带边权,表示花费。若两个点 ( u , v ) (u,v) (u,v) 在相互可达,则 u u u 到 v v v 和 v v v 到 u u u 之间可以无花费的跳。否则,只能一步一步沿着边走。求 1 1 1 到 n n n 的最小花费为多少。 n , m < = 2 e 5 n,m<=2e5 n,m<=2e5
- 问题分析: 缩完点之后,就是一颗有向无环图。从起点 d f s dfs dfs 一遍即可求得最短路径长度(注意,缩完点的图是有重边的)。
例题4:缩点带技巧
- 题目描述: 给定一个消息扩散图(有向图),1:求至少需要多少个消息源才能扩散整个图,2:至少需要添加多少条边,才能使整个图变成一个强连通分量。
- 问题分析: 先缩点,1:直接求入度为 0 的点的数量,2:将每个出度为 0 的点连向一个入度为 0 的点,因此答案为入度为 0 的点与出度为 0 的点的最大值。
例题5:缩点+dp
- 题目描述: 给定一个有向图,带点权。若经过一个点 u u u ,则可以获取其值 a [ u ] a[u] a[u] 。起点为 s s s ,终点为 p p p 个点中的任意一个。求最多可以获取到多少值。
- 问题分析: 先缩点。点权为强连通分量中所有点的点权和,若该强连通分量中存在终点,则该点可作为终点。之后在 D A G DAG DAG 图上拓扑序 d p dp dp 求最长路即可。
例题6:缩点求连通数
- 题目描述: 给定一个图,若 u u u 和 v v v 可达,则连通数加1。求一共有多少个连通数。 n < = 2000 , m < = n 2 n<=2000,m<=n^2 n<=2000,m<=n2。保证无重边
- 问题分析:
- 方法1强连通分量: 先缩点。那么 a n s = ∑ i = 1 c o ∑ j = 1 c o s u m i × s u m j × h [ i ] [ j ] ans=\sum_{i=1}^{co}\sum_{j=1}^{co} sum_i\times sum_j\times h[i][j] ans=∑i=1co∑j=1cosumi×sumj×h[i][j],其中 h [ i ] [ j ] = 1 h[i][j]=1 h[i][j]=1 表示 i i i 和 j j j 可达。以每个点作为起点,跑一次 d f s dfs dfs 即可。时间复杂度 O ( n 2 ) O(n^2) O(n2)
- 方法2bitset:
bitset<N> f[N];
int ans,n;
char s[N];
int main() {
scanf("%d",&n);
for(int i=1; i<=n; ++i) {
scanf("%s",s);
for(int j=1; j<=n; ++j)
if(s[j-1]=='1'||i==j)f[i][j]=1;
}
for(int i=1; i<=n; ++i)
for(int j=1; j<=n; ++j)
if(f[j].test(i))f[j]|=f[i];
//如果j可以到达i,那么点i能到的点肯定能被点j所达到
for(int i=1; i<=n; ++i)ans+=f[i].count();
printf("%d\n",ans);
}
例题7:缩点+spfa
- 题目描述: 给定一个图,边有边权 w w w 和递减系数 r r r ,第一次经过可以获得 w w w 个蘑菇,第二次经过可以获得 w × r w\times r w×r 个蘑菇,第 t t t 次经过可以获得 w × r t − 1 w\times r^{t-1} w×rt−1 个蘑菇(向下取整)。给定起点 s s s ,求最多可以获得多少个蘑菇。 n < = 8 e 4 , m < = 2 e 5 n<=8e4,m<=2e5 n<=8e4,m<=2e5
- 问题分析: 先缩点。同一个强连通分量里的所有蘑菇(以及不断产生的蘑菇)可以全部拿到,可以赋为点权。之后 s p f a spfa spfa 求最长路即可。
核心代码如下:
for(int i=n+1;i<=co;i++)dis[i]=-1e9;
dis[color[s]]=sum[color[s]];
queue<int>q;
q.push(color[s]);
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=0;
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(dis[v]<dis[u]+e[i].w+sum[v]){
dis[v]=dis[u]+e[i].w+sum[v];
if(vis[v]==0){
vis[v]=1;
q.push(v);
}
}
}
}
for(int u=1; u<=n; u++) {
for(int i=vex[u]; i; i=e[i].next) {
int v=e[i].v;
if(color[u]!=color[v]){
add(color[u],color[v],e[i].w);
}else{
sum[color[u]]+=f(e[i].w,e[i].r);
}
}
}
例题8:缩点+dp
- 题目描述: 给定一个图,有点权。找出一条路径,使得这条路径经过的点权和最大(同一个点经过多次只计算一次)。输出最大点权和,以及这条路径经过的点权最大值。 n < = 2 e 5 , m < = 5 e 5 n<=2e5,m<=5e5 n<=2e5,m<=5e5
- 问题分析: 缩点后 D A G DAG DAG 图上 d p dp dp
- 状态设置: f [ i ] f[i] f[i] 为到 i i i 点的路径点权最大和。 g [ i ] g[i] g[i] 为到 i i i 点的路径点权最大值。
- 转移方程: f [ v ] = m a x ( f [ u ] ) + a [ v ] f[v]=max(f[u])+a[v] f[v]=max(f[u])+a[v], g [ v ] = m a x ( g [ u ] , m a x x [ v ] ) g[v]=max(g[u],maxx[v]) g[v]=max(g[u],maxx[v])。【补充】后者的 u u u 为令 f [ v ] f[v] f[v] 最大的 u u u , a [ v ] , m a x x [ v ] a[v],maxx[v] a[v],maxx[v] 分别为强连通分量 v v v 的点权和以及点权最大值。
例题9:缩点+spfa+反向建图
- 题目描述: 给定一个有向图。有一次将某条边,添加一条对应的反向边,变成无向边的机会。求从起点 1 出发,回到终点 1 ,经过的点数量最多为多少。 n , m < = 1 e 5 n,m<=1e5 n,m<=1e5
- 问题分析: 先缩点。 s p f a spfa spfa 分别求出 1 1 1 到所有点的最长路 d i s 1 dis1 dis1,以及所有点到 1 1 1 的最长路 d i s 2 dis2 dis2(反向建图)。这条反向边导致的结果: 1 1 1 到达某个点 u u u , u u u 通过这条反向边到达某个点 v v v , v v v 到达点 1 1 1 。枚举 u u u ,枚举 u u u 的反向边 v v v ,计算出最大值 m a x ( d i s 1 [ u ] + d i s 2 [ v ] − s u m [ c o l o r [ 1 ] ] ) max(dis1[u]+dis2[v]-sum[color[1]]) max(dis1[u]+dis2[v]−sum[color[1]]) 即可。
二.割点与割边
1.定义
- 割点: 给定一张无向连通图,若存在点 u u u 满足删除该点后,连通图不再连通,则该点 u u u 为该图的割点
- 割边: 给定一张无向连通图,若存在边 e e e 满足删除该边后,连通图不再连通,则该边 e e e 为该图的割边 (也称“桥”)
2.算法tarjan
- 实现方法: tarjan 跑一个深搜优先树
- 对于根节点:若其有两个以上的孩子,则该点为割点
- 对于非根点:若从 u u u 出发跑,不能到达 u u u 的上层,则 u u u 点为割点
- 对于任意点:若从边 u v uv uv 跑,不能跑到 u u u 点以及 u u u 点的上层,则该边 u v uv uv 为割边
根据定义很好写出代码
void tarjan(int u,int root) {
dfn[u]=low[u]=++cnt;
int child=0;
for(int i=vex[u]; i; i=e[i].next) {
int v=e[i].v;
if(!dfn[v]) {
tarjan(v,root);
low[u]=min(low[v],low[u]);
if(low[v]>=dfn[u]&&u!=root)cutnode[u]=1;
//子节点出发能到的最早点在该节点后或等于该节点则该点为割点
if(low[v]>dfn[u])cutedge[i]=1;
//子节点出发能到的最早点在该节点后则该边为割边
//反向边也为割边,即编号从0开始存的,i与i^1
child++;//统计根节点的孩子数
}
else low[u]=min(low[u],dfn[v]);
}
if(u==root&&child>=2)cut[u]=1;//根节点有两个子树则为割点
if(dfn[u]==low[u]){
co++;
while(1){
int t=stk[top];
color[t]=co;
top--;
instk[t]=0;
if(t==u)break;
}
}
}
- 为什么这次没有像求强联通分量的时候一样 e l s e i f ( i n s t k [ ] ) else if(instk[]) elseif(instk[])???因为在无向图求割点割边的时候不会出现存在可到达点属于它自己的强联通分量的情况
三.双联通分量及圆方树
点双联通分量
- 定义: 若无向连通图中,去掉任意一个节点都不会改变此图的连通性,即不存在割点,则称作点双连通图。一个无向连通图中的每一个极大点双连通子图称作此无向图的点双连通分量。
- 性质: 一个无向图一定是由若干个点双连通分量构成的,且点双连通分量之间由 割点 连接
- 算法: tarjan求割点,删除割点,dfs求连通块即可
void tarjan(int u,int root) {
dfn[u]=low[u]=++cnt;
int child=0;
for(int i=vex[u]; i!=-1 i=e[i].next) {
int v=e[i].v;
if(!dfn[v]) {
tarjan(v,root);
low[u]=min(low[v],low[u]);
if(low[v]>=dfn[u]&&u!=root)cutnode[u]=1;
child++;
}
else low[u]=min(low[u],dfn[v]);
}
if(u==root&&child>=2)cutnode[u]=1;
}
void dfs(int u){
vis[u]=1;
for(int i=vex[u];i!=-1;i=e[i].next){
int v=e[i].v;
if(cutnode[v]||vis[v])continue;
dfs(v);
}
}
void solve() {
tarjan(1,1);
for(int i=1;i<=n;i++)if(!vis[i]&&cutnode[i]==0)dfs(i);
}
边双连通分量
- 定义: 若无向连通图中,去掉任意一条边都不会改变此图的连通性,即不存在割边,则称作边双连通图。一个无向连通图中的每一个极大边双连通子图称作此无向图的边双连通分量。
- 性质: 一个无向图一定是由若干个边双连通分量构成的,且边双连通分量之间由 割边 连接
- 算法: tarjan求割边,删除割边,dfs求连通块即可
void tarjan(int u,int root) {
dfn[u]=low[u]=++cnt;
for(int i=vex[u]; i!=-1 i=e[i].next) {
int v=e[i].v;
if(!dfn[v]) {
tarjan(v,root);
low[u]=min(low[v],low[u]);
if(low[v]>dfn[u])cutedge[i]=cutedge[i^1]=1;
}
else low[u]=min(low[u],dfn[v]);
}
}
void dfs(int u){
visnode[u]=1;
for(int i=vex[u];i!=-1;i=e[i].next){
int v=e[i].v;
if(cutedge[i]||visdege[i])continue;
visedge[i]=visedge[i^1]=1;
dfs(v);
}
}
void solve() {
tarjan(1,1);
for(int i=1;i<=n;i++)if(!visnode[i])dfs(i);
}
圆方树
- 前言:
- 有向图中,强连通分量缩成点,得到一张 D A G DAG DAG 图。
- 无向图中,点双连通分量变形成菊花图,得到一棵树。
- 构造方法: 原图里每个节点都是原点,将每个环里加入一个方点,方点直接连向环内各个节点。如下图: