图论--单源最短路

 BELLMAN-FORD


/**
 * Bellman-ford 可处理带有负权边的单源最短路径
 * 算法思想:
 *   自底向上的方式计算最短路径
 *   第i次遍历得到距源点最多i条边的最短路径
 *   由于对于一个|V|个顶点的图的最短路径最多只可能经过|V-1|条边
 *   所以循环次数最多为|V-1|次
*/
void Bellman_ford() {
    bool flag = false;  // 优化,若当前遍历没有松弛,说明已经找完最短路了
    for(int t=0; t<n-1; ++t) { // loop times
        for(int i=1; i<=m; ++i) { // iterate edges
            int v = e[i].v, u = e[i].u;
            if(dis[v] > dis[u] + e[i].w) {
                dis[v] = dis[u] + e[i].w, flag = true;
            }if(!flag) break; // 没有可以松弛的了,结束算法
        }
    }
}
// 利用队列优化 --> spfa

void spfa() {
    memset(dis, Inf, sizeof(dis));
    memest(inq, false, sizeof(inq));
    memset(cnt, 0, sizeof(cnt));
    queue<Node> q;    while(q.size()) q.pop();
    q.push(Node(s, 0));
    dis[s] = 0, inq[s] = true;

    while(q.size()) {
        Node now = q.fornt(); q.pop();
        inq[now.u] = false;
        cnt[now.u] += 1;
        if(cnt[now.u] >= n) { /* 存在负环 */ break; }
        for(int i=hd[now.u]; i; i=e[i].nxt) {
            int v = e[i].to;
            if(dis[v] > dis[now.u] + e[i].w) {
                dis[v] = dis[now.u] + e[i].w;
                if(!inq[v]) {
                    cnt[v] += 1;
                    q.push(Node(v, dis[v]));
                    inq[v] = true;
                }
            }
        }
    }return;
}

bellman --> spfa    

 

  写了一天的代码有些小累,总结总结,顺便休息休息。

  为了应对CCF考试的第四题,本人开始干图论,由于基础一般,目前做的都是模板题,大佬可以绕行聊~

Floyd

  • 稳定的O(n^3)复杂度,一般题目的节点数>300就不要再考虑了,小于300还是可以水水的,毕竟算法简单。
    • 注意最外层循环为中间节点,是通过遍历中间节点不断更新两点间的距离。

来一道题luogu社交网络:这里还涉及到计算最短路径数量的一个方法。用一个times[u][v]数组存储u和v之间最短路的数量,初始化都是1(题目说明任意两点都相连时)。每次能更新最短路u-v时,(中间节点为k),times[u][v]也要相应更新为u-k间最短路数 * k-v间最短路数,这个就是分步联系事件的乘法原理。当u-v间距离恰好==u-k距离与k-v距离之和时,不同情况的事件加法原理增加u-v之间的最短路数量。

这道题还涉及精度的问题,用float会丢失精度,必须用double。这个原因我还没弄懂,希望能有人解答,我个人认为是因为是long long两个数相除的原因。一般严格小于6位的精度,可以直接用float;小于16位的精度可以使用double。题目:社交网络

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
#define Inf 0x3f3f3f3f
#define ll long long
const int maxv = 100 + 4, maxe = 4500 + 4;
int table[maxv][maxv];
ll edge[maxv][maxv];
double Imp[maxv];
int n, m, u, v, w;
//注意floyd是所有点间的遍历故双向性已经考虑在内
//每次单向处理即可
void floyd(){
    for(int k=1; k<=n; ++k){
        for(int i=1; i<=n; ++i){
            for(int j=1; j<=n; ++j){
                if(table[i][j] > table[i][k] + table[k][j]){//重新更新
                    table[i][j] = table[i][k] + table[k][j];
                    edge[i][j] = edge[i][k] * edge[k][j];
                }
                else if(table[i][j] == table[i][k] + table[k][j]){
                    //不需要更新,因为中转节点是变化的,所以必然是新的最短路,累加即可
                    //不用担心有重复的计算。乘法原理
                    edge[i][j] += edge[i][k] * edge[k][j];
                }
            }
        }
    }
    return;
}
void solve(){
    for(int k=1; k<=n; ++k){
        for(int i=1; i<=n; ++i){
            for(int j=1; j<=n; ++j){
                if(i==j || j==k || i==j) continue;
                if(table[i][j] == table[i][k] + table[k][j])
                    Imp[k] += ((double)edge[i][k] * (double)edge[k][j] / (double)edge[i][j]);
            }
        }
        printf("%.3f\n", Imp[k]);
    }
    return;
}
inline int read(){
    char ch = getchar();
    int ans = 0;
    while(ch < '0' || ch >'9') ch = getchar();
    while(ch >= '0' && ch <= '9'){
        ans = (ans << 3) + (ans << 1) + ch - '0';
        ch = getchar();
    }return ans;
}
void inline ini()
{
    memset(table, Inf, sizeof(table));
    n = read(), m = read();
    while(m--){
        u = read(), v = read(), w = read();
        table[u][v] = table[v][u] = w;
        edge[u][v] = edge[v][u] = 1;
    }
    return;
}
int main()
{
    ini();
    floyd();
    solve();
    return 0;
}
社交网络

