差分约束意在理解数学 与 图论直接的关系。
当我们遇到类似于如一类问题:
x 1 − x 2 < = y 1 x_1 - x_2 <= y_1 x1−x2<=y1
x 2 − x 3 < = y 2 x_2-x_3<=y_2 x2−x3<=y2
等等一类的问题,有可能是任意解,有可能是最大或最小解。
这是后我们就会用到差分约束。
了解差分约束之前,我们首先来回忆一下 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 的样例为例:
我们可以将他建立起如下的一张图:
首先,要移项变号,转换成最短路径 三角不等式的样子:
很明显,我们需要从 x2 向 x1做一条边权为3的有向边,以此类推其他3条边。如下图所示:
建图之前,我们明确一点:按照约束条件来建图,因为约束条件可能有很多,例如P1250
此时取值就是从起点(任一点)到该点的距离了。
在这里用我的是求最长路的方法。
其实也可以用最短路。懒得画图了嘻嘻嘻
有读者就要问了,这两个有什么区别?我们可以看这样一个式子:
由于
x 1 − x 2 < = y 1 x_1 - x_2 <= y_1 x1−x2<=y1
x 2 − x 3 < = y 2 x_2-x_3<=y_2 x2−x3<=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寻找环。
之后我会专门出一个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分数规划 的题。
对于最长路,判断是否无解的依据是图中有没有正环。tarjan算法缩点后的某个scc,如果这个scc中有某条边权值大于0 ,且scc中的任意两个点都可互相到达,所以一定存在正环,即不满足差分约束的条件。
最短路同理。
因为同一scc内部的边权都为0,所以同一个scc中的所有点到超级源点的距离都相同,只需要对tarjan缩点后的拓扑图跑最短/长路,求出每个scc的最短/长路即可。
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 ;
}
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 ;
}
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 ;
}
有关 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);
}
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;
}
是否存在一种数学方式证明等式相悖从而证明负环?这将大大减少使用的时间复杂度。
有待证明。