web-dev-qa-db-ja.com

カウントダウンスタイルの数学数パズルを計算するアルゴリズムを設計する方法

私はいつもこれをやりたいと思っていましたが、問題について考え始めるたびに、その指数関数的な性質のために頭がおかしくなります。

私が理解できるようにしたい問題ソルバーとコードは、カウントダウン数学の問題用です:

与えられた数X1からX5のセットは、数学演算を使用してそれらを組み合わせてYを作成する方法を計算します。乗算、除算、加算、および減算を適用できます。

では、1,3,7,6,8,3作成348

回答:(((8 * 7) + 3) -1) *6 = 348

この問題を解決できるアルゴリズムを作成するにはどうすればよいですか?このような問題を解決しようとするとき、どこから始めますか?このようなアルゴリズムを設計する際に考慮すべき重要な考慮事項は何ですか?

27
drlobo

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()関数再帰の各レベルで処理する演算を順序付けるのに役立ちます。

このようなヒューリスティック関数の例は、たとえば、中間結果と合計ターゲット結果の違いです。

ただし、この方法では、最良の場合と平均的な場合の複雑さのみが改善されます。最悪の場合の複雑さはそのままです。

最悪の場合の複雑さは、ある種の分岐切断によって改善できます。この場合、それが可能かどうかはわかりません。

6
Ondrej Bozek

確かにそれは指数関数的ですが、それは小さいので、良い(十分な)素朴な実装は良いスタートになるでしょう。通常のインフィックス表記をブラケットで削除し、ポストフィックスを使用することをお勧めします。プログラミングが簡単です。出力はいつでも別のステージとしてきれいにすることができます。

数値と演算子のすべての(有効な)シーケンスをリストして評価することから始めます。例(postfix):

1 3 7 6 8 3 + + + + + -> 28
1 3 7 6 8 3 + + + + - -> 26

私のJavaは笑える、笑うためにここに来ることはないので、これのコーディングはあなたに任せます。

これを読んでいるすべての賢い人々にとって:はい、私はこのような小さな問題でもより速くなる可能性が高いよりスマートなアプローチがあることを知っています。他の誰かがよりスマートなソリューションで答えを書くことができます。

だから、あなたの質問に答えるために:

  • 私は、すぐに実用的な解決策につながると思うアルゴリズムから始めます。この場合、(私にとって)明らかな選択は、考えられるすべての計算の徹底的な列挙とテストです。
  • 明らかなアルゴリズムがパフォーマンス上の理由で魅力がないように見える場合は、より深く考え始め、パフォーマンスが向上する可能性が高いとわかっている他のアルゴリズムを思い出します。代わりに、最初にそれらの1つをコーディングします。
  • 徹底的なアルゴリズムに固執し、実行時間が実際には長すぎるとわかった場合は、前のステップに戻って再度コードを作成する可能性があります。しかし、それはしばらくの間価値がある必要があります。費用便益評価を行う必要があります。私のコードがレイチェル・ライリーを上回ることができる限り、私は満足するでしょう。
  • 重要な考慮事項には、私の時間vsコンピューターの時間が含まれます。私の時間は、はるかに多くの費用がかかります。

以下の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;
}
6
mitchnull

入力は明らかに数字と演算子のセットです: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の回答を読んでください。

5
Arne

Oxford's Computer Science Docs (with Java Source Code))から素晴らしいアルゴリズムをずっと前に見つけました。そして、このソリューションを読むたびにそれを賞賛します。私はそれを信じています。参考になります。

0
tutal

はるかに簡単なアプローチは、インテリジェントにブルートフォースすることです。 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つのツリーを生成できます:/および。等々。

それらをオンザフライで一連の式に変換し(演算子と数値をループするだけ)、目的の数値が得られるまで、それらを評価します。

0
Angelo Wentzler

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()

アルゴリズムは次のように機能します。

  1. 番号は、長さ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)}。

  2. 次に、グループは階層ツリーに編成されます。すべてのグループは、空でないサブグループのすべての一意の順序付けられていないペアに分割されます。

    例:(9、5、1、1)-> [(9、5、1)+(1)、(9、1、1)+(5)、(5、1、1)+(9)、 (9、5)+(1、1)、(9、1)+(5、1)]。

  3. 数値の各グループ内で、計算が実行され、結果が保存されます。長さが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]です。

  4. このようにして、最小グループから最大グループまで、すべての結果が生成されます。最後に、アルゴリズムはすべての結果を反復処理し、ターゲット番号に最も近い結果を選択します。

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の乗算など)を無視し、ゲームのルールでは中間結果は次のようになっている必要があるため、実際にははるかに少なくなります。正の整数(除算演算子の使用を制限します)。

0
Pulsar

まず、問題を厳密に定義する必要があると思います。許可されていることと許可されていないこと。あなたはそれを単純にし、乗算、除算、減算、加算のみを許可することから始めることができます。

これで、問題のある空間の入力のセット、使用可能な操作のセット、および必要な入力がわかりました。 4つの演算とx入力しかない場合、組み合わせの数は次の数より少なくなります。

操作を実行できる順序の数(x!)に、各ステップで可能な操作の選択肢を掛けた数:4 ^ x。 6つの数値でわかるように、2949120の妥当な演算が得られます。これは、これがブルートフォースアルゴリズムの制限になる可能性があることを意味します。

ブルートフォースを取得し、それが機能することがわかったら、何らかのヒューリスティック関数を定義する必要がある A *アルゴリズム でアルゴリズムを改善し始めることができます。

私の意見では、それについて考える最良の方法は探索問題としてです。主な難しさは、優れたヒューリスティック、または問題スペースを減らす方法を見つけることです(答えに合わない数がある場合は、少なくとも1つの乗算などが必要になります)。小さなことから始めて、それに基づいて、コードができたらフォローアップの質問をします。

0
bjedrzejewski

私は少し簡単なバージョンを書きました:

  1. リストの2つの(個別の)要素のすべての組み合わせについて、+、-、*、/を使用してそれらを組み合わせます(a> bなので、a-bのみが必要で、a%b = 0の場合はa/bのみです)
  2. 組み合わせがターゲットの場合、ソリューションを記録します
  3. 削減されたリストを再帰的に呼び出す
0
Alan Swindells