还有一道,这道题稍微要思考一下,要事先对中间节点排序再枚举。newid[]数组好好意会意会,有点像数学中的坐标转换~将所有变量在我理想的“坐标下实现”, 最后的输出也要是在这个“理想坐标下”操作。感觉题目有些表述问题,一开始一直以为是加上所有节点后的最大值。题目:牛收费

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define Inf 0x3f3f3f3f
#define ll long long
const int maxn = 250 + 4;
struct Node{
    int w, id;
}node[maxn];
ll table[maxn][maxn], dis[maxn][maxn];
int n, m, k, u, v, w, st, en;
int newid[maxn];
inline int read(){
    char ch = getchar();
    int ans = 0;
    while(ch < '0' || ch > '9') ch = getchar();
    while(ch >= '0' && ch <= '9'){
        ans = (ans << 3) + (ans << 1) + ch - '0';
        ch =getchar();
    }return ans;
}
inline bool comp(const Node& a, const Node& b){
    return a.w < b.w;
}

inline void floyd(){
    for(int k=1; k<=n; ++k){
        for(int i=1; i<=n; ++i){
            for(int j=1; j<=n; ++j){
                if(i == j || i == k || j == k) continue;
                table[i][j] = min(table[i][j], table[i][k] + table[k][j]);
                dis[i][j] = min(dis[i][j], table[i][j] + max(node[k].w, max(node[i].w, node[j].w)));
            }
        }
    }
    return;
}
//涉及到最短路和最短路上的最大节点,用两个数组分别存储
//由于floyd中间节点由前往后遍历,所以我们把权值越大的点越往后遍历
//使得最终遍历到k==n时,保证如果选取中间节点选取的节点是所有中间节点中权值最大的
int main()
{
    memset(table, Inf, sizeof(table));
    memset(dis, Inf, sizeof(dis));
    n = read(), m = read(), k = read();
    for(int i=1; i<=n; ++i) 
        {node[i].w = read(), node[i].id = i, table[i][i] = 0;}
    sort(node+1, node+n+1, comp); //节点按权值升序
    for(int i=1; i<=n; ++i)
        newid[node[i].id] = i; 
        //相当于把我想要优先遍历的序号排在前边
        //这样在floyd中的遍历就更加方便注意之后的存图位置也要用newid数组

    for(int i=1; i<=m; ++i){
        u = read(), v = read(), w = read();
        table[newid[u]][newid[v]] = table[newid[v]][newid[u]] = min(table[newid[u]][newid[v]], (ll)w);
        //为方便之后的floyd访问,用newid【】数组来获取新的顺序编号
        //转换之后,权值较小的节点会被优先遍历到
        //注意可能会有重边,所以要有一个min()判断一下
    }

    floyd();
    while(k--){
        st = read(), en = read();
        printf("%lld\n", dis[newid[st]][newid[en]]);
    }
    return 0;
}
牛收费--关键点newid

 

 

 

Dijkstra

  • 推荐使用堆优化后的版本,即结合priority_queue。复杂度比Spfa稳定,一般没有负权的图推荐用Dijkstra。Dijkstra(堆优化版本)复杂度O(VlogE), Spfa复杂度最坏情况会到达O(VE)容易被题目数据卡掉。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
#define ll long long 
#define Inf    0x3f3f3f3f
const int maxe = 50000 + 5, maxv = 50000 + 5;
struct Edge{
    int to, w;    int next;
    Edge(int a=0, int b=0, int c=0):to(a), w(b), next(c){}
}e[maxe * 2];
int head[maxv], cnte, pw[maxv];
struct Node{
    int u; ll dis;
    Node(int a=0, ll b=0):u(a), dis(b){}
    bool operator < (const Node& b) const{
        return this->dis > b.dis;
    }
};
bool vis[maxv];
ll ans, dis[maxv];
int nv, ne, a, b, w, st, t;

