最短路&差分约束笔记

最短路径

基础算法

特殊图

特殊图即边权只包含 \(0,1\)\(1\) 或某个特定的数的图。这种图可以用 \(bfs\)\(\rm O(n)\) 时间内求出单源最短路,在 \(\rm O(n^2)\) 内求出多源最短路。

单源最短路径

单元最短路径指的是在一张联通图中,起点 \(s\) 到其他所有点的最短路径。

计算单元最短路的常见算法有:\(spfa\)\(dijkstra\)

若图带负边权(注意,此时只能是有向图,无向图负边权类似负环),则必须使用 \(spfa\),时间复杂度 \(O(kE)\)\(E\) 表示边的数量;最坏时间复杂度会被卡到 \(O(VE)\)\(V\) 表示点的数量。

若图带正边权,则使用 \(dijkstra\) 速度更快。堆优化时间复杂度约为 \(O(m\log n)\),相当稳定。

若图边权为 \(1\),则使用 \(bfs\) 即可,由于 \(bfs\) 的特性,第一次碰到的点就是最短路,所以时间复杂度是 \(O(n)\);若边权是 \(0,1\),使用 \(01bfs\) 即可,时间复杂度仍为 \(O(n)\)

多源最短路径

在一张连通图中,分别以点 \(1\sim n\) 为起点,到其他点的最短距离。

\(Floyed\) 算法可以在 \(O(n^3)\) 的时间求解多源最短路。算法的代码是这样的:

    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                f[i][j]=min(f[i][k]+f[k][j],f[i][j]);

本质是:当 \(k=2\) 时,任意点对 \(i\rightarrow j\) 的最短路径上都只包括了 \(1,2\) 两点(端点不算),对于 \(3\sim n\) 的点,并未包括在内。

坑点:该算法需要判断重边的情况(求最短路的话是取边中的\(\min\))。

可以用这个性质 \(Floyed\) 算法还能求解图中的最小环。题目:无向图的最小环问题

\(Floyed\) 的衍生应用:求传递闭包(即任意两点之间的连通性)。

朴素的 \(\rm O(n^3)\) 做法:直接将 f[i][j]=min(f[i][k]+f[k][j],f[i][j]); 改为 f[i][j]|=f[i][k]&f[k][j] 即可。

\(bitset\) 优化的传递闭包,时间复杂度 \(\rm O(\frac{n^3}{\omega})\)
观察到如果 \(f_{i,k}=1\),那么原式即可转化为:\(f_{i,j}|=f_{k,j}\),而由于 \(j\) 是同一位,那么即用 \(bitset\)\(f_{i}\) 表示为点 \(i\) 的联通情况,原式转化为 \(f_i|=f_k\),代码:

for(int k=1;k<=n;k++){
    for(int i=1;i<=n;i++) {
	    if(f[i][k]) f[i]=f[i]|f[k];
    }
}

模板题:[JSOI2010] 连通数


\(Johnson\) 全源最短路算法。

