私はいつもこれをやりたいと思っていましたが、問題について考え始めるたびに、その指数関数的な性質のために頭がおかしくなります。
私が理解できるようにしたい問題ソルバーとコードは、カウントダウン数学の問題用です:
与えられた数X1からX5のセットは、数学演算を使用してそれらを組み合わせてYを作成する方法を計算します。乗算、除算、加算、および減算を適用できます。
では、1,3,7,6,8,3
作成348
?
回答:(((8 * 7) + 3) -1) *6 = 348
。
この問題を解決できるアルゴリズムを作成するにはどうすればよいですか?このような問題を解決しようとするとき、どこから始めますか?このようなアルゴリズムを設計する際に考慮すべき重要な考慮事項は何ですか?
Javaでの非常に迅速で汚い解決策:
_public class JavaApplication1
{
public static void main(String[] args)
{
List<Integer> list = Arrays.asList(1, 3, 7, 6, 8, 3);
for (Integer integer : list) {
List<Integer> runList = new ArrayList<>(list);
runList.remove(integer);
Result result = getOperations(runList, integer, 348);
if (result.success) {
System.out.println(integer + result.output);
return;
}
}
}
public static class Result
{
public String output;
public boolean success;
}
public static Result getOperations(List<Integer> numbers, int midNumber, int target)
{
Result midResult = new Result();
if (midNumber == target) {
midResult.success = true;
midResult.output = "";
return midResult;
}
for (Integer number : numbers) {
List<Integer> newList = new ArrayList<Integer>(numbers);
newList.remove(number);
if (newList.isEmpty()) {
if (midNumber - number == target) {
midResult.success = true;
midResult.output = "-" + number;
return midResult;
}
if (midNumber + number == target) {
midResult.success = true;
midResult.output = "+" + number;
return midResult;
}
if (midNumber * number == target) {
midResult.success = true;
midResult.output = "*" + number;
return midResult;
}
if (midNumber / number == target) {
midResult.success = true;
midResult.output = "/" + number;
return midResult;
}
midResult.success = false;
midResult.output = "f" + number;
return midResult;
} else {
midResult = getOperations(newList, midNumber - number, target);
if (midResult.success) {
midResult.output = "-" + number + midResult.output;
return midResult;
}
midResult = getOperations(newList, midNumber + number, target);
if (midResult.success) {
midResult.output = "+" + number + midResult.output;
return midResult;
}
midResult = getOperations(newList, midNumber * number, target);
if (midResult.success) {
midResult.output = "*" + number + midResult.output;
return midResult;
}
midResult = getOperations(newList, midNumber / number, target);
if (midResult.success) {
midResult.output = "/" + number + midResult.output;
return midResult
}
}
}
return midResult;
}
}
_
[〜#〜]更新[〜#〜]
これは基本的に、指数関数的に複雑な単純なブルートフォースアルゴリズムです。ただし、いくつかのヒューリスティック関数を活用することで、いくつかの改善を得ることができます。これは、一連の数値または(および)getOperatiosn()
関数再帰の各レベルで処理する演算を順序付けるのに役立ちます。
このようなヒューリスティック関数の例は、たとえば、中間結果と合計ターゲット結果の違いです。
ただし、この方法では、最良の場合と平均的な場合の複雑さのみが改善されます。最悪の場合の複雑さはそのままです。
最悪の場合の複雑さは、ある種の分岐切断によって改善できます。この場合、それが可能かどうかはわかりません。
確かにそれは指数関数的ですが、それは小さいので、良い(十分な)素朴な実装は良いスタートになるでしょう。通常のインフィックス表記をブラケットで削除し、ポストフィックスを使用することをお勧めします。プログラミングが簡単です。出力はいつでも別のステージとしてきれいにすることができます。
数値と演算子のすべての(有効な)シーケンスをリストして評価することから始めます。例(postfix):
1 3 7 6 8 3 + + + + + -> 28
1 3 7 6 8 3 + + + + - -> 26
私のJavaは笑える、笑うためにここに来ることはないので、これのコーディングはあなたに任せます。
これを読んでいるすべての賢い人々にとって:はい、私はこのような小さな問題でもより速くなる可能性が高いよりスマートなアプローチがあることを知っています。他の誰かがよりスマートなソリューションで答えを書くことができます。
だから、あなたの質問に答えるために:
以下のc ++ 11の実用的なソリューション。
基本的な考え方は、スタックベースの評価を使用し( [〜#〜] rpn [〜#〜] を参照)、表示のために実行可能なソリューションを 中置記法 に変換することです。のみ。
N
入力桁がある場合、各演算子はバイナリであるため、(N-1)
演算子を使用します。
まず、オペランドと演算子(selector_
配列)の有効な順列を作成します。有効な順列とは、スタックのアンダーフローなしで評価でき、スタック上の1つの値(結果)で終わる順列です。したがって、1 1 +
は有効ですが、1 + 1
は無効です。
このような各オペランドと演算子の順列を、オペランドのすべての順列(values_
配列)および演算子のすべての組み合わせ(ops_
配列)でテストします。マッチング結果はきれいに印刷されます。
引数はコマンドラインから[-s] <target> <digit>[ <digit>...]
として取得されます。 -s
スイッチは完全な検索を防ぎ、最初に一致した結果のみが出力されます。
(./mathpuzzle 348 1 3 7 6 8 3
を使用して元の質問の回答を取得します)
このソリューションでは、入力桁を連結して数値を形成することはできません。これは、追加の外部ループとして追加できます。
作業コードは here からダウンロードできます。 (注:ソリューションを形成するために入力桁を連結するためのサポートでそのコードを更新しました)
追加の説明については、コードコメントを参照してください。
#include <iostream>
#include <vector>
#include <algorithm>
#include <stack>
#include <iterator>
#include <string>
namespace {
enum class Op {
Add,
Sub,
Mul,
Div,
};
const std::size_t NumOps = static_cast<std::size_t>(Op::Div) + 1;
const Op FirstOp = Op::Add;
using Number = int;
class Evaluator {
std::vector<Number> values_; // stores our digits/number we can use
std::vector<Op> ops_; // stores the operators
std::vector<char> selector_; // used to select digit (0) or operator (1) when evaluating. should be std::vector<bool>, but that's broken
template <typename T>
using Stack = std::stack<T, std::vector<T>>;
// checks if a given number/operator order can be evaluated or not
bool isSelectorValid() const {
int numValues = 0;
for (auto s : selector_) {
if (s) {
if (--numValues <= 0) {
return false;
}
}
else {
++numValues;
}
}
return (numValues == 1);
}
// evaluates the current values_ and ops_ based on selector_
Number eval(Stack<Number> &stack) const {
auto vi = values_.cbegin();
auto oi = ops_.cbegin();
for (auto s : selector_) {
if (!s) {
stack.Push(*(vi++));
continue;
}
Number top = stack.top();
stack.pop();
switch (*(oi++)) {
case Op::Add:
stack.top() += top;
break;
case Op::Sub:
stack.top() -= top;
break;
case Op::Mul:
stack.top() *= top;
break;
case Op::Div:
if (top == 0) {
return std::numeric_limits<Number>::max();
}
Number res = stack.top() / top;
if (res * top != stack.top()) {
return std::numeric_limits<Number>::max();
}
stack.top() = res;
break;
}
}
Number res = stack.top();
stack.pop();
return res;
}
bool nextValuesPermutation() {
return std::next_permutation(values_.begin(), values_.end());
}
bool nextOps() {
for (auto i = ops_.rbegin(), end = ops_.rend(); i != end; ++i) {
std::size_t next = static_cast<std::size_t>(*i) + 1;
if (next < NumOps) {
*i = static_cast<Op>(next);
return true;
}
*i = FirstOp;
}
return false;
}
bool nextSelectorPermutation() {
// the start permutation is always valid
do {
if (!std::next_permutation(selector_.begin(), selector_.end())) {
return false;
}
} while (!isSelectorValid());
return true;
}
static std::string buildExpr(const std::string& left, char op, const std::string &right) {
return std::string("(") + left + ' ' + op + ' ' + right + ')';
}
std::string toString() const {
Stack<std::string> stack;
auto vi = values_.cbegin();
auto oi = ops_.cbegin();
for (auto s : selector_) {
if (!s) {
stack.Push(std::to_string(*(vi++)));
continue;
}
std::string top = stack.top();
stack.pop();
switch (*(oi++)) {
case Op::Add:
stack.top() = buildExpr(stack.top(), '+', top);
break;
case Op::Sub:
stack.top() = buildExpr(stack.top(), '-', top);
break;
case Op::Mul:
stack.top() = buildExpr(stack.top(), '*', top);
break;
case Op::Div:
stack.top() = buildExpr(stack.top(), '/', top);
break;
}
}
return stack.top();
}
public:
Evaluator(const std::vector<Number>& values) :
values_(values),
ops_(values.size() - 1, FirstOp),
selector_(2 * values.size() - 1, 0) {
std::fill(selector_.begin() + values_.size(), selector_.end(), 1);
std::sort(values_.begin(), values_.end());
}
// check for solutions
// 1) we create valid permutations of our selector_ array (eg: "1 1 + 1 +",
// "1 1 1 + +", but skip "1 + 1 1 +" as that cannot be evaluated
// 2) for each evaluation order, we permutate our values
// 3) for each value permutation we check with each combination of
// operators
//
// In the first version I used a local stack in eval() (see toString()) but
// it turned out to be a performance bottleneck, so now I use a cached
// stack. Reusing the stack gives an order of magnitude speed-up (from
// 4.3sec to 0.7sec) due to avoiding repeated allocations. Using
// std::vector as a backing store also gives a slight performance boost
// over the default std::deque.
std::size_t check(Number target, bool singleResult = false) {
Stack<Number> stack;
std::size_t res = 0;
do {
do {
do {
Number value = eval(stack);
if (value == target) {
++res;
std::cout << target << " = " << toString() << "\n";
if (singleResult) {
return res;
}
}
} while (nextOps());
} while (nextValuesPermutation());
} while (nextSelectorPermutation());
return res;
}
};
} // namespace
int main(int argc, const char **argv) {
int i = 1;
bool singleResult = false;
if (argc > 1 && std::string("-s") == argv[1]) {
singleResult = true;
++i;
}
if (argc < i + 2) {
std::cerr << argv[0] << " [-s] <target> <digit>[ <digit>]...\n";
std::exit(1);
}
Number target = std::stoi(argv[i]);
std::vector<Number> values;
while (++i < argc) {
values.Push_back(std::stoi(argv[i]));
}
Evaluator evaluator{values};
std::size_t res = evaluator.check(target, singleResult);
if (!singleResult) {
std::cout << "Number of solutions: " << res << "\n";
}
return 0;
}
入力は明らかに数字と演算子のセットです:D = {1,3,3,6,7,8,3}およびOp = {+、-、*、/}。最も単純なアルゴリズムは ブルートフォース ソルバーであり、これは 列挙 これらのセットのすべての可能な組み合わせです。セットOpの要素は何度でも使用できますが、セットDの要素は1回だけ使用されます。擬似コード:
D={1,3,3,6,7,8,3}
Op={+,-,*,/}
Solution=348
for each permutation D_ of D:
for each binary tree T with D_ as its leafs:
for each sequence of operators Op_ from Op with length |D_|-1:
label each inner tree node with operators from Op_
result = compute T using infix traversal
if result==Solution
return T
return nil
それ以外:jedrus07とHPMの回答を読んでください。
Oxford's Computer Science Docs (with Java Source Code))から素晴らしいアルゴリズムをずっと前に見つけました。そして、このソリューションを読むたびにそれを賞賛します。私はそれを信じています。参考になります。
はるかに簡単なアプローチは、インテリジェントにブルートフォースすることです。 6つの数値と4つの演算子から構築できる式の数は有限であり、単純にそれらすべてを通過します。
幾つ?すべての数値を使用する必要はなく、同じ演算子を複数回使用する可能性があるため、この問題は、「最大6つのリーフと4つの可能なラベルでいくつのラベル付き厳密二分木(別名完全二分木)を作成できるか」と同等です。各非リーフノードについて?」.
N枚の葉を持つ完全な二分木の量はcatalan(n-1)に等しい。これは次のように確認できます。
N個の葉を持つすべての完全な二分木にはn-1個の内部ノードがあり、独自の方法でn-1個のノードを持つ非完全な二分木に対応します(完全な葉からすべての葉を削除して取得します)。たまたまn個のノードを持つcatalan(n)の可能な二分木があるので、n枚の葉を持つ厳密な二分木はcatalan(n-1)の可能な異なる構造を持っていると言えます。
リーフ以外のノードごとに4つの可能な演算子があります。4^(n-1)の可能性リーフにはnで番号を付けることができます。 *(6は(n-1)を選択)さまざまな方法。 (k回発生する数ごとにこれをk!で割るか、すべての数が異なることを確認してください)
したがって、6つの異なる数値と4つの可能な演算子に対して、Sum(n = 1 ... 6)[Catalan(n-1)* 6!/(6-n)! * 4 ^(n-1)]合計33,665,406の可能な式。それほど多くはありません。
これらの木をどのように列挙しますか?
N-1以下のノードを持つすべてのツリーのコレクションが与えられた場合、すべてのn-1ツリーと空のツリー、すべてのn-2ツリーと1ノードのツリー、すべてnを体系的に組み合わせることにより、nノードのすべてのツリーを作成できます。 -3ツリー。2つのノードツリーすべてを含み、新しく形成されたツリーの左右のサブツリーとして使用します。
したがって、空のセットから始めて、最初にルートノードのみを持つツリーを生成し、次に新しいルートからそれを左または右のサブツリーとして使用して、次のような2つのツリーを生成できます:/および。等々。
それらをオンザフライで一連の式に変換し(演算子と数値をループするだけ)、目的の数値が得られるまで、それらを評価します。
Pythonで独自のカウントダウンソルバーを記述しました。
これがコードです。 GitHub でも利用できます。
#!/usr/bin/env python3
import sys
from itertools import combinations, product, Zip_longest
from functools import lru_cache
assert sys.version_info >= (3, 6)
class Solutions:
def __init__(self, numbers):
self.all_numbers = numbers
self.size = len(numbers)
self.all_groups = self.unique_groups()
def unique_groups(self):
all_groups = {}
all_numbers, size = self.all_numbers, self.size
for m in range(1, size+1):
for numbers in combinations(all_numbers, m):
if numbers in all_groups:
continue
all_groups[numbers] = Group(numbers, all_groups)
return all_groups
def walk(self):
for group in self.all_groups.values():
yield from group.calculations
class Group:
def __init__(self, numbers, all_groups):
self.numbers = numbers
self.size = len(numbers)
self.partitions = list(self.partition_into_unique_pairs(all_groups))
self.calculations = list(self.perform_calculations())
def __repr__(self):
return str(self.numbers)
def partition_into_unique_pairs(self, all_groups):
# The pairs are unordered: a pair (a, b) is equivalent to (b, a).
# Therefore, for pairs of equal length only half of all combinations
# need to be generated to obtain all pairs; this is set by the limit.
if self.size == 1:
return
numbers, size = self.numbers, self.size
limits = (self.halfbinom(size, size//2), )
unique_numbers = set()
for m, limit in Zip_longest(range((size+1)//2, size), limits):
for numbers1, numbers2 in self.paired_combinations(numbers, m, limit):
if numbers1 in unique_numbers:
continue
unique_numbers.add(numbers1)
group1, group2 = all_groups[numbers1], all_groups[numbers2]
yield (group1, group2)
def perform_calculations(self):
if self.size == 1:
yield Calculation.singleton(self.numbers[0])
return
for group1, group2 in self.partitions:
for calc1, calc2 in product(group1.calculations, group2.calculations):
yield from Calculation.generate(calc1, calc2)
@classmethod
def paired_combinations(cls, numbers, m, limit):
for cnt, numbers1 in enumerate(combinations(numbers, m), 1):
numbers2 = Tuple(cls.filtering(numbers, numbers1))
yield (numbers1, numbers2)
if cnt == limit:
return
@staticmethod
def filtering(iterable, elements):
# filter elements out of an iterable, return the remaining elements
elems = iter(elements)
k = next(elems, None)
for n in iterable:
if n == k:
k = next(elems, None)
else:
yield n
@staticmethod
@lru_cache()
def halfbinom(n, k):
if n % 2 == 1:
return None
prod = 1
for m, l in Zip(reversed(range(n+1-k, n+1)), range(1, k+1)):
prod = (prod*m)//l
return prod//2
class Calculation:
def __init__(self, expression, result, is_singleton=False):
self.expr = expression
self.result = result
self.is_singleton = is_singleton
def __repr__(self):
return self.expr
@classmethod
def singleton(cls, n):
return cls(f"{n}", n, is_singleton=True)
@classmethod
def generate(cls, calca, calcb):
if calca.result < calcb.result:
calca, calcb = calcb, calca
for result, op in cls.operations(calca.result, calcb.result):
expr1 = f"{calca.expr}" if calca.is_singleton else f"({calca.expr})"
expr2 = f"{calcb.expr}" if calcb.is_singleton else f"({calcb.expr})"
yield cls(f"{expr1} {op} {expr2}", result)
@staticmethod
def operations(x, y):
yield (x + y, '+')
if x > y: # exclude non-positive results
yield (x - y, '-')
if y > 1 and x > 1: # exclude trivial results
yield (x * y, 'x')
if y > 1 and x % y == 0: # exclude trivial and non-integer results
yield (x // y, '/')
def countdown_solver():
# input: target and numbers. If you want to play with more or less than
# 6 numbers, use the second version of 'unsorted_numbers'.
try:
target = int(sys.argv[1])
unsorted_numbers = (int(sys.argv[n+2]) for n in range(6)) # for 6 numbers
# unsorted_numbers = (int(n) for n in sys.argv[2:]) # for any numbers
numbers = Tuple(sorted(unsorted_numbers, reverse=True))
except (IndexError, ValueError):
print("You must provide a target and numbers!")
return
solutions = Solutions(numbers)
smallest_difference = target
bestresults = []
for calculation in solutions.walk():
diff = abs(calculation.result - target)
if diff <= smallest_difference:
if diff < smallest_difference:
bestresults = [calculation]
smallest_difference = diff
else:
bestresults.append(calculation)
output(target, smallest_difference, bestresults)
def output(target, diff, results):
print(f"\nThe closest results differ from {target} by {diff}. They are:\n")
for calculation in results:
print(f"{calculation.result} = {calculation.expr}")
if __name__ == "__main__":
countdown_solver()
アルゴリズムは次のように機能します。
番号は、長さ6のタプルに降順で配置されます。次に、長さが1〜6のすべての一意のサブグループが作成されます。最小のグループが最初に作成されます。
例:(75、50、5、9、1、1)-> {(75)、(50)、(9)、(5)、(1)、(75、50)、(75、9)、 (75、5)、...、(75、50、9、5、1、1)}。
次に、グループは階層ツリーに編成されます。すべてのグループは、空でないサブグループのすべての一意の順序付けられていないペアに分割されます。
例:(9、5、1、1)-> [(9、5、1)+(1)、(9、1、1)+(5)、(5、1、1)+(9)、 (9、5)+(1、1)、(9、1)+(5、1)]。
数値の各グループ内で、計算が実行され、結果が保存されます。長さが1のグループの場合、結果は単に数値自体になります。より大きなグループの場合、計算はサブグループのすべてのペアで実行されます。各ペアで、最初のサブグループのすべての結果が+、-、x、および/を使用して2番目のサブグループのすべての結果と結合され、有効な結果が保存されます。
例:(75、5)はペア((75)、(5))で構成されます。 (75)の結果は75です。 (5)の結果は5です。 (75、5)の結果は[75 + 5 = 80、75-5 = 70、75 * 5 = 375、75/5 = 15]です。
このようにして、最小グループから最大グループまで、すべての結果が生成されます。最後に、アルゴリズムはすべての結果を反復処理し、ターゲット番号に最も近い結果を選択します。
M個の数のグループの場合、算術計算の最大数は次のとおりです。
comps[m] = 4*sum(binom(m, k)*comps[k]*comps[m-k]//(1 + (2*k)//m) for k in range(1, m//2+1))
長さが1から6のすべてのグループの場合、計算の最大総数は次のようになります。
total = sum(binom(n, m)*comps[m] for m in range(1, n+1))
これは1144386です。アルゴリズムは重複グループの結果を再利用し、些細な操作(0の加算、1の乗算など)を無視し、ゲームのルールでは中間結果は次のようになっている必要があるため、実際にははるかに少なくなります。正の整数(除算演算子の使用を制限します)。
まず、問題を厳密に定義する必要があると思います。許可されていることと許可されていないこと。あなたはそれを単純にし、乗算、除算、減算、加算のみを許可することから始めることができます。
これで、問題のある空間の入力のセット、使用可能な操作のセット、および必要な入力がわかりました。 4つの演算とx入力しかない場合、組み合わせの数は次の数より少なくなります。
操作を実行できる順序の数(x!)に、各ステップで可能な操作の選択肢を掛けた数:4 ^ x。 6つの数値でわかるように、2949120の妥当な演算が得られます。これは、これがブルートフォースアルゴリズムの制限になる可能性があることを意味します。
ブルートフォースを取得し、それが機能することがわかったら、何らかのヒューリスティック関数を定義する必要がある A *アルゴリズム でアルゴリズムを改善し始めることができます。
私の意見では、それについて考える最良の方法は探索問題としてです。主な難しさは、優れたヒューリスティック、または問題スペースを減らす方法を見つけることです(答えに合わない数がある場合は、少なくとも1つの乗算などが必要になります)。小さなことから始めて、それに基づいて、コードができたらフォローアップの質問をします。
私は少し簡単なバージョンを書きました: