图论算法
图的数据结构
图的存储结构往往跟使用的算法有关,主要有邻接矩阵,邻接表等方式
邻接矩阵
使用
V∗V
的二维数组来表示图,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;
}