图论算法

图论算法


图的数据结构

图的存储结构往往跟使用的算法有关,主要有邻接矩阵,邻接表等方式

邻接矩阵

使用 VV 的二维数组来表示图,g[i][j] 表示顶点i和顶点j的关系。
缺点:

  • 一般内存限制在4G以下时, V<40000 . 如果V比较大,就很占用空间。
  • 不能保存重边

优点:
- 常数时间内可以查询两点之间的边
- 实现简单

邻接表

边上有属性的时候:

struct edge{int to, cost;}
vector<edge> G[MAX_V];

边上没有属性的时候:

vector<int> G[MAX_V];

顶点上有属性的时候:

struct vertex{
    vector<vertex*> edge;
    /*
    * 顶点的属性
    */
}

优点:

  • 节省空间
  • 可以访问一个点所有的出边

缺点:

  • 不能在常数时间查询两点之间的边
  • 有向图不能快速获取反向边

边表

在Bellman-Ford算法和Kruskal算法时,只需要存储边的表就可以了。

struct edge{int u, v, cost;};
edge es[MAX_E];

优点:

  • 比邻接表更简单
  • 可以快速地遍历边,对边排序等操作
  • 适合于Bellman-Ford算法(遍历边)和Kruskal算法(排序和遍历边)

缺点:

  • 不能快速获取一个顶点地所有邻边
  • 不适合于Dijkstra算法和网络流算法

结合邻接表和边表

在刘汝佳的《算法竞赛入门经典》一书中,结合邻接表和边表,可以使用一种特殊的存储方式具有二者的优点。先用边表存储边,然后使用邻接表的方式存储

struct edge{int from; int to; int dist;
    edge(int u, int v, int c):from(u), to(v), dist(c){}
};
vector<edge> edges;
vector<int> G[MAX_V];
// 如果是有向图,且需要加上反向边,可以按照下边的方式存储。
void AddEdge(int from, int to, int dist){
    edges.push_back(edge(from, to, dist));
    edges.push_back(edge(to, from, dist));
    int m = edges.size();
    G[from].push_back(m-2);
    G[to].push_back(m-1);
}

优点:

  • 具有邻接表的所有优点和功能
  • 可以快速查询有向图的反向边
  • 适合于网络流算法、Dijkstra算法
    缺点:
  • 不能对边排序
  • 不适合于Kruskal算法

结合邻接表和边表2(网络流算法)

struct edge{int v, cap, next;};
edge e[MAX_E];
int head[MAX_V];    //邻接表的头节点
// 添加双向边的代码
int add_edge(int u, int v, int c1, int c2) {
  int& i=num_of_edges;

  assert(c1>=0 && c2>=0 && c1+c2>=0); // check for possibility of overflow
  e[i].v = v;
  e[i].cap = c1;
  e[i].next = head[u];
  head[u] = i++;

  e[i].v = u;
  e[i].cap = c2;
  e[i].next = head[v];
  head[v] = i++;
  return i;
}
// e[i]和e[i^1]互为反向边

邻接表+反向边(网络流算法)

// rev是反向边在链表中的序号。
struct edge{int to, cap, rev;};
vector<edge> G[MAX_V];
void add_edge(int from, int to, int cap){
    G[from].push_back((edge){to, cap, G[to].size()});
    G[to].push_back((edge){from, 0, G[from].size()-1});
}

最短路算法

最短路算法常见的有Bellman-Ford算法,Floyd算法,Dijkstra算法。
Bellman-Ford算法和Dijkstra算法计算单源点最短路算法。Floyd算法计算任意两点最短路问题。
前两者都可以用来检测负环,而后者只适用于边权重都为正的情况。
Bellman-Ford算法复杂度是 O(VE) ,其中V是点数,E是边数
Floyd算法复杂度是 O(V3)
Dijkstra算法不使用堆或优先队列的话,复杂度是 O(V2) ,如果使用堆或者优先队列,复杂度降为 O(VlogE)

检测负环

使用poj上一道检测负环的题来分别展现Bellman-Ford算法和Dijkstra算法。
题目链接:poj 2240 Arbitrage
Bellman-Ford算法:

#include<iostream>
#include<vector>
#include<algorithm>
#include<queue>
#include<string>
#include<cstring>
#include<map>
using namespace std;

// Arbitrage
// 判断是否存在负环
// 首先采用Bellman-Ford算法
// 如果选定源点,可能依旧存在源点不可达的环。所以循环让每个点都作为源点做Bellman-Ford算法
// 如果一个点作为源点时检测到负环,就停止算法。
// Bellman-Ford运行N-1次可以求出源点到任意一点的最短路径。(因为N个点的最短路径的边数最多是N-1条)
// 如果运行N次而源点到某一点的最短路径依旧更新了,说明该最短路径上存在负环。

