差分约束、拓扑排序、强连通分量(笔记整理)

差分约束系统

概念

  • 一种特殊的 n n n 元一次不等式组,包含 n n n 个变量以及 m m m 个约束条件
  • 每个约束条件是由两个其中的变量做差构成的,形如 x i − x j ≤ c k x_i-x_j≤c_k xixjck ,其中 c k c_k ck 是常数
  • 要解决的问题:求出一组界 x 1 = a 1 , x 2 = a 2 , . . . , x n = a n x_1=a_1,x_2=a_2,...,x_n=a_n x1=a1x2=a2...xn=an,使得所有的约束条件得到满足,否则判断出无解
  • 如果 { a 1 , a 2 , a 3 , . . . , a n } \{a_1,a_2,a_3,...,a_n\} {a1,a2,a3,...,an} 是该系统的一组解,那么对于任意的常数 d d d { a 1 + d , a 2 + d , a 3 + d , . . . , a n + d } \{a_1+d,a_2+d,a_3+d,...,a_n+d\} {a1+d,a2+d,a3+d,...,an+d} 也是该差分约束系统的一组解

与图论模型的联系

  • 求解差分约束系统,都可以转化为图论中的单源最短(长)路问题
  • 用最短路径求差分方程的最大解
    • 对于不等式 x i − x j < = c k x_i-x_j<=c_k xixj<=ck,移项得 x i < = c k + x j x_i<=c_k+x_j xi<=ck+xj,把 x i x_i xi x j x_j xj 看作是图中的两个结点,则该不等式表示的是从结点 x j x_j xj 到结点 x i x_i xi 有一条边权为 c k c_k ck 的有向边,于是就转换成了单源最短路问题
    • 如果最终令 x 1 = 0 x_1=0 x1=0,那么 x i = d i s [ i ] x_i=dis[i] xi=dis[i] 便是差分约束问题的一组解
    • 求解方法: S P F A SPFA SPFA D i j k s t r a Dijkstra Dijkstra
  • 用最长路径求差分方程的最小解.
    • 对于不等式 x i − x j > = c k x_i-x_j>=c_k xixj>=ck,移项得 x i > = c k + x j x_i>=c_k+x_j xi>=ck+xj,把 x i x_i xi x j x_j xj 看作是图中的两个结点,则该不等式表示的是从结点 x j x_j xj 到结点 x i x_i xi 有一条边权为 c k c_k ck 的有向边,于是就转换成了单源最长路问题
    • 如果最终令 x 1 = 0 x_1=0 x1=0,那么 x i = d i s [ i ] x_i=dis[i] xi=dis[i] 便是差分约束问题的一组解
    • 求解方法: S P F A SPFA SPFA D i j k s t r a Dijkstra Dijkstra
  • S P F A SPFA SPFA 只需将松弛部分的符号取反
  • D i j s k t r a Dijsktra Dijsktra 由于最长路没有最优子结构,所以一个点从堆中弹出,并不一定是最长路,修改方法为
    • 小根堆改为大根堆
    • 松弛条件符号取反
    • 允许重复出堆
void spfa(int s) {
	std::queue<int> q;
	for(int i = 0; i <= n; ++i)	dis[i] = -inf, vis[i] = 0;
	q.push(s);
	dis[s] = 0;
	vis[s] = 1;
	while(!q.empty()) {
		int u = q.front(); q.pop();
		vis[u] = 0;
		for(int i = head[u]; ~i; i = e[i].next) {
			int v = e[i].to;
			if(dis[v] < dis[u] + e[i].w) {
				dis[v] = dis[u] + e[i].w;
				if(!vis[v]) {
					q.push(v);
					vis[v] = 1;
				}
			}
		}
	}
}
void dijkstra1(int s) {
	std::priority_queue<std::pair<int,int> > q;
	for(int i = 0; i <= n; ++i)	dis[i] = -inf;
	dis[s] = 0;
	q.push({dis[s],s});
	while(!q.empty()) {
		int u = q.top().second, W = q.top().first; q.pop();
		if(W < dis[u]) continue;
		for(auto it:G[u]){
			int v = it.first, w = it.second;
			if(dis[v] < dis[u] + w){
				dis[v] = dis[u] + w;
				q.push({dis[v],v});
			}
		}
	}
}

解的存在性

  • 在求解最短路的过程中可能会出现以下情况
  • 存在负环, x i − x 1 ≤ T x_i-x_1≤T xix1T 中的 T为无限小, x i x_i xi 的结果不存在
  • 终点不可达,表示 x i x_i xi x 1 x_1 x1 之间没有约束关系, x i x_i xi 的结果可以是无限大,对应的是 d i s [ i ] = i n f dis[i]=inf dis[i]=inf
  • 以上情况均无解

