最短路专题一(dijkstra)

做了一下《训练指南》上的的最短路的例题,贴一下代码,就当是做个备忘录了。如果要看题解,书上写的挺详细的~~
下面的有些题意和分析是网上搜的,自己太懒了
还有有的题的代码是lrj的,他的代码有注释,读起来容易~~
题目:https://vjudge.net/contest/135918#overview

题目:UVa 11374
题意:

在Iokh市中,机场快线是市民从市内去机场的首选交通工具。机场快线分为经济线和商业线两种,线路,速度和价钱都不同。你有一张商业线车票,可以做一站商业线,而其他时候只能乘坐经济线。假设换乘时间忽略不计,你的任务是找一条去机场最快的路线。。

分析:

枚举商业线T(a,b),则总时间为d1(a)+T(a,b)+d2(b);d1和d2用两次dijkstra来计算,以S为起点的dijkstra和以E为起点的dijkstra;

代码:

#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
#include <string>
#include <map>
#include <cmath>
#include <queue>
#include <set>
#include <stack>
using namespace std;
const int INF=1e8;
const int N = 500 + 10;
vector<int> path;
int d1[N],d2[N],f1[N],f2[N],w[N][N],vis[N];
int T,n,s,e,M,a,b,c;
void dij(int d[],int fa[],int s) {
    memset(vis,0,sizeof(vis));
    for(int i=1; i<=n; i++) {
        int x,m=INF;
        for(int j=1; j<=n; j++)if(!vis[j]&&d[j]<m)m=d[x=j];
        vis[x]=1;
        for(int j=1; j<=n; j++)if(!vis[j]&&d[j]>d[x]+w[x][j]) {
                d[j]=d[x]+w[x][j];
                fa[j]=x;
            }
    }
}
void print_ans1(int u) {
    if(f1[u])print_ans1(f1[u]);
    path.push_back(u);
}
void print_ans2(int u) {
    path.push_back(u);
    if(f2[u])print_ans2(f2[u]);
}
int main() {

    //freopen("f.txt","r",stdin);
    int cas=0;
    while(~scanf("%d%d%d",&n,&s,&e)) {
        if(cas)printf("\n");
        ++cas;
        memset(f1,0,sizeof(f1));
        memset(f2,0,sizeof(f2));
        path.clear();
        scanf("%d",&M);
        for(int i=1; i<=n; i++)for(int j=1; j<=n; j++)w[i][j]=INF;
        for(int i=0; i<M; i++) {
            scanf("%d%d%d",&a,&b,&c);
            if(c<w[a][b])
                w[a][b]=w[b][a]=c;
        }
        for(int i=1; i<=n; i++)d1[i]=d2[i]=INF;
        d1[s]=0;
        dij(d1,f1,s);
        d2[e]=0;
        dij(d2,f2,e);
        int ans=d1[e],p1=-1,p2=-1;
        int K;
        scanf("%d",&K);
        while(K--) {
            scanf("%d%d%d",&a,&b,&c);
            if(d1[a]+c+d2[b]<ans) {
                ans=d1[a]+c+d2[b];
                p1=a;
                p2=b;
            }
            if(d1[b]+c+d2[a]<ans) {
                ans=d1[b]+c+d2[a];
                p1=b;
                p2=a;
            }
        }
        if(p1==-1) {
            print_ans1(e);
        } else {
            print_ans1(p1);
            print_ans2(p2);
        }
        for(int i=0; i<path.size()-1; i++)printf("%d ",path[i]);
        printf("%d\n",e);
        if(p1==-1)puts("Ticket Not Used");
        else printf("%d\n",p1);
        printf("%d\n",ans);
    }
    return 0;
}

题目:UVa 10917
题意:

gbn最近打算穿过一个森林,但是他比较傲娇,于是他决定只走一些特殊的道路,他打算只沿着满足如下条件的(A,B)道路走:存在一条从B出发回家的路,比所有从A出发回家的路径都短。你的任务是计算一共有多少条不同的回家路径。其中起点的编号为1,终点的编号为2.

