【图论 进阶】差分约束 学习笔记

差分约束意在理解数学 与 图论直接的关系。

文章目录

  • 0x00 差分约束的使用场景
  • 0x10 差分约束工作原理
  • 0x20 差分约束的拓展
    • 0x21 0/1分数规划
    • 0x22 Tarjan优化差分约束
  • 0x30 差分约束的模板 P5960
  • 0x40 例题
    • 0x41 P1993 小 K 的农场
    • 0x42 P2294 [HNOI2005]狡猾的商人
    • 0x43 P2868 [USACO07DEC]Sightseeing Cows G
    • 0x44 P3275 [SCOI2011]糖果
  • 0x50 关于差分约束的猜想

0x00 差分约束的使用场景

当我们遇到类似于如一类问题:
x 1 − x 2 < = y 1 x_1 - x_2 <= y_1 x1x2<=y1
x 2 − x 3 < = y 2 x_2-x_3<=y_2 x2x3<=y2
等等一类的问题,有可能是任意解,有可能是最大或最小解。
这是后我们就会用到差分约束。

0x10 差分约束工作原理

了解差分约束之前,我们首先来回忆一下 Floyd 算法的公式。
例如一条有向边,从i指向j,我们可以发现:
d i s [ j ] > d i s [ i ] + w dis[j] >dis[i]+w dis[j]>dis[i]+w
时,我们需要更新 dis[j] 的取值。
我们发现这个式子与差分约束式子十分的相像,于是考虑用最短路问题来解决这一类问题。
举个例子:
以 luogu P5960 的样例为例:
【图论 进阶】差分约束 学习笔记_第1张图片
我们可以将他建立起如下的一张图:
首先,要移项变号,转换成最短路径 三角不等式的样子:

【图论 进阶】差分约束 学习笔记_第2张图片
很明显,我们需要从 x2 向 x1做一条边权为3的有向边,以此类推其他3条边。如下图所示:
【图论 进阶】差分约束 学习笔记_第3张图片
建图之前,我们明确一点:按照约束条件来建图,因为约束条件可能有很多,例如P1250
此时取值就是从起点(任一点)到该点的距离了。
在这里用我的是求最长路的方法。
其实也可以用最短路懒得画图了嘻嘻嘻
有读者就要问了,这两个有什么区别?我们可以看这样一个式子:
由于
x 1 − x 2 < = y 1 x_1 - x_2 <= y_1 x1x2<=y1
x 2 − x 3 < = y 2 x_2-x_3<=y_2 x2x3<=y2
所以有
x 1 < = y 1 + x 2 < = y 1 + y 2 + x 3 x_1<=y_1 + x_2 <= y_1+y_2+x_3 x1<=y1+x2<=y1+y2+x3
由于y1、y2是定值,当x3越大,x1越大。所以我们可以发现,最短路求最大值,最长路求最小值。

还需要考虑另一个问题:如果构建的图是非连通图怎么办?
很简单,建立一个超级源点,将他与每个点连通,针对他遍历整张图即可。(一般将超级源点定义为0,但是有时节点编号有0,所以也可以定义为n+1)
注意,定义超级源点的条件是求最长路,如果是最短路我们发现到达每个点的最短路都是0.
当我们使用最短路时,最好使用 for 循环寻找从来没进过队列的点,再次遍历spfa寻找环。

0x20 差分约束的拓展

0x21 0/1分数规划

之后我会专门出一个blog介绍0/1分数规划,这里只讲一个大概:
0/1分数规划模型是指,给定整数a1,a2,…,an和b1,b2,…,bn,求一组解xi(1≤i≤n,xi=0∨xi=1)使得下列式子最大化:∑ni=1ai∗xi / ∑ni=1bi∗xi
本质方法就是利用二分答案枚举最大值,查看是否合法。

后面给出了 差分约束 和 0/1分数规划 的题。

0x22 Tarjan优化差分约束

对于最长路,判断是否无解的依据是图中有没有正环。tarjan算法缩点后的某个scc,如果这个scc中有某条边权值大于0 ,且scc中的任意两个点都可互相到达,所以一定存在正环,即不满足差分约束的条件。
最短路同理。
因为同一scc内部的边权都为0,所以同一个scc中的所有点到超级源点的距离都相同,只需要对tarjan缩点后的拓扑图跑最短/长路,求出每个scc的最短/长路即可。

0x30 差分约束的模板 P5960

code:

#include
#include
#include
#include
using namespace std ;

const int MAXN = 5e6+10 ;

int m ,n ;
int d[MAXN] ;

struct Edge{
	int to ;
	int next ;
	int w ;
}edge[MAXN << 1] ;
int head[MAXN] ;
int cnt = 1 ;

inline void add(int from , int to , int w){
	edge[cnt].to = to ;
	edge[cnt].next = head[from] ;
	edge[cnt].w = w ;
	head[from] = cnt++ ;
}

queue<int> q ;
bool in_que[MAXN] ;
int tot[MAXN] ;

inline bool spfa(){
	memset(d , -1 , sizeof(d)) ;
	
	q.push(0) ;
	d[0] = 0 ;
	in_que[0] = true ;
//	tot[0] = 1 ;
	
	while(q.empty() == false){
		int node = q.front() ;
		q.pop() ;
		in_que[node] = false ;
		
		for(int i = head[node];i;i = edge[i].next){
			int neww = edge[i].to ;
			if(d[neww] < d[node] + edge[i].w){
				d[neww] = d[node] + edge[i].w ;
				tot[neww] = tot[node] + 1 ;
				
				if(tot[neww] >= n+1)
					return false ;
				
				if(in_que[neww] == false){
					q.push(neww) ;
					in_que[neww] = true ;
//					tot[neww]++ ;
//					if(tot[neww] >= n + 1)
//						return false ;
				}
			}
		}
	}
	return true ;
}

int main(){
	scanf("%d%d" , &n , &m) ;
	for(int i = 1;i <= m;++i){
		int u ,v ,w ;
		scanf("%d%d%d" , &u , &v , &w) ;
		add(u , v , -w) ;
	}
	for(int i = 1;i <= n;++i)
		add(0 , i , 0) ;//防止图不连通
	
	if(spfa() == false){
		printf("NO") ;
		return 0 ;
	}
	for(int i = 1;i <= n;++i)
		printf("%d " , d[i]) ;
	return 0 ;
}

0x40 例题

0x41 P1993 小 K 的农场

code:

/*
当你建图的时候使用的是s[x]-s[y]<=T形式的方程组建图时,即y向x连一条权值为T的边,
应该选择跑最短路。
如果使用的是s[x]-s[y]>=T形式的方程组来建图时,应该选择跑最长路。
*/
#include
#include
#include
#include
using namespace std ;

const int MAXN = 5005 ;

int m ,n ;
int d[MAXN] ;
int tot[MAXN] ;
bool in_que[MAXN] ;

struct Edge{
	int to ;
	int nxt ;
	int w ;
}edge[15005] ;
int head[MAXN] ;
int cnt = 1 ;

queue<int> q ;

inline void add(int from , int to , int w){
	edge[cnt].to = to ;
	edge[cnt].nxt = head[from] ;
	edge[cnt].w = w ;
	head[from] = cnt++ ;
}

inline bool spfa(){
	memset(d , 0x7f , sizeof(d)) ;
	d[0] = 0 ;
	in_que[0] = true ;
	q.push(0) ;
	
	while(q.empty() == false){
		int node = q.front() ;
		q.pop() ;
		in_que[node] = false ;
		
		for(int i = head[node];i;i = edge[i].nxt){
			int neww = edge[i].to ;
			if(d[neww] > d[node] + edge[i].w){//???
				d[neww] = d[node] + edge[i].w ;
				tot[neww] = tot[node] + 1 ;
				
				if(tot[neww] >= n + 1)
					return false ;
				
				if(in_que[neww] == false){
					q.push(neww) ;
					in_que[neww] = true ;
				}
			}
		}
	}
	return true ;
}

int main(){
	scanf("%d%d" , &n , &m) ;
	for(int i = 1;i <= m;++i){
		int ops ,a ,b ,c ;
		scanf("%d%d%d" , &ops , &a , &b) ;
		if(ops == 1){
			scanf("%d" , &c) ;
			add(a , b , -c) ;
		}else if(ops == 2){
			scanf("%d" , &c) ;
			add(b , a , c) ;
		}else{
			add(a , b , 0) ;
			add(b , a , 0) ;
		}
	}
	for(int i = 1;i <= n;++i)
		add(0 , i , 0) ;
		
	if(spfa() == false)
		printf("No") ;
	else
		printf("Yes") ;
	
	return 0 ;
}

0x42 P2294 [HNOI2005]狡猾的商人

code:

#include
#include
#include
#include
using namespace std ;

const int MAXN = 500005 ;
const int INF = 0x7f7f7f7f ;

