vi
から vj
への edge(vi, vj)
について i < j
となることが任意の edge
について成立する vi
の順序。in.txt
5 4 ← 頂点数、辺数
0 3 9 ← vi と vj の cost
1 2 1
1 3 2
1 4 8
main.cpp
隣接リスト
#include <iostream>
#include <vector>
#define INF (1e9)
using namespace std;
const int MAXN = 1024; // 最大頂点数
typedef pair<int,int> Edge;
#define to first
#define cost second
vector<Edge> G[MAXN];
int V,E;
int main() {
cin>>V>>E;
for (int i = 0; i < E; ++i) {
int a,b,cost; cin>>a>>b>>cost;
G[a].push_back(Edge(b,cost));
G[b].push_back(Edge(a,cost));
}
for(int i = 0; i < V; ++i) {
for(int j = 0; j < (int)G[i].size(); ++j) {
cout << "from: " << i
<< ", to: " << G[i][j].to
<< ", cost: " << G[i][j].cost
<< endl;
}
}
return 0;
}
隣接行列
#include <iostream>
#include <vector>
#define INF (1e9)
using namespace std;
const int MAXN = 1024; // 最大頂点数
int G[MAXN][MAXN];
int V,E;
int main() {
cin>>V>>E;
fill((int *)G, (int *)(G+MAXN), INF);
for (int i = 0; i < E; ++i) {
int a,b,cost; cin>>a>>b>>cost;
G[a][b] = G[b][a] = cost;
}
for(int i = 0; i < V; ++i) {
for(int j = 0; j < V; ++j) {
if(G[i][j] == INF) continue;
cout << "from: " << i
<< ", to: " << j
<< ", cost: " << G[i][j]
<< endl;
}
}
return 0;
}
グラフの頂点を O(n)
で走査して、グラフの性質を判定できます。二部グラフの判定や連結グラフの判定など。
#include <iostream>
#include <vector>
#define INF (1e9)
using namespace std;
const int MAXN = 1024;
typedef pair<int,int> Edge;
#define to first
#define cost second
vector<Edge> G[MAXN];
int V,E;
int color[MAXN]; // 初期値: 0, 二色: -1, +1
bool dfs(int v, int c) {
color[v] = c;
for(int i = 0; i < (int)G[v].size(); ++i) { // 頂点 v の辺を走査
if(color[G[v][i].to] == c) return false;
if(color[G[v][i].to] == 0 && !dfs(G[v][i].to, -c)) return false;
}
return true; // 彩色できる
}
int main() {
cin>>V>>E;
fill(color, color+MAXN, 0);
for (int i = 0; i < E; ++i) {
int a,b,cost; cin>>a>>b>>cost;
G[a].push_back(Edge(b,cost));
G[b].push_back(Edge(a,cost));
}
// 二部グラフ判定用 dfs
// 連結グラフ用
cout << (dfs(0, 1) ? "Yes" : "No") << endl;
// // 連結していない可能性がある場合はこちら
// for(int i = 0; i < V; ++i) {
// if(color[i] != 0) continue;
// if(!dfs(i, 1)) { cout << "No" << endl; return 0; }
// }
// cout << "Yes" << endl;
return 0;
}
while
ループ一回につき、始点から 1 ステップ先の最短経路が確定するイメージのアルゴリズムです。負の辺がない場合に使用できます。ベルマンフォード法よりも効率がよいです。
#include <iostream>
#include <vector>
#include <queue>
#define INF (1e9)
using namespace std;
const int MAXN = 1024;
typedef pair<int,int> Edge;
#define to first
#define cost second
vector<Edge> G[MAXN];
typedef pair<int,int> pii;
#define A top().first
#define B top().second
int V,E;
int dist[MAXN];
void dijkstra(int s) {
fill(dist, dist+MAXN, INF);
dist[s] = 0;
priority_queue<pii,vector<pii>,greater<pii> > pq; // greater: pair.first が小さい順
pq.push(pii(0,s)); // cost, vertex
while(!pq.empty()) {
int cost = pq.A;
int v = pq.B;
pq.pop();
if(cost > dist[v]) continue;
for(int i = 0; i < (int)G[v].size(); ++i) {
Edge e = G[v][i];
if(dist[e.to] > dist[v] + e.cost) {
dist[e.to] = dist[v] + e.cost;
pq.push(pii(dist[e.to], e.to));
}
}
}
}
int main() {
cin>>V>>E;
for (int i = 0; i < E; ++i) {
int a,b,cost; cin>>a>>b>>cost;
G[a].push_back(Edge(b,cost));
G[b].push_back(Edge(a,cost));
}
dijkstra(0); // 始点 0 からの dist を計算
for(int i = 0; i < V; ++i) cout << "0 -> " << i << " : " << dist[i] << endl;
return 0;
}
previous
に最短経路が更新された頂点の直前の頂点を記憶しておくことで、最短経路の出力ができます。以下のように 4 箇所を追加します。ベルマンフォード法やワーシャルフロイド法も同様の考え方で対応できます。
#include <iostream>
#include <vector>
#include <queue>
#define INF (1e9)
using namespace std;
const int MAXN = 1024;
typedef pair<int,int> Edge;
#define to first
#define cost second
vector<Edge> G[MAXN];
typedef pair<int,int> pii;
#define A top().first
#define B top().second
int V,E;
int dist[MAXN];
int previous[MAXN]; // vi の直前の vertex を記憶するための配列 (追加 1/4)
void dijkstra(int s) {
fill(dist, dist+MAXN, INF);
fill(previous, previous+MAXN, -1); // 追加 2/4
dist[s] = 0;
priority_queue<pii,vector<pii>,greater<pii> > pq; // greater: pair.first が小さい順
pq.push(pii(0,s)); // cost, vertex
while(!pq.empty()) {
int cost = pq.A;
int v = pq.B;
pq.pop();
if(cost > dist[v]) continue;
for(int i = 0; i < (int)G[v].size(); ++i) {
Edge e = G[v][i];
if(dist[e.to] > dist[v] + e.cost) {
dist[e.to] = dist[v] + e.cost;
previous[e.to] = v; // 追加 3/4
pq.push(pii(dist[e.to], e.to));
}
}
}
}
vector<int> get_path(int t) { // 追加 4/4
vector<int> path;
for(; t != -1; t = previous[t]) path.push_back(t);
reverse(path.begin(), path.end());
return path;
}
int main() {
cin>>V>>E;
for (int i = 0; i < E; ++i) {
int a,b,cost; cin>>a>>b>>cost;
G[a].push_back(Edge(b,cost));
G[b].push_back(Edge(a,cost));
}
dijkstra(0); // 始点 0 からの dist を計算
for(int i = 0; i < V; ++i) { // 始点 0 から各 vi までの最短経路を表示する例
vector<int> path = get_path(i);
for(int j = 0; j < (int)path.size(); ++j) {
cout << path[j];
if(j != (int)path.size() - 1) cout << " -> ";
}
cout << endl;
}
return 0;
}
while
ループ一回につき、始点から 1 ステップ先の最短経路が確定するイメージのアルゴリズムです。負のコストの閉路が存在しなければ |V| - 1
回の while
で終了します。
#include <iostream>
#include <vector>
#define INF (1e9)
using namespace std;
const int MAXN = 1024;
typedef pair<int,int> Edge;
#define to first
#define cost second
vector<Edge> G[MAXN];
int V,E;
int dist[MAXN];
void bellman_ford(int s) {
fill(dist, dist+MAXN, INF);
dist[s] = 0;
while(true) {
bool update = false;
for(int i = 0; i < V; ++i) {
for(int j = 0; j < (int)G[i].size(); ++j) {
Edge e = G[i][j];
if(dist[i] != INF && dist[e.to] > dist[i] + e.cost) {
dist[e.to] = dist[i] + e.cost;
update = true;
}
}
}
if(!update) break;
}
}
int main() {
cin>>V>>E;
for (int i = 0; i < E; ++i) {
int a,b,cost; cin>>a>>b>>cost;
G[a].push_back(Edge(b,cost));
G[b].push_back(Edge(a,cost));
}
bellman_ford(0); // 始点 0 からの dist を計算
for(int i = 0; i < V; ++i) cout << "0 -> " << i << " : " << dist[i] << endl;
return 0;
}
O(n^3)
と計算量はかかりますが、任意の二つの頂点の最短経路をすべて計算できます。単一始点最短経路問題の解法にもなります。隣接リストではなく隣接行列を利用します。
G[i][i]
(どちらも i
です) が存在します。検出に使えます。main.cpp
#include <iostream>
#include <vector>
#define INF (1e9)
using namespace std;
const int MAXN = 1024;
int G[MAXN][MAXN]; // vi から vj への最短経路
int V,E;
void warshall_floyd() {
for (int k = 0; k < V; ++k) { // vertex k を固定して考えます。
for (int i = 0; i < V; ++i) {
for (int j = 0; j < V; ++j) {
// k を経由したほうがよいかどうか (複数の vertex 集合が k として代表される)
G[i][j] = min(G[i][j], G[i][k] + G[k][j]);
}
}
}
}
int main() {
cin>>V>>E;
fill((int *)G, (int *)(G+MAXN), INF);
for(int i = 0; i < V; ++i) G[i][i] = 0; // 重要
for (int i = 0; i < E; ++i) {
int a,b,cost; cin>>a>>b>>cost;
G[a][b] = G[b][a] = cost;
}
warshall_floyd();
for(int i = 0; i < V; ++i) {
for(int j = 0; j < V; ++j) {
if(G[i][j] == INF) continue;
cout << i << " -> "
<< j << " : "
<< G[i][j]
<< endl;
}
}
return 0;
}
最小全域木が存在する (連結な重み付き無向グラフである) とします。最小全域木をプリム法で求めることができます。
prim()
の返り値)vertex 0
としたときの vertex i
に下りるコスト (mincost[i]
)main.cpp
#include <iostream>
#include <vector>
#include <queue>
#define INF (1e9)
using namespace std;
const int MAXN = 1024;
typedef pair<int,int> Edge;
#define to first
#define cost second
vector<Edge> G[MAXN];
typedef pair<int,int> pii;
#define A top().first
#define B top().second
int V,E;
int mincost[MAXN]; // 最小全域木の根を `vertex 0` としたときの `vertex i` に下りるコスト
bool used[MAXN];
int prim() {
fill(mincost, mincost+MAXN, INF);
fill(used, used+MAXN, false);
mincost[0] = 0; // vertex 0 から貪欲的に使用済み集合を拡大
int res = 0; // 辺のコストの総和
priority_queue<pii,vector<pii>,greater<pii> > pq; // greater: pair.first が小さい順
pq.push(pii(0,0)); // cost, vertex
while(!pq.empty()) {
int cost = pq.A;
int v = pq.B;
pq.pop();
if(used[v]) continue;
res += cost;
used[v] = true; // 使用済み集合に加える
for(int i = 0; i < (int)G[v].size(); ++i) {
Edge e = G[v][i]; // vertex v に生えている辺は「使用済み集合」に新規に生えた辺
if(!used[e.to] && mincost[e.to] > e.cost) {
mincost[e.to] = e.cost;
pq.push(pii(mincost[e.to], e.to));
}
}
}
return res;
}
int main() {
cin>>V>>E;
for (int i = 0; i < E; ++i) {
int a,b,cost; cin>>a>>b>>cost;
G[a].push_back(Edge(b,cost));
G[b].push_back(Edge(a,cost));
}
cout << prim() << endl; // 最小全域木の辺のコストの総和
return 0;
}
Union-Find 木を用いたクラスカル法によっても最小全域木を求めることができます。イメージはプリム法と同じで、コストの小さい辺から順に unite
していきます。
kruskal()
の返り値)main.cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN = 1024;
int par[MAXN];
int rnk[MAXN];
void init(int n) { for(int i = 0; i < n; ++i) { par[i] = i; rnk[i] = 0; }}
int find(int x) { if(par[x] == x) return x; else return par[x] = find(par[x]); }
bool same(int x, int y) { return find(x) == find(y); }
void unite(int x, int y) { x = find(x); y = find(y); if(x == y) return;
if(rnk[x] < rnk[y]) par[x] = y; else { par[y] = x; if(rnk[x] == rnk[y]) ++rnk[x]; }}
// クラスカル法においては Edge を cost でソートする必要があるため、
// u, v を cost と同様に要素として扱えるデータ構造 struct を利用します。
struct Edge { int u, v, cost; };
Edge es[MAXN];
int V,E;
bool comp(const Edge& e1, const Edge& e2) {
return e1.cost < e2.cost;
}
int kruskal() {
sort(es, es + E, comp);
init(V);
int res = 0;
for(int i = 0; i < E; ++i) {
Edge e = es[i];
if(!same(e.u, e.v)) {
unite(e.u, e.v);
res += e.cost;
}
}
return res;
}
int main() {
cin>>V>>E;
for (int i = 0; i < E; ++i) {
Edge e; cin>>e.u>>e.v>>e.cost;
es[i] = e;
}
cout << kruskal() << endl; // 最小全域木の辺のコストの総和
return 0;
}