分析:

首先从终点Dijkstra一次,求出每个点u回家的最短路长度,那么相当于创建了一个新图,当d[B]< d[A]时有一条A指向B的有向边,则题目的目标就是求出起点到终点的路径条数。因为这个图是DAG,可以用动态规划求解。

代码:

using namespace std;
const int INF=0x3f3f3f3f;
const int N = 1000 + 10;
int w[N][N],f[N],d[N],vis[N],n,m,a,b,c;
void dij() {
    memset(vis,0,sizeof(vis));
    for(int i=1; i<=n; i++) {
        int x,m=INF;
        for(int j=1; j<=n; j++)if(!vis[j]&&d[j]<m)m=d[x=j];
        vis[x]=1;
        for(int j=1; j<=n; j++)if(!vis[j]&&d[j]>d[x]+w[x][j]) {
                d[j]=d[x]+w[x][j];
            }
    }
}


int dp(int u) {
    if(u==2)return 1;
    int& ans=f[u];
    if(ans>=0)return ans;
    ans=0;
    for(int i=1; i<=n; i++)
        if(w[u][i]<INF&&d[i]<d[u])ans+=dp(i);
    return ans;
}
int main() {
    //freopen("f.txt","r",stdin);
    while(~scanf("%d%d",&n,&m)&&n) {
        memset(w,0x3f,sizeof(w));
        memset(d,0x3f,sizeof(d));
        for(int i=0; i<m; i++) {
            scanf("%d%d%d",&a,&b,&c);
            if(c<w[a][b])w[a][b]=w[b][a]=c;
        }
        d[2]=0;
        dij();
        memset(f,-1,sizeof(f));
        printf("%d\n",dp(1));
    }

}

题目:LA 4080
题意:

①先求任意两点间的最短路径累加和,其中不连通的边权为L ②删除任意一条边,求全局最短路径和的最大值。

分析:

第一个问题很好解决,floyd 和 n 次 dij 或者 spfa 都可以。关键是删掉一条边那里。如果枚举删掉的边,那复杂度就是 m*n^3 ,换 dij 和 spfa 应该也差不多,都过不了。此时就要想到,不是无论删除哪条边,所有东西都要重新算的,只有那些在最短路树上的边被删除时,才需要重新算。所以就是先 dij 处理出 n 个最短路树和每个为源点时的最短路和。然后就是上面的步骤了,先判断要不要重新算就好了。这里有个陷阱的,就是有重边。如果你要删除了两个点之间的一条边,那么肯定是删的最短的那一条,因为要使 c 最大么,删掉后不是就没有边了,应该是第二短的边顶上,所以这里还要稍微处理一下。

代码:

// LA4080/UVa1416 Warfare And Logistics
// Rujia Liu
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
#include<queue>
using namespace std;

const int INF = 1000000000;
const int maxn = 100 + 10;

struct Edge {
    int from, to, dist;
};

struct HeapNode {
    int d, u;
    bool operator < (const HeapNode& rhs) const {
        return d > rhs.d;
    }
};

struct Dijkstra {
    int n, m;
    vector<Edge> edges;
    vector<int> G[maxn];
    bool done[maxn];    // 是否已永久标号
    int d[maxn];        // s到各个点的距离
    int p[maxn];        // 最短路中的上一条弧

    void init(int n) {
        this->n = n;
        for(int i = 0; i < n; i++) G[i].clear();
        edges.clear();
    }

    void AddEdge(int from, int to, int dist) {
        edges.push_back((Edge) {
            from, to, dist
        });
        m = edges.size();
        G[from].push_back(m-1);
    }