常用转化

  • 给出 x i − x j ≥ T x_i − x_j ≥ T xixjT,可以移项转化为 x j − x i ≤ − T x_j − x_i ≤- T xjxiT
  • 给出 x i − x j < T x_i − x_j < T xixj<T,在整数域上可以转化为 x i − x j ≤ T − 1 x_i − x_j ≤ T-1 xixjT1
  • 给出 x i − x j = T x_i − x_j = T xixj=T,可以转化为 x i − x j ≤ T x_i − x_j ≤ T xixjT x i − x j ≥ T x_i-x_j≥T xixjT
  • x i x j ≤ T \frac{x_i}{x_j}≤T xjxiT,两边同时取对数进行转化


拓扑排序

  • 在一个 D A G DAG DAG(有向无环图)中,我们将图中的顶点以线性方式进行排序,使得对于任何的顶点 u u u v v v 的有向边 ( u , v ) (u, v) (u,v) , 都可以有 u u u v v v 的前面。
  • 给定一个 D A G DAG DAG,如果从 u u u v v v 有边,则认为 v v v 依赖于 u u u 。如果 u u u v v v 有路径 ( u ( u u 可达 v ) v ) v,则称 v v v 间接依赖于 u u u
  • 拓扑排序的目标是将所有节点排序,使得排在前面的节点不能依赖于排在后面的节点。

Kahn算法

  • 将入度为 0 0 0 的点组成一个集合 S S S
  • 每次从 S S S 里面取出一个顶点 u u u (可以随便取)放入 L L L , 然后遍历顶点 u u u 的所有边 ( u , v ) (u, v) (u,v) , 并删除之,并判断如果该边的另一个顶点 v v v,在移除这一条边后入度为 0 0 0 , 那么就将这个顶点放入集合 S S S 中。不断地重复取出顶点然后重复这个过程……
  • 最后当集合为空后,就检查图中是否存在任何边。如果有,那么这个图一定有环路,否者返回 L L L , L L L 中顺序就是拓扑排序的结果
    在这里插入图片描述
bool TopoSort(){
	queue<int> q;
	for(int i=0;i<n;i++)
		if(indegree[i]==0)
			q.push(i);
	vector<int> ans;
	while(!q.empty()){
		int u=q.front();
		q.pop();
		ans.push_back(u);
		for each edge(u,v){
			if(--indegree[v]==0)
				q.push(v);
		}
	}
	if(ans.size()==n){
		for(int i=0;i<n;i++)
			cout<<ans[i]<<" ";
		cout<<endl;
		return true;
	}
	else 
		return false;
}

若要按字典序生成拓扑序列,则把 queue 改为 priority_queue 即可



强连通分量(SCC)

Kosaraju 算法

  • 可以找到有向图中所有的SCC
  • 第一遍 dfs 确定原图的逆后序序列
  • 第二遍 dfs 在反图中按照逆后序序列进行遍历
    • 反图即将原图中的有向边反向
    • 每次由起点遍历到的点即构成一个 SCC

算法模拟

  • DFS 序列
    • 前序序列:第一次达到点 x 的次序,用 d[x] 表示
    • 后序序列:x 点遍历完成的次序,即回溯时间,用 f[x] 表示
    • 逆后序序列:后序序列的逆序

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

const int maxn=10010;
//dnt - dfs序计数
//scnt - scc计数
//dfn[i] - dfs后序序列中第i个点
//c[i] - i号点所在 scc编号
int n,m,a,b,c[maxn],dfn[maxn],SCC[maxn],dcnt,scnt;
bool vis[maxn];
//G1-原图,G2-反图,G3-缩点后的原图
vector<int> G1[maxn],G2[maxn],G3[maxn];

void init(int nn){
	dcnt=scnt=0;
	for(int i=1;i<=n;i++){
		vis[i]=0;
		c[i]=0;
		SCC[i]=0;
	}
}

void dfs1(int x){
	vis[x]=1;
	for(int i=0;i<G1[x].size();i++){
		if(!vis[G1[x][i]])
			dfs1(G1[x][i]);
	} 
	dfn[++dcnt]=x;
}
 
void dfs2(int x){
	c[x]=scnt;
	SCC[scnt]++;
	for(int i=0;i<G2[x].size();i++){
		if(!c[G2[x][i]])
			dfs2(G2[x][i]);
	} 	
} 

void kosaraju(){
	init(n);
	for(int i=1;i<=n;i++)
		if(!vis[i])
			dfs1(i);
	for(int i=n;i>=1;i--){
		if(!c[dfn[i]]){
			++scnt;
			dfs2(dfn[i]);
		}
	}
}
//缩点
void MergePoint(){
	for(int x=0;x<n;x++){
		for(int i=0;i<G1[x].size();i++){
			if(c[x]==c[G1[x][i]])
				continue;
			G3[c[x]].push_back(c[G1[x][i]]);	
		} 
	}
}

相关例题

https://blog.csdn.net/weixin_44771757/article/details/105439877

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值