AtCoder

ABC348 F問題(Oddly Similar)を解く

AtCoder_ABC348_F

AtCoder が提供しているABC(AtCoder Beginner Contest)348 のF問題をC++で解いてみました。ABC348は、2024年4月6日21:00に実施されました。

AtCoder の紹介はこちらに、プログラミングの方針はこちらに記事があります。

F問題 Oddly Similar(Difficulty : 1955)

問題はリンク先をご覧ください。

ABC348 F問題 Oddly Similar

AtCoderの実行環境について考えてみます。AtCoder Problems による Difficulty は 1955 でした。

解答案

以下は、素直に実装したプログラムです。すべての行の組み合わせについて、一致している列の個数が奇数か調べています。難易度としてはB問題程度でしょうか。

#include <bits/stdc++.h>
using namespace std;

int main()
{
	int n, m;
	cin >> n >> m;
	vector<vector<int>> a(n, vector<int>(m));
	for (int i = 0; i < n; ++i) {
		for (int j = 0; j < m; ++j) {
			cin >> a[i][j];
		}
	}

	int result = 0;
	for (int i = 0; i < n; ++i) {
		for (int j = i + 1; j < n; ++j) {
			int same_col = 0;
			for (int k = 0; k < m; ++k) {
				if (a[i][k] == a[j][k]) {
					++same_col;
				}
			}
			if (same_col % 2 == 1) {
				++result;
			}
		}
	}

	cout << result << endl;

	return 0;
}

このプログラムは、TLE(Time Limit Exceed)判定となります。まぁ、F問題がこの考えで解けるわけはありません。

計算量(ループ回数)を評価します。$1 \leqq N, M \leqq 2000$ です。すべての行の組み合わせは、$2000 \times 1999 / 2$ となり約 $2 \times 10^6$ 通りです。すべての列を調べますから、計算量は $4 \times 10^9$ となります。

計算の複雑さに依存しますが1秒で $10^8$ 回程度のループが処理できると概算されています。この問題の制限時間は2秒です。微妙に足りません。

公式解説では、bitset高速化という手法で定数倍(64分の1)高速に処理する方法を紹介しています。

この記事では、AtCoder の C++ の実行環境を工夫して解く方法を紹介します。えびまラボ様の動画を参考にしました。

C++ プログラム例(ABC348F)

最適化オプションの強化

わたしは、C++ 環境では、「C++ 20 (gcc 12.2)」を使っています。GCC では、pragma でコンパイルオプションが指定できます。最適化 O3 に指定するだけでAC判定となりました。差分は最初の行だけです。

参考までに AtCoder が採用している gcc の最適化オプションは、O2 が指定されていました(こちらをご覧ください)。

#pragma GCC optimize("O3")

#include <bits/stdc++.h>
using namespace std;

int main()
{
	int n, m;
	cin >> n >> m;
	vector<vector<int>> a(n, vector<int>(m));
	for (int i = 0; i < n; ++i) {
		for (int j = 0; j < m; ++j) {
			cin >> a[i][j];
		}
	}

	int result = 0;
	for (int i = 0; i < n; ++i) {
		for (int j = i + 1; j < n; ++j) {
			int same_col = 0;
			for (int k = 0; k < m; ++k) {
				if (a[i][k] == a[j][k]) {
					++same_col;
				}
			}
			if (same_col % 2 == 1) {
				++result;
			}
		}
	}

	cout << result << endl;

	return 0;
}

整数の大きさを変える。

Vector コンテナの型を int(32ビット)から short(16ビット)にすると、少し速くなります。差分は以下です。

	vector<vector<short>> a(n, vector<short>(m));

入力の高速化

以下のおまじないを加えることによって、高速化できます。ただし、scanf/printf のような標準入力が使えなくなります。

	cin.tie(nullptr);
	ios::sync_with_stdio(false);

それぞれのプログラムの実行時間は以下でした。

条件実行時間(2回の平均)
(1) 対応無しTLE(2200 msec 超え)
(2) O3 を指定する。1000.5 msec
(3) (2)に加えて、shortを利用742.5 msec
(4) (3)に加えて、入力の高速化400.5 msec

O3、short利用、入力の高速化でかなり速くなっていることが分かりました。最終版のプログラムは以下です。

#pragma GCC optimize("O3")

#include <bits/stdc++.h>
using namespace std;

int main()
{
	cin.tie(nullptr);
	ios::sync_with_stdio(false);

	int n, m;
	cin >> n >> m;
	vector<vector<short>> a(n, vector<short>(m));
	for (int i = 0; i < n; ++i) {
		for (int j = 0; j < m; ++j) {
			cin >> a[i][j];
		}
	}

	int result = 0;
	for (int i = 0; i < n; ++i) {
		for (int j = i + 1; j < n; ++j) {
			int same_col = 0;
			for (int k = 0; k < m; ++k) {
				if (a[i][k] == a[j][k]) {
					++same_col;
				}
			}
			if (same_col % 2 == 1) {
				++result;
			}
		}
	}

	cout << result << endl;

	return 0;
}

AC(Accepted=正しいプログラム)と判定されました。

最後に

今回の解き方は、邪道かもしれません。ただし、gcc などの言語処理系について新しく学べることができました。

引き続き ABC の問題を紹介していきます。

COMMENT

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

CAPTCHA