先从一超级源点(记作 \(0\),向每个点连一条权值为 \(0\) 的边)出发,计算出到每个点的最短距离 \(d_i\),接着将 \(u\rightarrow v\) 的边权重新赋值为 \(d_u-d_v+w_{u\rightarrow v}\),然后从每个点跑一遍 \(dijkstra\)即可,时复(\(O(nm\log n\))。

最短路满足三角不等式:\(d_v\le d_u+w_{u\rightarrow v}\),所以新赋值的边权一定是 \(\ge 0\) 的,并且对于一条 \(a\rightarrow b\rightarrow c\) 的路径,用赋值后的边权计算得:\(d_a-d_c+w_{a\rightarrow b}+w_{b\rightarrow c}\),容易发现只与起点和终点的 \(d\) 有关,所以这样求出来的最短路一定是最短的。

多源多汇最短路

并不等同于多源最短路,这种问题包括:一个终点,多个起点,求起点到终点的最短路;多个起点,多个终点,求任意两点的最短路的最值。

这种问题的通用解法,就是建立超级原点或超级汇点。

对于第一个问题,我们可以建立超级源点,记作 \(0\),然后向每个起点连一条边权为 \(0\) 的边,然后直接从 \(0\) 号点跑一遍最短路即可。

对于第二个问题,建立一个超级原点(记作 \(0\)),向每个起点连一条边权为 \(0\) 的边,建立一个超级汇点(记作 \(n+1\)),从每个终点向其连一条边权为 \(0\) 边,接着从 \(0\)\(n+1\) 跑一遍最短路即可。

负环

【模板】负环
负环是一个相当恶心的东西,只要有负环在,就不存在最短路(可以在负环绕,越绕距离越小)(包括负边权的双向边),判负环的方式就是跑 \(spfa\),然后记录一个 \(cnt\) 数组,表示最短路径点的数量,如果存在 \(cnt_x>n\),说明最短路径上的点超过了 \(n\),但是这显然是不可能的。

判断负环一定要从一个可以到达所有点的点出发,需要建立超级源点。并且有一个很 \(trick\) 的方法,就是当总入队次数超过 \(5\times E\) 时,说明很大概率是有负环的,此时直接跳出即可。

最短路径树(SPT)

这里讲了。

同余最短路

用于解决 \(0\le\sum_{i=1}^{n}a_ix_i\le n\) 的问题。

我们记一个合法解为 \(b=\sum_{i=1}^{n}a_ix_i\),那么 \(b\) 显然可以表示为 \(b=i+p\times q\) 的形式(其中\(i<q\)),那么 \(i\) 实际上是 \(b\)\(q\) 意义下的值。如果对于一个 \(q\),我们可以求出所有可行的 \(i\),那么我们显然可以计算出 \(n\) 以内的方案数。

\(dis_i\) 表示模 \(q\) 意义下为 \(i\) 的最小值。比如 \(dis_2\) 表示模 \(3=2\) 的最小值,也许是 \(2\),也可以是 \(5\)

那么显然有如下转移:

\[dis_{(i+a[j])\% p}=\min\{dis_i+a[j]\} \]

由于转移顺序并不确定,所以将其当做最短路来做。可以看做 \(i\)\((i+a[j])\% p\) 连了一条边权为 \(a[j]\) 的边。

然后跑一遍最短路,求出 \(dis_i\),那么答案就是\(\sum_{i=0}^{q-1}\left \lfloor\frac{n-dis_i}{p}\right \rfloor+1\)

差分约束

差分约束可用于求解形如下面的方程组的特殊解:

\[\begin{cases} x_{i_1}-x_{j_1}\ge a_1 \\ x_{i_2}-x_{j_2}\ge a_2 \\ ...\\ x_{i_n}-x_{j_n}\ge a_n \end{cases} \]

我们随便拿一个式子出来,变形得:

\[x_i\ge x_j+a_k \]

可以发现,这个东西很像三角不等式。如果将其看做从 \(j\)\(i\) 连的一条边权为 \(k\) 的有向边,那么上述结果就是求完最长路的情况。

如果求最长路,如果存在正环,就说明无解;求最短路,存在负环,说明无解。

如果求最小值,我们显然将式子变形成:\(x_i\ge x_j+a_k\) 的形式,即跑一遍最长路;如果求最大值,将式子变形成 \(x_i\le x_j+a_k\) 的形式,跑最短路。

一定要从一个可以到达所有点的点开始,这样才能保证符合所有情况,建立超级源点即可。

题目

最短路计数

题意简述:求 \(1\) 到其他点的最短路数量(边权为 \(1\))。

算是一个基本模型,我们在原来记录最短路长度的基础上再记录一个方案数,在松弛的时候,如果距离等于现在储存的距离,就 \(+1\),否则设为走过来的点的方案数。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

const int N=1e6+10;
const int MOD=100003;
int n,m;
vector<int> g[N];

queue<int> q;
int dis[N],vis[N],cnt[N];

void bfs() {
    memset(dis,0x3f,sizeof(dis));
    dis[1]=0; vis[1]=1; cnt[1]=1; q.push(1);

    while(q.size()) {
        int x=q.front(); q.pop();
        for(int y:g[x]) {
            if(dis[y]>dis[x]+1) {
                dis[y]=dis[x]+1;
                cnt[y]=cnt[x];
                if(!vis[y]) {
                    vis[y]=1;
                    q.push(y);
                }
            }
            else if(dis[y]==dis[x]+1) {
                cnt[y]=(cnt[y]+cnt[x])%MOD;
            }
        }
    }
    for(int i=1;i<=n;i++) cout<<cnt[i]<<'\n';
}

int main() {
    cin>>n>>m;
    for(int i=1;i<=m;i++) {
        int x,y; cin>>x>>y;
        g[x].push_back(y); g[y].push_back(x);
    }
    bfs();
    return 0;
}

灾后重建

题意简述:每个点有一个恢复时间,在这个时间之前,计算最短路不能经过这个点,现在给定 \(x,y,t\),要求计算在 \(t\) 时刻,从 \(x\)\(y\) 的最短路。

考察对 \(Floyed\) 的理解。

首先是一个全源最短路问题,观察到 \(n\le 200\),大胆用 \(Floyed\),由于该算法的本质是:只有在以 \(k\) 为中转点计算过以后,任意两点的最短路中才会包含 \(k\) 点,所以我们可以写一个 \(update(k)\) 函数,表示将 \(k\) 作为中转点计算一遍。

由于题目保证 \(t\) 不降,所以还是挺好写的。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

const int N=210;
int n,m,q;
int f[N][N];
int bt[N],vis[N];

void change(int k) { //将k作为中转点计算
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
}

int main() {
    memset(f,0x3f,sizeof(f));
    cin>>n>>m;
    for(int i=1;i<=n;i++) {
        cin>>bt[i];
    }
    for(int i=1;i<=m;i++) {
        int x,y,w; cin>>x>>y>>w;
        ++x; ++y;
        f[x][y]=f[y][x]=w;
    }
    int p=1;
    cin>>q;
    while(q--) {
        int x,y,t; cin>>x>>y>>t;
        ++x; ++y;
        while(bt[p]<=t&&p<=n) {
            change(p);
            vis[p]=1;
            p++;
        }
        if(!vis[x]||!vis[y]||f[x][y]==1061109567) cout<<-1<<endl;
        else cout<<f[x][y]<<endl;
    }


    return 0;
}

大逃离

题意简述:求严格次短路。

记录 \(dismin_x\) 表示到 \(x\) 最短路,\(discmin_x\) 表示到 \(x\) 的严格次短路,仔细分析一下转移就好。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N=5100;
int n,m,k;
int deg[N];
struct node {
    int to,w;
};
vector<node> g[N];
set<int> se[N];

void add(int x,int y,int z) {
    g[x].push_back({y,z});
}

queue<int> q;
priority_queue<int,vector<int>,greater<int>> qq;
LL dis_min[N],dis_cmin[N]; int f[N];

void spfa() {
    q.push(1); f[1]=1;
    memset(dis_min,0x3f,sizeof(dis_min));
    memset(dis_cmin,0x3f,sizeof(dis_cmin));
    
    dis_min[1]=0;
    for(node t:g[1]) {
        int y=t.to,w=t.w;
        if(y!=n&&deg[y]<k) continue;
        if(dis_min[1]+2*w<dis_cmin[1]) dis_cmin[1]=dis_min[1]+2*w;
    }

    while(q.size()) {
        int x=q.front(); f[x]=0; q.pop();
        for(node t:g[x]) {
            int y=t.to,w=t.w;
            if(y!=n&&deg[y]<k) continue;
            if(dis_min[y]>dis_min[x]+w) {
                dis_cmin[y]=dis_min[y];
                dis_min[y]=dis_min[x]+w;
                if(!f[y]) {
                    f[y]=1;
                    q.push(y);
                }
                if(dis_cmin[y]>dis_cmin[x]+w) {
                    dis_cmin[y]=dis_cmin[x]+w;
                }
            }
            else if(dis_min[y]!=dis_min[x]+w&&dis_cmin[y]>dis_min[x]+w) {
                dis_cmin[y]=dis_min[x]+w;
                if(!f[y]) {
                    f[y]=1;
                    q.push(y);
                }
            }
            dis_cmin[y]=min(dis_cmin[y],dis_min[y]+2*w);
        }
    }
}

int main() {
    cin>>n>>m>>k;
    for(int i=1;i<=m;i++) {
        int x,y,z;
        cin>>x>>y>>z;
        add(x,y,z);
        add(y,x,z);
        se[x].insert(y); se[y].insert(x);
    }
    for(int i=1;i<=n;i++) {
    	deg[i]=se[i].size();
	}
    spfa();
    if(dis_cmin[n]==4557430888798830399) cout<<-1;
    else cout<<dis_cmin[n];

    return 0;
}

Elaxia的路线

题意简述:求两个点对最短路的最长公共路径。

写过题解,这里再强调一下最短路的相关应用:
判断一条边 \(u\leftrightarrow v\) 是否在 \(s\rightarrow t\) 的最短路上呢?只要满足如下式子:

\[dis_{s\rightarrow u}+w_{u\rightarrow v} +dis_{v\rightarrow t}=dis_{s\rightarrow t} \]

或者:

\[dis_{s\rightarrow v}+w_{v\rightarrow u} +dis_{u\rightarrow t}=dis_{s\rightarrow t} \]

即可,这里需要从起点和终点分别出发跑一遍最短路。

[POI2014] RAJ-Rally

题意简述:给定一个边权均为 \(1\)\(DAG\),求删去一个点后的最长路径的最小值。

想不到啊,居然真的是一个一个删点,然后优化求最小值。。。

\(dst_x\) 表示 \(x\) 为起点的最短路,\(ded_x\) 表示 \(x\) 为终点的最短路。

\(DAG\) 中有一个很神奇的结论。最短路可以表示为:

\[ded_u+w_{u\rightarrow v}+dst_v \]

其实很好理解,若存在一条 \(u\rightarrow v\) 的边,那么 \(v\) 的拓扑序一定是 \(>u\) 的,不可能通过一个环在绕回 \(u\) 的前面。所以在 \(DAG\) 中,这个式子是成立的。

那么在非\(DAG\)中,其实也可以推出类似的结论,我们只需规定好起点终点即可:

\[dis_{s\rightarrow u}+w_{u\rightarrow v} +dis_{v\rightarrow t} \]

我们按照拓扑序从小到大进行删点,删完点的集合称作\(A\),还没删的点的集合称作 \(B\),直观的看:\(A\)\(B\) 的前面。

所以最长路应该来自:\(ded_A,dst_B,ded_A+dst_B+w_{A\rightarrow B}\),如果此时删除 \(B\) 集合中的 \(x\) 点,那么很显然,所有包含 \(x\) 点的最短路都应该删去,即,我们应该将 \(dst_x,ded_A+w_{A\rightarrow x}+dst_x\) 删去。然后在剩下的值中统计最小值。在计算完贡献后,显然应该将 \(x\) 点加进 \(A\) 集合中,此时加入 \(ded_x,dst_x+w_{x\rightarrow B}+ded_{B}\) 即可。

为了维护以上信息,我们需要实现几个操作:

  • 插入数字。
  • 删除数字。
  • 查询最小值。

由于数字可能有重复,所以使用 \(multiset\) 即可。注意 \(multiset\) 删除值时,使用:s.erase(s.find(x));,这样是删除一个,而 s.erase(x); 是将 x 全删。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N=5e5+10;
int n,m;
struct node {
    int to,w;
};
vector<node> gz[N],gf[N];

int degz[N],degf[N];
int dis_ed[N],dis_st[N];
int topo_num[N],id[N];

void topoz() {
    queue<int> q;
    while(q.size()) q.pop();
    q.push(0);
    while(q.size()) {
        int x=q.front(); q.pop();
        for(auto t:gz[x]) {
            int y=t.to,w=t.w;
            if(dis_ed[y]<dis_ed[x]+w) {
                dis_ed[y]=dis_ed[x]+w;
            }
            degz[y]--;
            if(!degz[y]) {
                q.push(y);
                topo_num[y]=++topo_num[0];
                id[topo_num[y]]=y;
            }
        }
    }
}

void topof() {
    queue<int> q;
    while(q.size()) q.pop();
    q.push(n+1);
    while(q.size()) {
        int x=q.front(); q.pop();
        for(auto t:gf[x]) {
            int y=t.to,w=t.w;
            if(dis_st[y]<dis_st[x]+w) {
                dis_st[y]=dis_st[x]+w;
            }
            degf[y]--;
            if(!degf[y]) {
                q.push(y);
            }
        }
    }
}

multiset<int> a,b;

int main() {
    cin>>n>>m;
    for(int i=1;i<=m;i++) {
        int x,y; cin>>x>>y;
        gz[x].push_back({y,1});
        degz[y]++;
        gf[y].push_back({x,1});
        degf[x]++;
    }
    for(int i=1;i<=n;i++) {
        degz[i]++;
        gz[0].push_back({i,0});
    }
    for(int i=1;i<=n;i++) {
        gf[n+1].push_back({i,0});
        degf[i]++;
    }

    topof();
    topoz();

    int ans=1e9,p=0;
    for(int i=1;i<=n;i++) b.insert(dis_st[i]);
    for(int i=1;i<=n;i++) {//此处i表示拓扑序
        int x=id[i];
        for(auto t:gf[x]) {
            int y=t.to,w=t.w;
            if(!w) continue;
            a.erase(a.find(dis_ed[y]+dis_st[x]+1));
        }
        b.erase(b.find(dis_st[x]));
        int maxn=0;
        if(b.size()) maxn=max(maxn,*b.rbegin());
        if(a.size()) maxn=max(maxn,*a.rbegin());
        if(maxn<ans) {
            ans=maxn;
            p=x;
        }
        a.insert(dis_ed[x]);
        for(auto t:gz[x]) {
            int y=t.to,w=t.w;
            if(!w) continue;
            a.insert(dis_ed[x]+1+dis_st[y]);
        }
    }
    cout<<p<<' '<<ans;

    return 0;
}

[GXOI/GZOI2019] 旅行者

题意简述:求 \(n\) 个点两两最短路的最小值。

求“两两”的其实并不陌生。多源多汇最短路就能求若干个起点到若干个终点的最短路的最小值,但现在问题是:这题求的两两,我们根本不知道谁是起点,谁是终点。

所以一个很暴力的想法就是:爆搜进行分组,分到 \(1\) 组的作为起点,分到 \(2\) 组的作为终点,建超级源汇跑一遍就能求,时间复杂度 \(O(2^n\times m\log n)\)

这样很显然是过不了的,甚至不如直接枚举起点和终点,然后直接跑最短路,时间复杂度 \(O(n^2m\log n)\)

让我们来思考上面的做法究竟慢在哪里?

我们设取到最小值的两点是 \(st,ed\),那么只要将 \(st,ed\) 分到不同的组别就好,至于谁和他们一组,这个无所谓。

这里就要采用二进制分组的思想,由于 \(st,ed\) 至多有 \(1\) 个二进制位不相同,所以我们按照二进制位分组,枚举 \(0\sim \log n\),若第 \(i\) 位为 \(1\),分到 \(1\) 组,否则分到 \(2\) 组,注意将起点终点调换。

总时间复杂度是 \(O(m\log^2 n)\),开 \(O2\) 还是不成问题的。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N=1e5+10;
int T,n,m,k;
int city[N];
struct node {
    int to,w;
};
vector<node> g[N];
LL minn=1e18;

#define PII pair<LL,int> 
priority_queue<PII> q;
LL dis[N]; int vis[N];

void dij(int st,int ed) {
    memset(vis,0,sizeof(vis));
    memset(dis,0x3f,sizeof(dis));
    dis[st]=0; q.push({0,st});
    while(q.size()) {
        int x=q.top().second; q.pop();
        if(vis[x]) continue;
        vis[x]=1;
        for(node t:g[x]) {
            int y=t.to,w=t.w;
            if(dis[y]>dis[x]+w) {
                dis[y]=dis[x]+w;
                q.push({-dis[y],y});
            }
        }
    }
    minn=min(minn,dis[ed]);
}

void Solve() {
    cin>>n>>m>>k;

    for(int i=1;i<=m;i++) {
        int x,y,z; cin>>x>>y>>z;
        g[x].push_back({y,z});
    }   
    for(int i=1;i<=k;i++) {
        cin>>city[i];
    }
    sort(city+1,city+1+k);
    k=unique(city+1,city+1+k)-city-1;
    int len=log(k);
    for(int i=0;i<=len;i++) {
        for(int j=1;j<=k;j++) {
            if(j&(1<<i)) g[n+2].push_back({city[j],0});
            else g[city[j]].push_back({n+1,0});
        }
        dij(n+2,n+1);
        g[n+2].clear();
        for(int j=1;j<=k;j++) {
            if(!(j&(1<<i))) g[city[j]].pop_back();
        }
        for(int j=1;j<=k;j++) {
            if(!(j&(1<<i))) g[n+2].push_back({city[j],0});
            else g[city[j]].push_back({n+1,0});
        }

        dij(n+2,n+1);
        g[n+2].clear();
        for(int j=1;j<=k;j++) {
            if(j&(1<<i)) g[city[j]].pop_back();
        }        
    }
    cout<<minn<<endl;
}

void Clear() {
    minn=1e18;
    for(int i=0;i<=n+2;i++) g[i].clear();
    memset(city,0,sizeof(city));
    n=m=k=0;
}

int main() {
    cin>>T;
    while(T--) {
        Solve();
        Clear();
    }
    return 0;
}

[国家集训队] 墨墨的等式

\(l\sim r\) 的解转化为 \(0\sim r\) 的解减去 \(0\sim l-1\) 的解即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

#define PII pair<int,int>
typedef long long LL;
const int N=20,M=5e5+10;
int n,q;
LL l,r;
int a[N];
struct node{
    int to,w;
};
vector<node> g[M];

void add(int x,int y,int z) {
    g[x].push_back({y,z});
}

LL dis[M]; int vis[M];
priority_queue<PII> qu;
void dij() {
    memset(dis,0x3f,sizeof(dis));
    dis[0]=0; qu.push({0,0});
    while(qu.size()) {
        int x=qu.top().second; qu.pop();
        if(vis[x]) continue;
        vis[x]=1;
        for(auto t:g[x]) {
            int y=t.to,w=t.w;
            if(dis[y]>dis[x]+w) {
                dis[y]=dis[x]+w;
                qu.push({-dis[y],y});
            }
        }
    }
}

LL query(LL x,LL y) {
    if(x>=y) return (x-y)/q+1;
    else return 0;
}

int main() {
    cin>>n>>l>>r;
    for(int i=1;i<=n;i++) {
        cin>>a[i];
        q=max(q,a[i]);
    }
    for(int i=0;i<q;i++) {
        for(int j=1;j<=n;j++) {
            add(i,(i+a[j])%q,a[j]);
        }
    }

    dij();

    LL ans=0;
    for(int i=0;i<q;i++) {
        ans+=query(r,dis[i])-query(l-1,dis[i]);
    }
    cout<<ans;


    return 0;
}
posted @ 2023-08-14 22:30  2017BeiJiang  阅读(22)  评论(0编辑  收藏  举报