    void dijkstra(int s) {
        priority_queue<HeapNode> Q;
        for(int i = 0; i < n; i++) d[i] = INF;
        d[s] = 0;
        memset(done, 0, sizeof(done));
        Q.push((HeapNode) {
            0, s
        });
        while(!Q.empty()) {
            HeapNode x = Q.top();
            Q.pop();
            int u = x.u;
            if(done[u]) continue;
            done[u] = true;
            for(int i = 0; i < G[u].size(); i++) {
                Edge& e = edges[G[u][i]];
                if(e.dist > 0 && d[e.to] > d[u] + e.dist) { // 此处和模板不同,忽略了dist=-1的边。此为删除标记。
                        //根据题意和dijkstra算法的前提,正常的边dist>0
                    d[e.to] = d[u] + e.dist;
                    p[e.to] = G[u][i];
                    Q.push((HeapNode) {
                        d[e.to], e.to
                    });
                }
            }
        }
    }
};

 题目相关
Dijkstra solver;
int n, m, L;
vector<int> gr[maxn][maxn]; // 两点之间的原始边权
int used[maxn][maxn][maxn]; // used[src][a][b]表示源点为src的最短路树是否包含边a->b
int idx[maxn][maxn]; // idx[u][v]为边u->v在Dijkstra求解器中的编号
int sum_single[maxn]; // sum_single[src]表示源点为src的最短路树的所有d之和

int compute_c() {
    int ans = 0;
    memset(used, 0, sizeof(used));
    for(int src = 0; src < n; src++) {
        solver.dijkstra(src);
        sum_single[src] = 0;
        for(int i = 0; i < n; i++) {
            if(i != src) {
                int fa = solver.edges[solver.p[i]].from;
                used[src][fa][i] = used[src][i][fa] = 1;
            }
            sum_single[src] += (solver.d[i] == INF ? L : solver.d[i]);
        }
        ans += sum_single[src];
    }
    return ans;
}

int compute_newc(int a, int b) {
    int ans = 0;
    for(int src = 0; src < n; src++)
        if(!used[src][a][b]) ans += sum_single[src];
        else {
            solver.dijkstra(src);
            for(int i = 0; i < n; i++)
                ans += (solver.d[i] == INF ? L : solver.d[i]);
        }
    return ans;
}

int main() {
    while(scanf("%d%d%d", &n, &m, &L) == 3) {
        solver.init(n);
        for(int i = 0; i < n; i++)
            for(int j = 0; j < n; j++) gr[i][j].clear();

        for(int i = 0; i < m; i++) {
            int a, b, s;
            scanf("%d%d%d", &a, &b, &s);
            a--;
            b--;
            gr[a][b].push_back(s);
            gr[b][a].push_back(s);
        }

        // 构造网络
        for(int i = 0; i < n; i++)
            for(int j = i+1; j < n; j++) if(!gr[i][j].empty()) {
                    sort(gr[i][j].begin(), gr[i][j].end());
                    solver.AddEdge(i, j, gr[i][j][0]);
                    idx[i][j] = solver.m - 1;
                    solver.AddEdge(j, i, gr[i][j][0]);
                    idx[j][i] = solver.m - 1;
                }

        int c = compute_c();
        int c2 = -1;
        for(int i = 0; i < n; i++)
            for(int j = i+1; j < n; j++) if(!gr[i][j].empty()) {
                    int& e1 = solver.edges[idx[i][j]].dist;
                    int& e2 = solver.edges[idx[j][i]].dist;
                    if(gr[i][j].size() == 1) e1 = e2 = -1;
                    else e1 = e2 = gr[i][j][1]; // 大二短边
                    c2 = max(c2, compute_newc(i, j));
                    e1 = e2 = gr[i][j][0]; // 恢复
                }

        printf("%d %d\n", c, c2);
    }
    return 0;
}

题目:UVa 10537
题意:

有两种节点,一种是大写字母,一种是小写字母。首先输入m条边,当经过小写字母时需要付一单位的过路费,当经过大写字母时,要付当前财务的1/20做过路费。问在起点最少需要带多少物品使到达终点时还有k个物品。当有多条符合条件的路径时输出字典序最小的一个。

分析:

