C++

ラムダ式で再帰関数を実装する

ISO_C++_Logo

ラムダ式を使った関数内での再帰関数の実装を紹介します。

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;
}

紹介記事でのプログラムは、再帰関数で参照する変数が多くあります。これらをグローバル変数にしていたため、全体の見通しがよくないプログラムになっていました。

ラムダ式を使ったプログラムは、実行したいこと(指定された橋を通る経路の最小値の計算)をその場に書いているため、全体が把握しやすいです。

最後に

整理しておきたい文法事項を記事にしました。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA