グラフを使った解法を勉強した. グラフの表現方法として隣接行列と隣接リストの2つがあって, 最短経路問題を特にはベルマンフォード法, ダイクストラ法, ワーシャル-フロイド法があり, 最小全域木問題を特にはプリム法とクラスカル法がある. この一部を使った演習問題を紹介する.
二部グラフ判定
深さ優先探索を用いて, 隣接している頂点をどんどん塗っていく. グラフは隣接リストを使って表現されている.
#include <iostream>
#include <vector>
using namespace std;
const int MAX_V = 10000;
vector<int> G[MAX_V];
int V, E;
int color[MAX_V];
//頂点を1と-1で塗っていく
bool dfs(int v, int c) {
color[v] = c;
for (int i = 0; i < G[v].size(); i++) {
// 隣接している頂点が同じ色ならfalse
if (color[G[v][i]] == c) {
return false;
}
// 隣接している頂点がまだ塗られていないなら-cで塗る
if (color[G[v][i]] == 0 && !dfs(G[v][i], -c)) {
return false;
}
}
return true;
}
int main(int argc, char const *argv[]) {
cin >> V >> E;
for (int i = 0; i < E; i++) {
int s, t;
cin >> s >> t;
G[s].push_back(t);
G[t].push_back(s);
}
for (int i = 0; i < V; i++) {
if (color[i] == 0) {
if (!dfs(i, 1)) {
printf("No\n");
return 1;
}
}
}
printf("Yes\n");
return 0;
}
Roadblocks
道を辺として, 交差点を頂点として2番目に短い経路を求める問題. 基本的にはダイクストラ法を用いるが, 2番目に短い経路も記憶しておくところがみそ.
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
const int MAX_N = 1000;
const int INF = 99999999;
struct edge {
int to, cost;
};
typedef pair<int, int> P; // firstは最短距離, secondは頂点の番号
int N, R, E;
vector<edge> G[MAX_N]; // グラフの隣接リスト表現
int dist[MAX_N];
int dist2[MAX_N];
void read() {
cin >> N >> R >> E;
for (int i = 0; i < E; i++) {
int s, t, c;
cin >> s >> t >> c;
edge x, y;
x.to = s;
y.to = t;
x.cost = c;
y.cost = c;
G[s].push_back(y);
G[t].push_back(x);
}
}
void solve() {
priority_queue<P, vector<P>, greater<P> > que;
fill(dist, dist + N, INF);
fill(dist2, dist2 + N, INF);
// 発ノード
dist[0] = 0;
que.push(P(0, 0));
while (!que.empty()) {
P p = que.top();
que.pop();
int v = p.second;
int d = p.first;
if (dist2[v] < d) {
// 2番目の最短路より長い
continue;
}
// vと隣接している頂点を更新
for (int i = 0; i < G[v].size(); i++) {
edge e = G[v][i];
int d2 = d + e.cost;
if (dist[e.to] > d2) {
// 最短より短い場合
swap(dist[e.to], d2);
que.push(P(dist[e.to], e.to));
}
if (dist2[e.to] > d2 && dist[e.to] < d2) {
// 2番目より短い場合
dist2[e.to] = d2;
que.push(P(dist2[e.to], e.to));
}
}
}
printf("%d\n", dist2[N - 1]);
}
int main(int argc, char const *argv[]) {
read();
solve();
return 0;
}
Conscription
人を頂点として親密度をコスト付きの辺として, 最小全域木をもとめる問題. この時, コストの高い辺を使いたいのでコストの正負を反転させる. また, この問題は男と女が分けられているが, 特殊な構造を使わなかった.
最小全域木を求めるときはクラスカル法を用いた. クラスカル法はUnionFind木の手法を応用している.
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
const int MAX_N = 50000;
const int MAX_E = 50000;
const int MAX_R = 50000;
// union find
// --------------------------------------------
int par[MAX_N];
int depth[MAX_N];
void init_union_find(int n) {
for (int i = 0; i < n; i++) {
par[i] = i;
depth[i] = 0;
}
}
int find(int x) {
if (par[x] == x) {
return x;
} else {
return par[x] = find(par[x]);
}
}
void unite(int x, int y) {
x = find(x);
y = find(y);
if (x == y) return;
if (depth[x] < depth[y]) {
par[x] = y;
} else {
par[y] = x;
if (depth[x] == depth[y]) depth[x]++;
}
}
bool same(int x, int y) { return find(x) == find(y); }
struct edge {
int u, v, cost;
};
bool comp(const edge& e1, const edge& e2) { return e1.cost < e2.cost; }
edge es[MAX_E];
int V, E;
int kruskal() {
sort(es, es + E, comp);
init_union_find(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 N, M, R;
int x[MAX_R], y[MAX_R], d[MAX_R];
void read() {
cin >> N >> M >> R;
for (int i = 0; i < R; i++) {
cin >> x[i] >> y[i] >> d[i];
}
}
void solve() {
V = N + M;
E = R;
for (int i = 0; i < E; i++) {
es[i] = (edge){x[i], N + y[i], -d[i]};
}
printf("%d\n", 10000 * (V) + kruskal());
}
int main(int argc, char const* argv[]) {
read();
solve();
return 0;
}
Layout
牛を頂点と考え, 牛の番号, 仲の良さ, 仲の悪さをコストつきの辺として最短経路を求める問題. 発想としては, 全てを制約条件に落としこんだ結果, 両辺に変数が1つずつしか現れていないことに着目する.
今回は負の辺があるのでベルマンフォード法を用いる. 負閉路が存在するかどうかは, 牛の数だけupdateを行ったあとにも更新があるかどうかで判断する.
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
const int MAX_ML = 10000;
const int MAX_MD = 10000;
const int MAX_N = 1000;
const int INF = 99999999;
int N, ML, MD;
int AL[MAX_ML], BL[MAX_ML], DL[MAX_ML];
int AD[MAX_MD], BD[MAX_MD], DD[MAX_MD];
int d[MAX_N];
bool updated;
void read() {
cin >> N >> ML >> MD;
for (int i = 0; i < ML; i++) {
cin >> AL[i] >> BL[i] >> DL[i];
}
for (int i = 0; i < MD; i++) {
cin >> AD[i] >> BD[i] >> DD[i];
}
}
void update(int& x, int y) {
if (x > y) {
x = y;
updated = true;
}
}
void bellmanford() {
for (int k = 0; k <= N; k++) {
updated = false;
// i+1からiへコスト0
for (int i = 0; i + 1 < N; i++) {
if (d[i + 1] < INF) {
update(d[i], d[i + 1]);
}
}
// ALからBLへコストDL
for (int i = 0; i < ML; i++) {
if (d[AL[i] - 1] < INF) {
update(d[BL[i] - 1], d[AL[i] - 1] + DL[i]);
}
}
// BDからADへコスト-DD
for (int i = 0; i < MD; i++) {
if (d[BD[i] - 1] < INF) {
update(d[AD[i] - 1], d[BD[i] - 1] - DD[i]);
}
}
}
}
void solve() {
// 負閉路チェック
fill(d, d + N, 0);
bellmanford();
if (updated) {
printf("%d\n", -1);
return;
}
fill(d, d + N, INF);
d[0] = 0;
bellmanford();
int res = d[N - 1];
if (res == INF) {
res = -2;
}
printf("%d\n", res);
}
int main(int argc, char const* argv[]) {
read();
solve();
return 0;
}
感想
最後の牛の並びを最短経路問題に変換する方法はむずすぎる….