最短路合集(分层图最短路、传递闭包、路径还原、k短路...)

ACM-ICPC模板


一、线段树优化的$Dijkstra$

优先队列版本已经烂大街了,这里就不贴了,而且在下面的分层图里有写

1.普通线段树

时间和内存均是优先队列优化版本的 $\frac{1}{2}$

int n, m;
struct edge {
	int to, w, nxt;
	edge() {}
	edge(int t, int ww, int nn) {to = t, w = ww, nxt = nn;}
}e[maxn << 1];
 
int head[maxn], k = 0;
void add(int u, int v, int w) {e[k] = edge(v, w, head[u]); head[u] = k++;}
 
ll ans[maxn];
struct node {
	ll dis; int x;
	node() {}
	node(ll d, int xx) {dis = d, x = xx;}
}dis[maxn << 2];
 
//建树初始化,主要是编号也要返回所以要先预处理一下 
void build(int p, int l, int r) {
	if(l == r) {dis[p].x = l; return;}
	int mid = l + r >> 1;
	build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
	dis[p].x = dis[p << 1].x;
}
 
void change(int p, int l, int r, int x, int y) {
	if(l == r) {dis[p].dis = y; return;}
	int mid = l + r >> 1;
	if(x <= mid) change(p << 1, l, mid, x, y);
	else change(p << 1 | 1, mid + 1, r, x, y);//单点修改的板子操作 
	if(dis[p << 1].dis < dis[p << 1 | 1].dis) dis[p] = dis[p << 1];
	else dis[p] = dis[p << 1 | 1];
}
 
//因为用距离得到最小,但是需要的是编号,所以返回node 
node ask(int p, int l, int r, int ls, int rs) {
	if(ls <= l && r <= rs) {return dis[p];}
	int mid = l + r >> 1; node ans = node(inf, 0), tmp;
	if(ls <= mid) ans = ask(p << 1, l, mid, ls, rs);
	if(rs > mid) {
		node tmp = ask(p << 1 | 1, mid + 1, r, ls, rs);
		if(ans.dis > tmp.dis) ans = tmp;
	}
	return ans;
}
 
int S;
void dij() {
	for(int k = 1; k < n; k++) {//n-1次够用的。虽然我也不知道为什么最后n次跑的比n-1次还要快…… 
		register int u = ask(1, 1, n, 1, n).x;
		for(int i = head[u]; ~i; i = e[i].nxt) {
			register int v = e[i].to;
			if(ans[u] + e[i].w < ans[v]) {//最短路更新 
				ans[v] = ans[u] + e[i].w, change(1, 1, n, v, ans[v]);//单点修改 
			}
		}
		change(1, 1, n, u, inf);//取出来过后要赋值INF,以免再次取用 
	}
}
 
int main() {
	memset(head, -1, sizeof head);
	n = read(), m = read(), S = read();
	for(int u, v, w, i = 1; i <= m; i++) u = read(), v = read(), w = read(), add(u, v, w);
	
	//初始化 
	for(int i = 1; i <= (n << 2); i++) dis[i].dis = inf;
	for(int i = 1; i <= n; i++) ans[i] = inf;
	
	//线段树初始化,dis是线段树,ans是答案 
	build(1, 1, n);
	change(1, 1, n, S, 0); ans[S] = 0;
	dij();
	
	for(int i = 1; i <= n; i++) printf("%lld ", ans[i]);
	return 0;
}

2.最强的zkw线段树优化版本

时间和内存均是优先队列优化版本的 $\frac{1}{4}$ %%%

#define rep(I, A, B) for (int I = (A); I <= (B); ++I)
#define dwn(I, A, B) for (int I = (A); I >= (B); --I)
#define erp(I, X) for (int I = head[X]; I; I = next[I])
const int maxn = 1e5 + 207, maxm = 2e5 + 207, inf = INT_MAX;
int v[maxm], w[maxm], head[maxn], next[maxm], tot;
int dist[maxn], mp[maxn << 2], M = 1;
int n, m, s;

template  inline void read(T& t) {
    int f = 0, c = getchar(); t = 0;
    while (!isdigit(c)) f |= c == '-', c = getchar();
    while (isdigit(c)) t = t * 10 + c - 48, c = getchar();
    if (f) t = -t;
}

template  void write(T x) {
    if (x < 0) x = -x, putchar('-');
    if (x > 9) write(x / 10);
    putchar(x % 10 + 48);
}
inline void ae(int x, int y, int z) { v[++tot] = y; w[tot] = z; next[tot] = head[x]; head[x] = tot; }
inline int cmp(int a, int b) { return dist[a] < dist[b] ? a : b; }
inline void build(int n) {
    while (M < n + 2) M <<= 1;
    mp[0] = n + 1;
}
inline void modify(int x, int nv) {
    for (int i = x + M; dist[mp[i]] > nv; i >>= 1)
        mp[i] = x;
    dist[x] = nv;
}
inline void del(int x) {
    for (mp[x += M] = 0, x >>= 1; x; x >>= 1)
        mp[x] = cmp(mp[x << 1], mp[x << 1 | 1]);
}
inline void dijkstra(int s) {
    rep(i, 0, n) dist[i] = inf;
    build(n); modify(s, 0);
    rep(t, 1, n - 1) {
        int x = mp[1]; del(x);
        erp(i, x) if (dist[v[i]] > dist[x] + w[i])
            modify(v[i], dist[x] + w[i]);
    }
}

int main() {
    read(n),read(m),read(s);
    rep(i, 1, m) {
        int x, y, z; read(x),read(y),read(z); ae(x, y, z);
    }
    dijkstra(s);
    rep(i, 1, n) write(dist[i]), putchar(' ');
    puts("");
    return 0;
}

3.路径还原

白书上的代码,没有优化是$O(n^2)$,正序输出最短路径

/*如果需要输出路径
可以用一个prev[ j ]来记录最短路上顶点 j 的前驱,
那么在o(|V|)的时间内完成最短路径的恢复。
在d[ j ]被d[ j ] = d[ k ] + cost[ k ][ j ]更新时,
修改prev[ j ] = k,这样就可以求出来prev的数组,
可以在以上算法中用路径前驱标记法来还原出来路径。*/

int cost[MAX_V][MAX_V];  //cost[u][v]表示边e=(u, v)的权值(不存在这条边时设为INF)
int d[MAX_V];            //从顶点s出发的最短距离
bool used[MAX_V];        //已经使用过的图中的点
int V;                   //顶点数
int prev[MAX_V];         //记录前驱点
//从s出发到各个顶点的距离
void dijkstra(int s)
{
    fill(d, d + V, INF);            //初始化
    fill(used, used + V, false);    //初始化
    fill(prev, prev + V, -1);
    d[s] = 0; 
    while(true)
    {
        int v = -1;
        for(int u = 0; u < V; u++){
            if(!used[u] && (v == -1 || d[u] < d[v])) v = u;
            // 从尚未使用过的顶点中选择一个距离最小的顶点
        }
 
        if(v == -1) break; //没有可跟新的了,结束
 
        used[v] = true;
 
        for(int u = 0; u < V; u++){
            d[u] = min(d[u], d[v] + cost[u][v]);  //因为加入了一个点V所以所有的d都要再更新一遍
            prev[u] = v;
        }
    }
} 
vector get_path(int t) //到顶点t的最短路
{
    vector path;
    for(; t != -1; t = prev[t])
        path.push_back(t);
    reverse(path.begin(), path.end());
    return path;
} 

二、可以处理负权值的$SPFA$

SPFA要谨慎使用!

int nex[M],ver[M],head[N],edge[M],tot;

void add(int u,int v,int val){
    ver[++tot] = v;
    edge[tot] = val;
    nex[tot] = head[u];
    head[u] = tot;
}

queueq;
bool vis[N];
int d[N];
int n,m;
void spfa(int s){
    //memset(d,0x3f,sizeof d);
    for(int i = 1; i<= n;++i)
        d[i] = 2147483647;
    memset(vis,0,sizeof vis);
    d[s] = 0;
    vis[s] = 1;
    q.push(s);
    while(q.size()){
        int x = q.front();
        q.pop();
        vis[x] = 0;
        //扫描所有出边
        for(int i = head[x];i;i = nex[i]){
            int y = ver[i];
            int z = edge[i];
            if(d[y] > d[x] + z){
                d[y] = d[x] + z;
                if(!vis[y])//不在堆里就入堆并标记一下
                    q.push(y),vis[y] = 1;
            }
        }
    }
}

int s;

int main()
{
    cin>>n>>m>>s;
    over(i,1,m){
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);
    }
    spfa(s);
    over(i,1,n){
        printf("%d ",d[i]);
    }
    return 0;
}

三、两种分层图最短路

分层次

分成$k+1$层图

const int INF = 0x3f3f3f3f;
const int N = 6e5+7;

int nex[N],ver[N],head[N],edge[N],tot;

int n,m,k,s,t;
int dist[N];
bool vis[N];

void init(){
    memset(head,-1,sizeof head);
    tot = 0;
    memset(dist,0x3f,sizeof dist);
    memset(vis,0,sizeof vis);

}

void add(int u,int v,int val){
    ver[++tot] = v;
    edge[tot] = val;
    nex[tot] = head[u];
    head[u] = tot;
}

void dijkstra(){
    priority_queue,greater >q;
    dist[1] = 0;
    //vis[s] = 1;dijkstra和spfa不一样!这里不用标记,不然就走不动啦
    q.push({dist[1],1});
    while(q.size()){
        PII now = q.top();//这种声明的变量后面不要用逗号运算符!!!
        q.pop();
        int u = now.second;
        if(vis[u])continue;
        vis[u] = 1;
        for(int i = head[u]; i != -1 ;i = nex[i]){
            int v = ver[i],val = edge[i];
            if(!vis[v] && dist[v] > dist[u] + val){
                dist[v] = dist[u] + val;
                q.push({dist[v],v});
            }
        }
    }
}

int main()
{
    init();
    scanf("%d%d%d",&n,&m,&k);
    over(i,1,m){
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        for(int j = 0;j <= k;++j){
            add(x + j * n,y + j * n,z);//这一层和这一层连起来
            add(y + j * n,x + j * n,z);
            if(j != k){//只要还有就这一层和下一层连起来,并且权值为0
                add(x + j * n,y + (j + 1) * n,0);
                add(y + j * n,x + (j + 1) * n,0);
            }
        }
    }
    dijkstra();
    int ans = INF;//dijkstra只是更新了一个dis数组,答案需要我们自己找
    over(i,0,k)
    ans = min(ans,dist[n + i * n]);
    printf("%d\n",ans);
    return 0;
}

分维度

这里是因为题目要求的答案是第k+1的权值最小,所以只求单条路径的权值即可,但是这样的更新并不能满足dijkstra的贪心性质,所以我们要选择SPFA算法。

/*POJ 3662 Telephone Lines*/
const int N = 3e3;
const int M = 5e4+7;//注意这里数据范围要用M

int d[N][N];
int n,m,k;
int nex[M],ver[M],head[N],edge[M],tot;

void add(int u,int v,int val){
    ver[++tot] = v;
    edge[tot] = val;
    nex[tot] = head[u];
    head[u] = tot;
}

queueq;
bool vis[N];

void spfa(int s){
    memset(d,0x3f,sizeof d);
    memset(vis,0,sizeof vis);
    d[s][0] = 0;
    vis[s] = 0;
    q.push(s);
    while(q.size()){
        int x = q.front();
        q.pop();
        vis[x] = 0;
        for(int i = head[x];i;i = nex[i]){
            int y = ver[i],z = edge[i];
            int w = max(d[x][0],z);//不同于往常的是每一条路径的 最大边 是这条路径的花费
            if(d[y][0]>w){//先算普通的最短路
                d[y][0] = w;
                if(!vis[y])
                    q.push(y),vis[y] = 1;
            }
            //反正就是动规,暴力所有状态转移取最小,状态转移的方式就是可以免费。
            //每次对每种路径都取一次最优
            for(int p = 1;p <= k;++p){//然后再看免费的,不一定是几条边最优//不管怎么说肯定是从前往后地推出的
                int w = min(d[x][p-1],max(d[x][p],z));//使最大边最小//主要这里的答案都是一条边的权值
                //当前的状态是从x转移到y,其中这条边免费或者不免费
                if(d[y][p] > w){
                    d[y][p] = w;
                    if(!vis[y])
                        q.push(y),vis[y] = 1;
                }
            }
        }
    }
}

int main(){
    cin>>n>>m>>k;
    over(i,1,m){
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);
        add(y,x,z);
    }
    spfa(1);
    int ans = 1e9+7;
    over(i,0,k){//注意是从0开始,因为也可以不选
        //就真答案并不在掌握之中
        //其实就是分层图,应该是 k + 1 层
        ans = min(ans,d[n][i]);
    }
    if(ans == 1e9+7)
        puts("-1");
    else printf("%d\n",ans);
    return 0;
}

四、Floyd算法,传递闭包

Floyd算法
设$D[k,i,j]$表示“经过若干个编号不超过k的结点”从i到j的最短路长度。
Floyd算法的实质是动态规划。
k是阶段,所以必须放在最外层循环。
i和j是附加状态,应该放置于内层循环。

int dis[1000][1000],n,m;
int main()
{
    cin>>n>>m;
    memset(dis,0x3f,sizeof dis);
    for(int i = 1;i <= n;++i)
        dis[i][i] = 0;
    for(int i = 1;i <= m;++i){
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        dis[x][y] = min(dis[x][y],z);//可能会重复
    }
    for(int k = 1;k <= n;++k)
        for(int i = 1;i <= n;++i)
            for(int j = 1;j <= n;++j)
                dis[i][j] = min(dis[i][j],dis[i][k] + dis[k][j]);
    for(int i = 1;i <= n;++i)
        for(int j = 1;j <= n;++j)
            printf("%d ",dis[i][j]);
    return 0;
}

使用Floyd实现的传递闭包

int dis[1000][1000],n,m;

int main()
{
    cin>>n>>m;
    for(int i = 1;i <= n;++i)
        dis[i][i] = 1;
    for(int i = 1;i <= m;++i){
        int x,y;
        scanf("%d%d",&x,&y);
        dis[x][y] = dis[y][x] = 1;
    }
    for(int k = 1;k <= n;++k)
        for(int i = 1;i <= n;++i)
            for(int j = 1;j <= n;++j)
                dis[i][j] |= dis[i][k] & dis[k][j];
                //把整个关系梳理出来
}

你可能感兴趣的:(最短路合集(分层图最短路、传递闭包、路径还原、k短路...))