【求ム】加算・減算して任意数になるかの判定関数
概要
車のナンバーでよくやる遊びですが、与えられた複数の数字を加算・減算して任意の数になるかどうかの判定処理を考えてます。
組み合わせをどう表現するか、という話になるかと思いますが、色々な言語で書いた際にどんな違いがあるかを見てみたいです。
以下ルールです。
- 言語・バージョンは問わない
- その言語で使える機能・記法なら何でも使って良い
- 外部ライブラリはなし(=その言語をセットアップした段階で使える機能のみ使用可能)
- 扱う数字は自然数の配列で与えられる
- 簡単のため、判定に使う数字は
10
とする(=加算・減算で10
になるかどうかを判定する) - 配列の先頭の数のみ、常に正の値とする(=減算を考慮しない)
-
10
になる場合はTRUE
を、ならない場合はFALSE
を出力する
上記を踏まえると、テストデータとしては以下でいいかなと思っています。
-
10
になる組み合わせ:[6,7,1,4]
-
10
にならない組み合わせ:[1,2,3,5]
- 先頭数字のマイナスを考慮した場合のみ
10
になる組み合わせ:[4,7,6,1]
回答やツッコミ(マサカリ)等々絶賛募集中です。
既に出ている例より短く・効率よく書ける例も募集です。
例えばJS
ならこんな感じ。
// 10になる組み合わせ(6+7+1-4)
const testData = [6,7,1,4];
// 10にならない組み合わせ
const testData2 = [1,2,3,5];
// 先頭をマイナスにすることで10になる組み合わせ
const testData3 = [4,7,6,1];
const checker = (data) => {
const result = data.reduce((acc, crr) => {
if (!acc.length) acc.push(crr)
else {
acc = acc.reduce((acc1, crr1) => {
acc1.push(crr1 + crr)
acc1.push(crr1 - crr)
return acc1
}, [])
}
return acc
}, []).includes(10);
console.log(result? 'TRUE' : 'FALSE');
}
// --> TRUE
checker(testData);
// --> FALSE
checker(testData2);
// --> FALSE
checker(testData3);
こんな感じで書くこともできそう?(あまり変わっていないですが)
const checker = (data) => {
const [first, ...rest] = data
const result = rest.reduce(
(acc, crr) => acc.map(n => [n + crr, n - crr]).flat(), // 現在の累計に足したパターンと引いたパターンを追加
[first]
).includes(10);
console.log(result ? 'TRUE' : 'FALSE');
}
回答ありがとうございます!
たしかにflat
使った方がスッキリしますね。
二分木として、深さ優先探索でやってみました(Scala)
object Solution {
def checker(data: Array[Int]): Boolean = {
val SUM = 10
def dfs(acc: Int, d: Array[Int]): Boolean = {
if (d.isEmpty) return acc == 10
dfs(acc + d(0), d.drop(1)) || dfs(acc - d(0), d.drop(1))
// こっちのがscalaっぽい?
// d.headOption.map(h => dfs(acc + h, d.drop(1)) || dfs(acc - h, d.drop(1))).getOrElse(acc == 10)
}
// dfs(0, data) 先頭マイナスを考慮した場合
dfs(data(0), data.drop(1))
}
}
回答ありがとうございます!
おお、Scala
初見なのですごく参考になります!
ビット全探索でやってみました。(c++)
#include <bits/stdc++.h>
using namespace std;
const int SUM = 10;
string checker(vector<int> A) {
for (int tmp = 0; tmp < (1 << A.size()); tmp++) {
bitset<20> s(tmp); // 最大20個
int sum = 0;
for (int i = 0; i < A.size(); ++i) {
if (i == 0 || s.test(i)) {
sum += A.at(i);
} else {
sum -= A.at(i);
}
}
if (sum == SUM) {
return "TRUE";
}
}
return "FALSE";
}
int main() {
cout << checker({6, 7, 1, 4}) << endl;
cout << checker({1, 2, 3, 5}) << endl;
}
回答ありがとうございます!
全然思いつきませんでした!
ビット全探索のパターンいいですね!
演算子の方を回してみた
f [a, b, c, d] = elem 10 $ (\op1 op2 op3 -> a `op1` b `op2` c `op3` d ) <$> [(+), (-)] <*> [(+), (-)] <*> [(+), (-)]
main = print $ f [6, 7, 1, 4] -- True
回答ありがとうございます!
サラッと書かれてますが、面白い上に短めのコードでまとまっていて個人的には好きです!
Haskell
分からないマンですが、これだけの記述でできちゃったりするんですね
面白そうだったのでやってみました。@ulwlu さんの方法がとても効率的なので、そちらを参考に部分和問題の部分を再帰関数で書いてみました (JavaScript) 。今回は真偽を判定するだけなので、解を見つけた時点で再帰処理を終了できます。
少し変更している部分として、先頭の数はそのまま使うルールなので、予めターゲットから引いておき、残りの数についてのみ考える。
また、正グループの総和が 0
のときはすべてが負グループということなので、その時点で真を返します。
const solve = (input, target) => {
let solved = false;
const [first, ...rest] = input;
// 先頭以外の絶対値を求める
const absRest = rest.map((v) => Math.abs(v));
// 先頭以外の正グループの総和を計算する
const targetOfRest = (absRest.reduce((acc, cur) => acc + cur) + target - first) / 2;
// 正グループの総和が整数じゃない場合は解がないのでこの時点で `false` を返す
if (!Number.isInteger(targetOfRest)) {
return false;
}
// 正グループの総和が 0 の場合はすべてが負グループということなのでこの時点で `true` を返す
if (targetOfRest === 0) {
return true;
}
// 部分和を探す再帰関数、解を見つけた時点で再帰処理を終了する
const find = (index = 0, acc = 0) => {
for (let i = index; i < absRest.length; i++) {
if (solved) break;
const sum = acc + absRest[i];
if (sum === targetOfRest) {
solved = true;
break;
} else if (sum < targetOfRest) {
find(i + 1, sum);
}
}
};
find();
return solved;
}
console.log(solve([6, 7, 1, 4], 10)); // true
console.log(solve([1, 2, 3, 5], 10)); // false
console.log(solve([4, 7, 6, 1], 10)); // false
console.log(solve([25, -99, 44, -50, 37, -289, 54, 500], 10)); // true
回答ありがとうございます!
普段JS
メインなので、JS
に落とし込むとより分かりやすかったです!
最初に自分がサンプルで提示したものより格段に効率がよくなりましたね
お礼
回答いただいた皆様、ありがとうございました!
様々な回答パターンが見れてとても参考になりました!
※この投稿時点で一旦このスクラップはクローズさせていただきますが、以降も回答いただいても構いません。