从终点逆着进行更新,用 d[ i ] 表示已经进入了 i 再从 i 到达终点需要准备的最小物资数。那么 d[ e ] 很明显就是输入的那个数。然后就是更新了,dij 和 spfa 都可以,就把所有的 d 都算出来了,然后从起点根据 d 递归打印路劲就行了。两个地方需要注意:(1)由于是d 是逆着算的,有一个地方,我纠结了很久,就是城镇那里,你知道了进入城镇后剩余的量,那怎么求进入之前的量,想了很久,公式退步出来,后来看了人家博客才发现,原来很简单,不用推公式,想想如果 x/20 不是向上取整, x - x/20 = y ,那么 x = 20*y/19,那么现在是向上取整,那么这个 x 肯定是 >= 20*y/19 。然后就是 ++ 暴力往上算凑即可,最多次数是多少,我不知道怎么算(回去后有时间再算算。。 = =),我试验了一下,这个次数很少的,对复杂度没有影响。(2)输出路径的时候,根据 d 进行输出,是递归满足 d[ u ] - w = d[ v ] 的最小的 v ,所以只要从小到大枚举 v 即可,这个w 就很好算了,这是正序的算。

代码:

// UVa10537 Toll! Revisited
// Rujia Liu
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int maxn = 52 + 10;
const long long INF = 1LL << 60;
typedef long long LL;

int n, G[maxn][maxn], src, dest, p;
int mark[maxn]; // 标记
LL d[maxn];     // d[i]表示从点i出发(已经交过点i的税了)时至少要带多少东西,到dest时还能剩p个东西

int read_node() {
    char ch[9];
    scanf("%s", ch);
    if(ch[0] >= 'A' && ch[0] <= 'Z') return ch[0] - 'A';
    else return ch[0] - 'a' + 26;
}

char format_node(int u) {
    return u < 26 ? 'A' + u : 'a' + (u - 26);
}

// 拿着item个东西去结点next,还剩多少个东西
LL forward(LL item, int next) {
    if(next < 26) return item - (item + 19) / 20;
    return item - 1;
}

// 至少要拿着多少个东西到达结点u,交税以后还能剩d[u]个东西
// 为了使代码容易理解,这里使用一种不太数学的解法
LL back(int u) {
    if(u >= 26) return d[u]+1;
    LL X = d[u] * 20 / 19; // 初始值
    while(forward(X, u) < d[u]) X++; // 调整
    return X;
}

void solve() {
    n = 52; // 总是有52个结点
    memset(mark, 0, sizeof(mark));
    d[dest] = p;
    mark[dest] = 1;
    for(int i = 0; i < n; i++) if(i != dest) {
            d[i] = INF;
            if(G[i][dest]) d[i] = back(dest);
        }

    // Dijkstra主过程,逆推
    while(!mark[src]) {
        // 找最小的d
        int minu = -1;
        for(int i = 0; i < n; i++) if(!mark[i]) {
                if(minu < 0 || d[i] < d[minu]) minu = i;
            }
        mark[minu] = 1;
        // 更新其他结点的d
        for(int i = 0; i < n; i++) if(!mark[i]) {
                if(G[i][minu]) d[i] = min(d[i], back(minu));
            }
    }

    // 输出
    printf("%lld\n", d[src]);
    printf("%c", format_node(src));
    int u = src;
    LL item = d[src];
    while(u != dest) {
        int next;
        for(next = 0; next < n; next++) // 找到第一个可以走的结点
            if(G[u][next] && forward(item, next) >= d[next]) break;
        item = d[next];
        printf("-%c", format_node(next));
        u = next;
    }
    printf("\n");
}

int main() {
    int kase = 0;
    while(scanf("%d", &n) == 1 && n >= 0) {
        memset(G, 0, sizeof(G));
        for(int i = 0; i < n; i++) {
            int u = read_node();
            int v = read_node();
            if(u != v) G[u][v] = G[v][u] = 1;
        }
        scanf("%d", &p);
        src = read_node();
        dest = read_node();
        printf("Case %d:\n", ++kase);
        solve();
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值