// 采用边表来表示图
struct Edge {
    int from;
    int to;
    double rate;
};
const int MAXV = 40;
const int MAXE = 1000;
Edge es[MAXE];  //边表
double d[MAXV]; // 最短距离

bool BellmanFord(int s, int n, int E) {
    // s是源点
    d[s] = 1.0; //初始化源点距离为1
    for (int i = 0; i < n; i++) {
        // 循环n-1次
        for (int j = 0; j < E; j++) {
            // 遍历边表,进行松弛操作。这个操作不需要从源点开始,所以我们遍历边表。
            int from = es[j].from;
            int to = es[j].to;
            double rate = es[j].rate;
            if (d[to] < d[from] * rate) {
                d[to] = d[from] * rate; //我们是希望得到最大距离(汇率),所以这里和最短路径数学表示略不同。
                if (i == n - 1) return true;
            }
        }
    }
    return false;
}
int main() {
    int n;
    int ct = 0;
    while (cin >> n && n != 0) {
        memset(d, 0, sizeof(d));
        memset(es, 0, sizeof(es));
        map<string, int> m;
        for (int i = 0; i < n; i++) {
            string s;
            cin >> s;
            m[s] = i;
        }
        int E;
        cin >> E;
        for (int i = 0; i < E; i++) {
            string from, to;
            double rate;
            cin >> from >> rate >> to;
            es[i].from = m[from];
            es[i].to = m[to];
            es[i].rate = rate;
        }
        bool isArbitrage = false;
        // 每个点作为源点
        for (int i = 0; i < n; i++) {
            if (BellmanFord(i, n, E)) {
                isArbitrage = true;
                break;
            }
        }
        cout << "Case " << ++ct << ": ";
        if (isArbitrage) {
            cout << "Yes" << endl;
        }
        else {
            cout << "No" << endl;
        }
    }
    return 0;
}

Floyd算法

#include<iostream>
#include<vector>
#include<algorithm>
#include<queue>
#include<string>
#include<cstring>
#include<map>
using namespace std;

// Floyd-Warshall算法
// d[k][i][j]为从i到j的只以(0...k)的节点为中间节点的最短路径长度
// 可以认为d[-1][i][j] = cost[i][j]
// d[k][i][j]=min(d[k-1][i][k]+d[k-1][k][j] + d[k-1][i][j])
const int MAXV = 40;
double g[MAXV][MAXV];

void floyd(int V) {
    for (int k = 0; k < V; k++) {
        for (int i = 0; i < V; i++) {
            for (int j = 0; j < V; j++) {
                g[i][j] = max(g[i][j], g[i][k] * g[k][j]);
            }
        }
    }
}
int main() {
    int V, E;
    int cnt = 0;
    while (cin >> V && V != 0) {
        map<string, int> m;
        for (int i = 0; i < V; i++) {
            string s;
            cin >> s;
            m[s] = i;
        }
        memset(g, 0, sizeof(g));
        for (int i = 0; i < V; i++) {
            g[i][i] = 1.0;
        }
        cin >> E;
        for (int i = 0; i < E; i++) {
            string s1, s2;
            double rate;
            cin >> s1 >> rate >> s2;
            g[m[s1]][m[s2]] = rate;
        }
        floyd(V);
        bool flag = false;
        cout << "Case " << ++cnt << ": ";
        for (int i = 0; i < V; i++) {
            if (g[i][i] > 1.0) {
                cout << "Yes" << endl;
                flag = true;
                break;
            }
        }
        if (!flag) {
            cout << "No" << endl;
        }
    }
    return 0;
}

Dijkstra算法

使用邻接表,优先队列。
这里没有定义结构体作为优先队列的节点,而是使用了pair的数据结构。使用typedef<int,int> P。同时使用greater<P>作为优先队列比较大小的方式,greater函数是fucntional库的函数,可以让优先队列从小到大排序。pair规定了先按照第一个数排序,再按照第二个数排序。

#include<iostream>
#include<vector>
#include<algorithm>
#include<queue>
#include<string>
#include<cstring>
#include<map>
#include<functional>
using namespace std;

// Dijkstra算法

struct edge { int to, cost; };
typedef pair<int, int> P; // first是最短距离,second是顶点编号

const int MAX_V = 1000;
int V;
vector<edge> G[MAX_V];
int d[MAX_V];