int n ,m ;
int t ;
struct Edge{
	int to ;
	int next ;
	int w ;
}edge[MAXN << 1] ;
int head[MAXN] ;
int cnt = 1 ;
int d[MAXN] ;
int tot[MAXN] ;
bool in_que[MAXN] ;
queue<int> q ;

inline void add(int from , int to , int w){
	edge[cnt].to = to ;
	edge[cnt].next = head[from] ;
	edge[cnt].w = w ;
	head[from] = cnt++ ;
}

inline bool spfa(int x){
	while(q.empty() == false)
		q.pop() ;
	for(int i = 1;i <= n;++i)
		d[i] = -INF ;
	q.push(x) ;
	in_que[x] = true ;
	d[x] = 0 ;	
	
	while(q.empty() == false){
		int node = q.front() ;
		q.pop() ;
		in_que[node] = false ;
		for(int i = head[node];i;i = edge[i].next){
			int neww = edge[i].to ;
			if(d[neww] < d[node] + edge[i].w){
				d[neww] = d[node] + edge[i].w ;
				tot[neww] = tot[node] + 1 ;
				
				if(tot[neww] >= n)
					return false ;
				
				if(in_que[neww] == false){
					q.push(neww) ;
					in_que[neww] = false ;
				}
			}
		}
	}
	return true ;
}

int main(){
	scanf("%d" , &t) ;
	for(int tim = 1;tim <= t;++tim){
		memset(head , 0 , sizeof(head)) ;
		memset(in_que , 0 , sizeof(in_que)) ;
		memset(tot , 0 , sizeof(tot)) ;
		
		scanf("%d%d" , &n , &m) ;
		for(int i = 1;i <= m;++i){
			int u ,v ,c ;
			scanf("%d%d%d" , &u , &v , &c) ;
			add(u-1 , v , c) ;
			add(v , u-1 , -c) ;
		}
		
		int f = 0 ;
		for(int i = 0;i <= n-1;++i)
			if(tot[i] == 0)
				if(spfa(i) == false){
					f = 1 ;
					break ;
				}
//		for(int i = 0;i <= n-1;++i)//之所以不行是因为最短路不可以用权值为0的超级源点
//			add(0 , i , 0) ;
//		if(spfa(0) == false)
//			printf("false\n") ;
//		else
//			printf("true\n") ;
		if(f == 0)
			printf("true\n") ;
		else
			printf("false\n") ;
	}
	return 0 ;
}

0x43 P2868 [USACO07DEC]Sightseeing Cows G

有关 0/1分数规划,check()函数就是 SPFA 判断环

#include 
#include 
#include 
using namespace std;

inline double lfabs(double x) {
	return x<0?-x:x;
}

int beg[1005];
int ed[5005];
int nxt[5005];
int len[5005];
int top;

void addedge(int a,int b,int c) {
	++top;
	ed[top] = b;
	len[top] = c;
	nxt[top] = beg[a];
	beg[a] = top;
}
int n;
int fi[5005];
int inq[5005];
int inqn[5005];
double dist[5005];

bool spfa(int s,double delta) {
	dist[s] = 0;
	inq[s] = 0;

	queue<int> q;
	q.push(s);

	while(!q.empty()) {
		int th = q.front();
		q.pop();
		inq[th] = 0;

		for(int p=beg[th]; p; p=nxt[p]) {
			if(dist[th] + (delta*len[p]-fi[th]) < dist[ed[p]]) {
				dist[ed[p]] = dist[th] + (delta*len[p]-fi[th]);

				if(!inq[ed[p]]) {
					q.push(ed[p]);
					++inqn[ed[p]];
					inq[ed[p]] = 1;

					if(inqn[ed[p]] > n+10) {
						return true;
					}
				}
			}
		}
	}

	return false;
}

int main() {
	int p;
	scanf("%d%d",&n,&p);
	for(int i=1; i<=n; ++i) {
		scanf("%d",fi+i);
	}
	for(int i=1; i<=p; ++i) {
		int a,b,t;
		scanf("%d%d%d",&a,&b,&t);
		addedge(a,b,t);
	}

	double l = 0;
	double r = 1005;
	while(lfabs(r-l) >= 0.0001) {
		double mid = (l+r)/2;

		for(int i=1; i<=n; ++i) {
			dist[i] = 99999999;
			inq[i] = inqn[i] = 0;
		}

		for(int i=1; i<=n; ++i) {
			if(!inqn[i]) {
				if(spfa(i,mid)) {
					l = mid;
					goto die;
				}
			}
		}

		r = mid;

		die:;
	}

	printf("%.2lf",l+0.00005);
}