inline void ini()
{
    memset(head, 0, sizeof(head));
    cnte = ans = 0;
}
inline void add_edge(const int& from, const int& to, const int& w){
    ++cnte;
    e[cnte].to = to, e[cnte].w = w;
    e[cnte].next = head[from];
    head[from] = cnte;
    return;
}
//与Prim的区别,入队更新的是到源点的距离dis[cur.u] + e[i].w
//Prim入队更新的的是到新生成的树的距离e[i].w
//已经确定过的点就不要再入队了,减少stl中不必要的排序
void dijkstra(){
    memset(vis, false, sizeof(vis));
    for(int i=1; i<=nv; ++i) dis[i] = Inf;    //注意这里的Inf不是0x3f3f3f3f只能用循环赋值
    dis[st] = 0;
    priority_queue<Node> pq;    while(pq.size())    pq.pop();
    pq.push(Node(st, 0));
    while(pq.size()){    
        Node cur = pq.top();
        pq.pop();
        if(vis[cur.u]) continue;
        vis[cur.u] = true;
        for(int i = head[cur.u]; i; i = e[i].next)
        {
            if(vis[e[i].to]) continue;
            if(dis[e[i].to] > dis[cur.u] + e[i].w){
                dis[e[i].to] = dis[cur.u] + e[i].w;
                pq.push(Node(e[i].to, dis[e[i].to]));
                //无需再判断是否已经确定,直接入队即可,因为每次弹出的值都是经过筛选的最小值
                //且访问过的不会再操作
            }
            else continue;
        }
    }
    return;
}
inline int read(){
    char ch = getchar();
    int ans = 0;
    while(ch < '0' || ch > '9')    ch = getchar();
    while(ch >= '0' && ch <= '9'){
        ans = (ans << 3) + (ans << 1) + ch - '0';
        ch = getchar();
    }return ans;
}
int main()
{
    t = read();
    while(t--){
        ini();
        nv = read(), ne = read(), st = 1;
        for(int i=1; i<=nv; ++i) 
            pw[i] = read();
        for(int i=0; i<ne; ++i){
            a = read(), b = read(), w = read();
            add_edge(a, b, w);
            add_edge(b, a, w);
        }
        dijkstra();
        bool have_ans = true;
        for(int i=1; i<=nv; ++i){
            if(dis[i] == Inf){
                have_ans = false;
                break;
            }
            ans += dis[i] * pw[i];
        }
        if(have_ans) printf("%lld\n", ans);
        else puts("No Answer");
    }
    
    return 0;
}
堆优化版dijkstra

 

这里仍然来一道题目。这题中比较巧秒的是建立正反图,将问题都转化为单源最短路,然后就可以用dijkstra了。题目:邮递员

邮递员

Spfa

  • 遇到处理带负权的图再考虑,其他情况请使用Dijkstra。

具体的题目不记得了,贴个板子:

#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
#define ll long long
#define Inf 0x3f3f3f3f
const int maxn = 50000 + 5;
struct Edge{
    int to, w;
    int next;
    Edge(int a=0, int b=0, int c=0):to(a), w(b), next(c){}
}e[maxn * 2];
int st, cnte, head[maxn], pw[maxn];
bool in_que[maxn];
ll ans, dis[maxn];
int nv, ne, u, v, w, t;

inline void ini()
{
    memset(in_que, false, sizeof(in_que));
    memset(dis, Inf, sizeof(dis));
    memset(head, 0, sizeof(head));
    cnte = ans = 0;
    return;
}
inline void add_edge(const int& from, const int& to, const int& w)
{
    ++cnte;
    e[cnte].to = to, e[cnte].w = w;
    e[cnte].next = head[from];
    head[from] = cnte;
    return;
}
//单源最短路
void spfa(const int s){
    queue<int> que;    while(que.size()) que.pop();
    que.push(s);
    in_que[s] = true;
    dis[s] = 0;

    while(que.size()){
        int cur = que.front();
        que.pop();     in_que[cur] = false;
        for(int i = head[cur]; i; i = e[i].next){
            if(dis[e[i].to] > dis[cur] + e[i].w){
                dis[e[i].to] = dis[cur] + e[i].w;
                if(!in_que[e[i].to]){
                    que.push(e[i].to);
                    in_que[e[i].to] = true;
                }
            }
        }
    }
    return;
}
Spfa
posted @ 2019-07-08 21:40  Bankarian  阅读(363)  评论(0编辑  收藏  举报