void dijkstra(int s) {
    priority_queue<P, vector<P>, greater<P> > que;
    d[s] = 0;
    que.push(P(0,s));
    while (!que.empty()) {
        P p = que.top();
        que.pop();
        int v = p.second;
        if (d[v] < p.first) continue;   // 如果已经处理过这个节点,就没必要再处理。
        for (int i = 0; i < G[v].size(); i++) {
            edge e = G[v][i];
            if (d[e.to] > d[v] + e.cost) {
                d[e.to] = d[v] + e.cost;
                que.push(P(d[e.to], e.to));
            }
        }
    }
}

最小生成树

Prim算法和Kruskal算法是两种求最小生成树的算法。
使用堆和优先队列的Prim算法复杂度是 O(ElogV) ,否则是 O(V2)
Kruskal算法的复杂度是 O(ElogV)
Prim算法和Dijkstra算法非常相似,都是从某个顶点出发,不断添加边的算法。

Prim算法

// Prim算法
// 集合X最初只有一个点,然后逐渐添加其他点到集合X

const int MAX_V = 1000;
int cost[MAX_V][MAX_V]; // 图的邻接矩阵
int mincost[MAX_V]; // 从集合X出发到每个顶点的最小权值。
bool used[MAX_V]; // 顶点i是否在集合X中
int V;

int prim() {
    for (int i = 0; i < V; i++) {
        mincost[i] = INT_MAX;
        used[i] = false;
    }
    mincost[0] = 0;
    int res = 0;
    while (true) {
        int v = -1; // 将要添加到X中的点
        // 寻找mincost最小且不在X中的点
        for (int u = 0; u < V; u++) {
            if (!used[u] && (mincost[u] < mincost[v])) {
                v = u;
            }
        }
        if (v == -1) break; // 都在X中了
        used[v] = true;
        res += mincost[v];   // 把顶点v加入X中,并把边权更新到最小生成树的结果里。
        for (int u = 0; u < V; u++) {
            mincost[u] = min(mincost[u], cost[v][u]);   // 和Dijkstra不一样的地方
        }
    }
    return res;
}

Kruskal算法

题目链接:poj 3723
这道题因为可能最后是森林,所以必须用Kruskal算法。

// ConsoleApplication1.cpp: 定义控制台应用程序的入口点。
//

#include "stdafx.h"

#include<iostream>
#include<vector>
#include<algorithm>
#include<queue>
#include<string>
#include<cstring>
#include<map>
#include<functional>
using namespace std;
#pragma warning(disable: 4996)
// poj 3723
// boy和girl如果有关系d,就可以招募另一个花费10000-d的费用
// 最小生成树,可能生成是一个森林,所以必须kruskal
// 构建一棵图的树,有关系的人之间边权为-d
// 邻接表

struct edge {
    int from, to, cost;
    /*edge(int u, int v, int d) :from(u), to(v), cost(d){}*/
    bool operator < (const edge& e2) const{
        return cost < e2.cost;
    }
};
const int MAXN = 10000 + 10;
const int MAXM = 10000 + 10;
const int MAXR = 50000 + 10;
edge es[MAXR];  // 边表
int N, M, R;
int fa[MAXN + MAXM];    //并查集
void init_union_found(int n) {
    for (int i = 0; i < n; i++) {
        fa[i] = i;
    }
}
int find(int x) {
    if (fa[x] == x) return x;
    if (fa[x] != x) {
        fa[x] = find(fa[x]);
    }
    return fa[x];
}
int kruskal() {
    sort(es, es+R);
    init_union_found(N + M);
    int res = 0;
    for (int i = 0; i < R; i++) {
        edge e = es[i];
        int f1 = find(e.from);
        int f2 = find(e.to);
        if (f1 != f2) {
            res += e.cost;
            fa[f2] = f1;
        }
    }
    return res;
}
int main() {
    int T;
    //ios::sync_with_stdio(false);
    scanf("%d", &T);
    //cin >> T;
    while (T--) {
        //cin >> N >> M >> R;
        scanf("%d%d%d", &N, &M, &R);
        memset(es, 0, sizeof(es));
        // 女孩从0到N-1,男孩从N到N+M-1
        for (int i = 0; i < R; i++) {
            int x, y, r;
            //cin >> x >> y >> r;
            scanf("%d%d%d", &x, &y, &r);
            // 无向图
            es[i].from = x;
            es[i].to = N + y;
            es[i].cost = -r;
        }
        int res = kruskal();    // 构建树
        res = 10000 * (N + M) + res;
        cout << res << endl;
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值