ラムダ式を使った関数内での再帰関数の実装を紹介します。
DFS を再帰関数として実装する。
以下は、グラフ $G$ に対して、DFS(深さ優先探索:Depth-First Search)を使って全探索を行うコードです。よく使われるコード例です。
#define N 1000001
vector<vector<int>> G(N);
vector<bool> seen(N, false);
void dfs(int v)
{
seen[v] = true;
for (auto next_v : G[v]) {
if (!seen[next_v]) {
dfs(next_v);
}
}
}
再帰関数 dfs を別関数として実装しています。グラフ G と配列 seen をグローバル変数にしてします。
この実装の利点は以下です。
- C++の言語バージョンを問わない(C++98/C++03 で使える)。
- 初歩的な文法で書ける。
良くないと感じるところは以下です。
- 引数で情報が渡しにくい場合、グローバル変数を使うしかない。
- グローバル変数は、ローカル変数と比較するとデバッグの手間がかかる。
- 競技プログラミングのような全体が小さいプログラムの場合、関数の定義と関数を呼び出す場所の距離が離れるため、繰り返し見るのが少し面倒に感じる。
ラムダ式で実装する。
まずは実装例を示します。C++14以降で利用できる文法を使っています。
vector<vector<int>> G(n + 1);
vector<bool> seen(n + 1, false);
auto dfs = [&](auto self, int v) -> void {
seen[v] = true;
for (auto next_v : G[v]) {
if (!seen[next_v]) {
self(self, next_v);
}
}
};
ラムダ式の補足です。
- ラムダ式の宣言の最初で
[&]
としています。スコープの外の変数を使う場合には、&
が必要です。引数しか使わない場合は、[]
と書きます。 - ラムダ式の最初の引数は自分自身を指定します。
- 再帰呼び出しは、最初の引数で関数自体を指定します(8行目)。
実際に使って見ましょう。連結成分個数を求める ABC284 C問題(解説記事)のプログラムをラムダ式で書きました。
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n, m;
cin >> n >> m;
vector<vector<int>> G(n + 1);
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
vector<bool> seen(n + 1, false);
auto dfs = [&](auto self, int v) -> void {
seen[v] = true;
for (auto next_v : G[v]) {
if (!seen[next_v]) {
self(self, next_v);
}
}
};
int result = 0;
for (int i = 1; i <= n; ++i) {
if (!seen[i]) {
++result;
dfs(dfs, i);
}
}
cout << result << endl;
return 0;
}
ラムダ式で書いた上記のプログラムは、再帰関数 dfs の関数本体と呼び出し(30行目)が近いため、ロジックの把握がしやすいです。また、すべてを main 関数内で書いているため、グローバル変数も必要ありません。
次に、昨日紹介した ABC369 E問題(解説記事)のプログラムをラムダ式で書きなおします。
#include <bits/stdc++.h>
using namespace std;
typedef long long int ll;
#define INF (1LL << 60)
int main()
{
int n, m;
cin >> n >> m;
vector<vector<ll>> dist(n, vector<ll>(n, INF));
for (int i = 0; i < n; ++i) {
dist[i][i] = 0;
}
vector<int> u(m);
vector<int> v(m);
vector<ll> t(m);
for (int i = 0; i < m; ++i) {
cin >> u[i] >> v[i] >> t[i];
--u[i];
--v[i];
dist[u[i]][v[i]] = min(dist[u[i]][v[i]], t[i]);
dist[v[i]][u[i]] = min(dist[v[i]][u[i]], t[i]);
}
for (int k = 0; k < n; ++k) {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if ((dist[i][k] < INF)&&(dist[k][j] < INF)) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
int q;
cin >> q;
for (int i = 0; i < q; ++i) {
int k;
cin >> k;
vector<int> b(k);
for (int i = 0; i < k; ++i) {
cin >> b[i];
--b[i];
}
vector<bool> used(k, false);
ll result = INF;
auto rec = [&](auto self, int num, ll w, int from) -> void {
if (num == k) {
result = min(result, w + dist[from][n - 1]);
return;
}
for (int i = 0; i < k; ++i) {
if (used[i]) {
continue;
}
used[i] = true;
self(self, num + 1, w + dist[from][u[b[i]]] + t[b[i]], v[b[i]]);
self(self, num + 1, w + dist[from][v[b[i]]] + t[b[i]], u[b[i]]);
used[i] = false;
}
return;
};
rec(rec, 0, 0, 0);
cout << result << endl;
}
return 0;
}
紹介記事でのプログラムは、再帰関数で参照する変数が多くあります。これらをグローバル変数にしていたため、全体の見通しがよくないプログラムになっていました。
ラムダ式を使ったプログラムは、実行したいこと(指定された橋を通る経路の最小値の計算)をその場に書いているため、全体が把握しやすいです。
最後に
整理しておきたい文法事項を記事にしました。