CSP-S 知识点复健 ---- 图论

引子:垃圾 f s y fsy fsy的图论比 f s y fsy fsy 还垃圾,于是来补一补


割点 x x x 是割点,当且仅当:

  1. x 是 d f s dfs dfs 树上的根结点且有两个以上的儿子
  2. l o w [ s o n ] ≥ d f n [ x ] low[son] \ge dfn[x] low[son]dfn[x]

因为大于等于说明能回到的最远点就是它
由于判断的是 l o w [ s o n ] ≥ d f n [ x ] low[son]\ge dfn[x] low[son]dfn[x],那么就不需要考虑父结点或重边的问题,从 x x x 出发的所有点的时间戳都可以用来更新 x x x

void dfs(int u){
	low[u] = dfn[u] = ++sign; int cnt = 0;
	for(int i = first[u]; i; i = nxt[i]){
		int t = to[i]; if(!dfn[t]){
			dfs(t), low[u] = min(low[u], low[t]);
			if(low[t] >= dfn[u]){ ++cnt; if(u != rt || cnt > 1) cut[u] = 1;}
		}
		else low[u] = min(low[u], dfn[t]);
	}
}

割边 x x x 是割边,当且仅当:
l o w [ s o n ] > d f n [ x ] low[son] > dfn[x] low[son]>dfn[x]
然后不能回到父亲边,考虑重边的影响,需要记录来的边的编号

void dfs(int u, int from){
	low[u] = dfn[u] = ++sign;
	for(int i = first[u]; i; i = nxt[i]){
		int t = to[i]; if(!dfn[t]){
			dfs(t, i), low[u] = min(low[u], low[t]);
			if(low[t] > dfn[u]) bridge[i >> 1] = true;
		}
		else if(i != (from ^ 1)) low[u] = min(low[u], dfn[t]);
	}
}

边双联通分量:删去所有桥后每个联通块就是一个边双联通分量
边双的缩点:保留所有桥边,连接两端的边双联通


点双联通分量
定义:在一个无向图中,若任意两点间至少存在两条 “点不重复” 的路径,则说这个图是点双连通的
性质:

  1. 任意两点间至少存在两条点不重复的路径等价于图中删去任意一个点都不会改变图的连通性,
    即 BCC 中无割点
  2. 若BCC间有公共点,则公共点为原图的割点
  3. 无向连通图中割点一定属于至少两个BCC,非割点只属于一个BCC

求法:

  1. 当一个结点被第一次访问时,入栈
  2. d f n [ x ] ≤ l o w [ s o n ] dfn[x]\le low[son] dfn[x]low[son] 时,无论 x x x是不是跟,都要:
    弹栈直到 s o n son son 被弹出,弹出的所有点
    弹出的点与 x x x 构成点双联通分量
void dfs(int u){
	low[u] = dfn[u] = ++sign; sta[++top] = u;
	for(int i = first[u]; i; i = nxt[i]){
		int t = to[i]; 
		if(!dfn[t]){
			dfs(t); low[u] = min(low[u], low[t]);
			if(low[t] >= dfn[u]){
				++id; do{ v[id].push_back(sta[top]); } while(sta[top--] != t);
				v[id].push_back(u);
			}
		} else low[u] = min(low[u], dfn[t]);
	}
}

点双联通的缩点
一个割点可能属于多个点双联通分量,缩完点后的点的个数等于割点数+点双连通分量个数
割点与包涵它的所有点双连边
连出来会是一棵树,可以求得无向图两点间的必经点:
树上两点路径上的割点


有向图的强连通分量
l o w [ x ] low[x] low[x] 为满足下列条件的最小时间戳:

  1. 在栈中
  2. 存在一条从 s u b t r e e ( x ) subtree(x) subtree(x) 出发的有向边以该点为终点

做法:

  1. 如果 y y y 没有访问过,那么 ( x , y ) (x,y) (x,y) 为树边,递归访问 y y y, 令 l o w [ x ] = m i n ( l o w [ x ] , l o w [ y ] ) low[x]=min(low[x],low[y]) low[x]=min(low[x],low[y])
  2. 如果 y y y 访问过,并且在栈中,那么令 l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x]= min(low[x],dfn[y]) low[x]=min(low[x],dfn[y])
  3. 回溯之前判断 d f n [ x ] = l o w [ x ] dfn[x]=low[x] dfn[x]=low[x],如果成立,弹栈直到 x x x,这些点属于一个强连通分量

强制在栈中才更新 l o w [ x ] low[x] low[x] 是为了避免横叉边的影响


2-SAT
重点讲方案输出的问题
法1:首先,在一个 S C C SCC SCC 中,只要确定了一个变量的赋值,其它的也就定了,这启发我们直接缩点
其次,如果我们选择一个有出点的边,它指向的点也要选,这样一来会比较繁琐
我们考虑先选择出度为 0 的点,然后反向做一次拓扑排序
法2: t a r j a n tarjan tarjan 算法的本质是一次 d f s dfs dfs,它回溯时会先取出有向图底部的 S C C SCC SCC 进行标记
所以说 t a r j a n tarjan tarjan 算法的 S C C SCC SCC 编号本身就对应自底向上的拓扑序
对于两个对应点,我们只需要选择 S C C SCC SCC 编号较小的那一个即可


二分图匹配的扩展
最小点覆盖:选择点集 S S S,使得任意一条边至少有一个顶点在 S S S中,求 m i n ( ∣ S ∣ ) min(|S|) min(S)
最小点覆盖 = 最大匹配
证明 感性理解:如果不是最大匹配那么会有一条边两个点都没有被覆盖

最大独立集:最大独立集 = n - 最大匹配
证明:最大独立集 ⇔ \Leftrightarrow 在图中去掉最少的点,使得剩下的点没有边
在图中去掉最少的点,使得剩下的点没有边 ⇔ \Leftrightarrow 选最少的点覆盖所有的边

D A G DAG DAG 路径覆盖1:用不相交的链覆盖 D A G DAG DAG,求链的最少个数
首先令答案为 n n n,相邻的两条路径可以接在一起,也就是可以匹配
每次匹配路径数减去1,最终答案为 n − m a t c h n-match nmatch,要让这个最小
也就是 m a t c h match match 最大,拆点求个最大匹配即可

D A G DAG DAG 路径覆盖2:用可以相交的链覆盖 D A G DAG DAG
若有两条路径 u − − p − − v u--p--v upv x − − p − − y x--p--y xpy
我们将 x x x y y y 连边就可以避免重复走 p p p, 求出传递闭包后同上
法2:相交可以看做 x x x 越过 y y y 去匹配后面的点,那么网络流时 y ′ y' y y y y 连流量正无穷的边即可


无向图最小环问题:
f l o y e d floyed floyed 外层循环执行到 k k k 时, d i s [ i ] [ j ] dis[i][j] dis[i][j] 表示的是 i i i j j j 经过不超过 k − 1 k-1 k1 的点的最短路
于是我们可以枚举 i , j i,j i,j,用 a i , k + a k , j + d i s i , j a_{i,k}+a_{k,j}+dis_{i,j} ai,k+ak,j+disi,j 去更新答案


有向图最小环问题
D i j s k t r a Dijsktra Dijsktra, 枚举起点 s s s,更新完 s s s 的出点过后将 d i s [ s ] dis[s] dis[s] 设成 i n f inf inf,然后 s s s 第二次从队列中取出时的 d i s dis dis 就是最小环


s p f a spfa spfa 判负环
c n t [ x ] cnt[x] cnt[x] 记录 1 到 x x x 的最短路包涵的边数
当更新 d i s [ y ] = d i s [ x ] + z dis[y]=dis[x]+z dis[y]=dis[x]+z 时,同样更新 c n t [ y ] = c n t [ x ] + 1 cnt[y]=cnt[x]+1 cnt[y]=cnt[x]+1
如果发现 c n t [ x ] ≥ n cnt[x]\ge n cnt[x]n,那么出现了负环

bool spfa(){
	memset(dis, 63, sizeof(dis));
	memset(vis, 0, sizeof(vis));
	memset(cnt, 0, sizeof(cnt));
	queue<int> q; q.push(1); dis[1] = 0;
	while(!q.empty()){
		int x = q.front(); q.pop(); vis[x] = 0;
		if(cnt[x] >= n) return true;
		for(int i = first[x]; i; i = nxt[i]){
			int t = to[i]; if(dis[t] > dis[x] + w[i]){
				dis[t] = dis[x] + w[i]; cnt[t] = cnt[x] + 1;
				if(!vis[t]) q.push(t), vis[t] = 1;
			}
		}
	} return false;
}

差分约束
给出 x i − x j ≤ k x_i-x_j\le k xixjk 的限制,求 m a x ( x n − x 0 ) max(x_n-x_0) max(xnx0)
考虑将所有不等式移项,加减,全部变成 x n − x 0 ≤ k i x_n-x_0 \le k_i xnx0ki
比如将 x n − x 1 ≤ k 1 x_n-x_1\le k_1 xnx1k1 x 1 − x 2 ≤ k 2 x_1-x_2\le k_2 x1x2k2 x 2 − x 0 ≤ k 3 x_2-x_0\le k_3 x2x0k3, 变成 x n − x 0 ≤ k 1 + k 2 + k 3 x_n-x_0\le k_1+k_2+k_3 xnx0k1+k2+k3
那么 m a x ( x n − x 0 ) = m i n ( k i ) max(x_n-x_0)=min(k_i) max(xnx0)=min(ki) m i n ( k i ) min(k_i) min(ki) 恰为 0 0 0 n n n 的最短路
于是对于 x i − x j ≤ k x_i-x_j \le k xixjk 的限制, j j j i i i 连一条权值为 k k k 的边跑最短路即可
另一种理解是 x i − x j ≤ k x_i-x_j \le k xixjk x i ≤ x j + k x_i\le x_j+k xixj+k d i s [ y ] ≤ d i s [ x ] + c [ i ] dis[y] \le dis[x]+c[i] dis[y]dis[x]+c[i] 类似
一般会对题目条件做一些等价变形
如果求 m i n ( x n − x 0 ) min(x_n-x_0) min(xnx0),需要把限制转换成 x i − x j ≥ k x_i-x_j \ge k xixjk 然后求最长路


欧拉回路
无向图:存在欧拉回路当前仅当 d e g [ x ] deg[x] deg[x] 全部为偶数
存在半欧拉回路(每条边经过一次,不用走回来),当且仅当每个点度数为偶数,只有两个点可以为奇数
这两个点就是起到和终点
有向图:存在欧拉回路当且仅当 i n [ x ] = o u t [ x ] in[x] = out[x] in[x]=out[x]
方案: d f s dfs dfs 搜索,不能再往下走便回溯,回溯时记录路径,将路径压入栈,最后将栈逆序输出就是一条欧拉回路,正确性看个图就会了 传送门

void dfs(int u){
	for(int &i = first[u]; i; i = nxt[i]){
		int t = to[i]; int j = i;
		if(!vis[j]){ vis[j] = 1; dfs(t); ans[++cnt] = j;}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

FSYo

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

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

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

打赏作者

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

抵扣说明:

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

余额充值