CodeIQ:プラマイゼロは何通り?

私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「プラマイゼロは何通り?」(CodeIQ)を参照してください。

問題の概要

次のようにパラメータが入力されます。

1
5
-3
3
-2
-5
10
入力された値から2つを選んだとき、それらの和がゼロになるような組み合わせが何通りあるかを求めてください。

私のプログラム

C++で解答しています。

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <set>
using namespace std;

int check(set< int >& to_add, set< int >& to_search, int n){
	int	ret = 0;

	set< int >::iterator i = to_search.find(-1*n);
	if(i != to_search.end()){
		ret = 1;
	}

	to_add.insert(n);

	return ret;
}

int isNumber(char* str){
	if((str[0] == '\n') || (str[0] == '\r') || (str[0] == 0)){
		return 0;
	}

	for(int i=0; str[i]; i++){
		if((str[i] == '\n') || (str[i] == '\r') || (str[i] == 0)){
			str[i] = 0;
			break;
		}
		else if((str[i] == '+') || (str[i] == '-')){
			continue;
		}
		else if(isdigit(str[i]) == 0){
			return 0;
		}
	}
	return 1;
}

int main(int argc, char* argv[]){
	char str[1024] = {0};
	set< int >	MinusSet;
	set< int >	PlusSet;
	int	Count = 0;

	while( fgets(str, sizeof(str), stdin) != NULL ){
		if(isNumber(str) == 0){
			break;
		}

		int n = atoi(str);
		if(n<0){
			Count += check(MinusSet, PlusSet, n);
		}
		else{
			Count += check(PlusSet, MinusSet, n);
		}
	}
	printf("%d", Count);
	return 0;
}

解説

それでは解説します。
言語にC++を選択したのはsetが使いたかったからです。この問題を解答した時点ではPythonを覚えていないので選択肢としてはC++かJava(JavascriptやPHPでも連想配列で代用できますがうっとおしいので)ですが、わざわざクラスを作るのが面倒なのでC++にしました。なのでプログラムはC++と言うよりsetを使ってC言語で書いたという感じになっています。

数値かどうかをチェックする

実務のプログラムではないので入力エラーに対してそれほど慎重になる必要はないかもしれませんが一応入力チェックをしています。isNumber()がそうです。
ごく単純に+か-で始まって0〜9の文字だけで構成されているかをチェックしています。33行目のisdigit()はCの標準ライブラリの関数で「('0' <= str[i] || str[i] <= '9')」でも同じですが用意されているものは使った方が良いと思います。
27行目で0を代入しているのは念のためです(atoi()やstrtol()のような関数は数値に変換できない文字を無視します(変換できるところまでを変換する))。

±0になる組み合わせを探す

考え方は次の通りです。
  • プラスの値の集合とマイナスの値の集合を作る
  • 入力値を1行ずつ処理する
  • 入力値がプラスならマイナスの集合から、マイナスならプラスの集合から相手を探す

問題には値の重複はないとあったのでsetを使用すれば簡単に集合に値が含まれるかをチェックできます。重複があったらmultisetを使えば済みます。C++のsetのデータ構造は二分木なので高速な探索が可能です。

実際の処理を行っているのがcheck()で、プラスの値の集合がPlusSet、マイナスの値の集合がMinusSetです。check()の第一引数は入力値の保存先の集合、第二引数は探索対象の集合、第三引数は入力値です。
入力値がプラスならプラスの集合に入力値を加え、マイナスの集合から対に成る値を探せば良いし、入力値がマイナスならその逆にすれば良いので、52〜57業目で入力値がプラスかマイナスで保存先と探索対象の集合を入れ替えて処理しています。

10行目で探索を行っています。入力値に-1をかけることで符合が反転し探索対象の集合の符合が一致するので集合に値があれば1を返し、なければ0を返します。0か1が返るので呼び出し元でそれを加算して行けば結果になります。
15行目は以降の探索のために入力値を追加対象の集合に加えています。

雑感

それほど変わったテクニックなどは使っていません。ただ、問題の通り合計して0に成る値を探すよりも、setのようなデータ構造を利用して符合を反転して同じ値を探す方が簡単なプログラムになります。私の経験上setは意外に使い出のあるデータ構造です。