0x44 P3275 [SCOI2011]糖果

code:

#include
#include
#include
#include
#include
using namespace std;

const int MAXN = 100000 + 10;

int n,k;
int scc[MAXN],sum,low[MAXN],dfn[MAXN],cnt,tot[MAXN];
//以上是强连通图的必备变量,唯一tot是记录每个强连通分量里面有多少个点
int dp[MAXN];
//这个用于DP记录答案
int in[MAXN];
//这个记录入度,用于Topo
long long ans;
//最终答案
struct Node {
	int next;//记录每个点的下一个点
	int v;//记录边权
};
vector<Node>nei[MAXN];//旧图
vector<Node>nnei[MAXN];//新图
bool Stack[MAXN];//用于Tarjan
stack<int> s;//用于Tarjan

inline int read() { //快速读入
	int f = 1, x = 0;
	char c = getchar();

	while (c < '0' || c > '9') {
		if (c == '-')
			f = -1;
		c = getchar();
	}

	while (c >= '0' && c <= '9') {
		x = x * 10 + c - '0';
		c = getchar();
	}

	return f * x;
}


void Tarjan(int u) { //Tarjan模板
	low[u] = dfn[u] = ++cnt;
	Stack[u] = true;
	s.push(u);

	int len = nei[u].size();
	for(int i = 0; i < len; i++) {
		int next = nei[u][i].next;

		if(dfn[next] == 0) {
			Tarjan(next);
			low[u] = min(low[u],low[next]);
		} else if(Stack[next]) {
			low[u] = min(low[u],dfn[next]);
		}
	}

	if(dfn[u] == low[u]) {
		sum++;
		scc[u] = sum;
		Stack[u] = false;
		tot[sum]++;

		while(s.top() != u) {
			Stack[s.top()] = false;
			scc[s.top()] = sum;
			s.pop();
			tot[sum]++;
		}
		s.pop();
	}
}

int main() {
	n = read(),k = read();//读入

	for(int i = 1; i <= k; i++) {
		int z = read(),x = read(),y = read();
		switch(z) { //使用开关函数
			case 1: { //一号情况
				nei[x].push_back((Node) {
					y,0
				});
				nei[y].push_back((Node) {
					x,0
				});
				//这里一定要建两条边!
				break;
			}
			case 2: { //二号情况
				nei[x].push_back((Node) {
					y,1
				});
				break;
			}
			case 3: { //三号情况
				nei[y].push_back((Node) {
					x,0
				});
				break;
			}
			case 4: { //四号情况
				nei[y].push_back((Node) {
					x,1
				});
				break;
			}
			case 5: { //五号情况
				nei[x].push_back((Node) {
					y,0
				});
				break;
			}
		}
	}

	for(int i = 1; i <= n; i++) {
		if(dfn[i] == 0)Tarjan(i);//Tajan
	}

	for(int i = 1; i <= n; i++) { //建新图
		int len = nei[i].size();

		for(int j = 0; j < len; j++) {
			int next = nei[i][j].next;
			int xx = scc[i];
			int yy = scc[next];

			if(xx == yy && nei[i][j].v == 1) { //判断无解
				cout<<-1<<"\n";
				return 0;
			}

			if(xx != yy) { //建新图
				nnei[xx].push_back((Node) {
					yy,nei[i][j].v
				});
				in[yy]++;
			}
		}
	}

	queue<int>q;//Topo模板

	for(int i = 1; i <= sum; i++) { //将入读为0的压入队列
		if(!in[i]) {
			q.push(i);
			dp[i] = 1;//初始化
		}
	}

	while(!q.empty()) { //拓扑模板
		int cur = q.front();
		q.pop();
		int len = nnei[cur].size();

		for(int i = 0; i < len; i++) {
			int next = nnei[cur][i].next;
			in[next]--;
			dp[next] = max(dp[next],dp[cur] + nnei[cur][i].v);//Dp方程

			if(!in[next])q.push(next);
		}
	}

	for(int i = 1; i <= sum; i++) { //累加答案
		ans += (long long) dp[i] * tot[i];
	}
	cout<<ans;//输出
	return 0;
}

0x50 关于差分约束的猜想

是否存在一种数学方式证明等式相悖从而证明负环?这将大大减少使用的时间复杂度。
有待证明。

你可能感兴趣的:(算法进阶-学习笔记,图论,学习,算法,c++,csp)