我一直想做这件事,但每次开始思考这个问题时,由于其成倍增长的本质让我感到很困惑。
我想要理解和编写的问题的解决方案是倒计时数学问题:
给定数字X1到X5的集合,计算它们如何使用数学运算符组合以得出Y。可以应用乘法,除法,加法和减法。
那么,如何使用1,3,7,6,8,3
得出348
?
答案:(((8 * 7) + 3) -1) *6 = 348
。
如何编写一个能够解决这个问题的算法?当尝试解决这样的问题时,从哪里开始?在设计这样的算法时,需要考虑哪些重要因素?
我一直想做这件事,但每次开始思考这个问题时,由于其成倍增长的本质让我感到很困惑。
我想要理解和编写的问题的解决方案是倒计时数学问题:
给定数字X1到X5的集合,计算它们如何使用数学运算符组合以得出Y。可以应用乘法,除法,加法和减法。
那么,如何使用1,3,7,6,8,3
得出348
?
答案:(((8 * 7) + 3) -1) *6 = 348
。
如何编写一个能够解决这个问题的算法?当尝试解决这样的问题时,从哪里开始?在设计这样的算法时,需要考虑哪些重要因素?
N
个输入数字,则会使用(N-1)
个运算符,因为每个运算符都是二进制的。selector_
数组)。有效排列是指可以在不出现堆栈下溢的情况下进行评估,并且以恰好一个值(结果)结束的排列。因此,1 1 +
是有效的,但1 + 1
则不是。values_
数组)和每个运算符的组合(ops_
数组)来测试每个这样的操作数-运算符排列。匹配结果将被漂亮地打印出来。[-s] <target> <digit>[ <digit>...]
。使用-s
开关可以防止进行详尽搜索,只会打印第一个匹配结果。
(使用./mathpuzzle 348 1 3 7 6 8 3
可以获得原问题的答案)
此解决方案不允许将输入数字连接起来形成数字。这可以作为额外的外部循环添加。
工作代码可从此处下载。(注意:我已更新该代码以支持将输入数字连接起来形成解决方案)
有关更多说明,请参见代码注释。
#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;
}
当然,这是指数级的,但非常小,因此一个足够好的天真实现将是一个很好的开始。我建议您放弃使用带括号的通常中缀符号表示法,而使用后缀表示法,这样更容易编程。您始终可以将输出美化为单独的阶段。
首先列出并评估所有(有效的)数字和运算符序列。例如(以后缀表示法):
1 3 7 6 8 3 + + + + + -> 28
1 3 7 6 8 3 + + + + - -> 26
我的Java水平有限,我不想被嘲笑,所以我将这个编码工作留给你。
对于所有聪明的读者:是的,我知道即使像这样的小问题也有更聪明的方法,这些方法很可能更快,我只是为了指导OP找到一个初始的工作解决方案。其他人可以使用更聪明的解决方案编写答案。
所以,回答您的问题:
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()
函数递归级别中的数字或(和)运算。
这样的启发式函数的一个例子是中间结果与目标总结果之间的差异。
然而,这只能改善最好情况和平均情况下的复杂度。最坏情况复杂度保持不变。
可以通过某种分支剪枝来改善最坏情况的复杂性。我不确定在这种情况下是否可能。
List<Integer> runList = new ArrayList<>(list);
你需要使用Java 7 或者用经典的Java语法替换钻石符号。 - Ondrej Bozek382 = ((7 * ((3 + 8) * (6 - 1))) - 3)
。 - mitchnull(((8 * 7) + 3) -1) *6 = 348
,对吗?你能确认我们也可以使用括号吗?如果是这样,我可以扩展我的解决方案。 - Ondrej Bozek输入显然是数字和运算符的集合:D={1,3,3,6,7,8,3} 和 Op={+,-,*,/}。最简单的算法是暴力破解求解器,它枚举这些集合的所有可能组合。其中,Op集合的元素可以随意使用,但D集合的元素仅能使用一次。伪代码:
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
如何枚举这些树?
给定一个包含n-1个或更少节点的所有树的集合,您可以通过将所有n-1棵树与空树配对,所有n-2棵树与1个节点树配对,所有n-3棵树与所有2个节点树等等,以此作为新形成的树的左右子树来创建所有具有n个节点的树。
因此,从一个空集开始,首先生成只有根节点的树,然后从新根开始,您可以将其用作左侧或右侧子树,从而产生两个看起来像这样的树:/ 和 . 以此类推。
您可以即时将它们转换为一组表达式(只需循环运算符和数字),并在进行计算时进行评估,直到其中一个产生目标数字。
#!/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和/将第一个子组的所有结果与第二个子组的所有结果组合,并存储有效的结果。
例如:(75, 5)由((75),(5))组成。(75)的结果为75;(5)的结果为5;(75, 5)的结果为[75+5=80,75-5=70,75*5=375,75/5=15]。
以这种方式生成所有结果,从最小的组到最大的组。最后,算法遍历所有结果,并选择与目标数字最接近的结果。
comps[m] = 4*sum(binom(m, k)*comps[k]*comps[m-k]//(1 + (2*k)//m) for k in range(1, m//2+1))
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*算法,这将要求你定义启发式函数。
在我看来,最好的思考方式是将其视为搜索问题。主要困难将是找到良好的启发式方法,或者减少问题空间的方法(如果你有不能相加得到答案的数字,你将需要至少一个乘法等)。从小处开始,逐步构建,并在你有一些代码后提出跟进问题。
我写了一个稍微简化的版本:
import sys
def driver():
try:
target = int(sys.argv[1])
nums = list((int(sys.argv[i+2]) for i in range(6)))
except (IndexError, ValueError):
print("Provide a list of 7 numbers")
return
solutions = list()
solve(target, nums, list(), solutions)
unique = set()
final = list()
for s in solutions:
a = '-'.join(sorted(s))
if not a in unique:
unique.add(a)
final.append(s)
for s in final: #print them out
print(s)
def solve(target, nums, path, solutions):
if len(nums) == 1:
return
distinct = sorted(list(set(nums)), reverse = True)
rem1 = list(distinct)
for n1 in distinct: #reduce list by combining a pair
rem1.remove(n1)
for n2 in rem1:
rem2 = list(nums) # in case of duplicates we need to start with full list and take out the n1,n2 pair of elements
rem2.remove(n1)
rem2.remove(n2)
combine(target, solutions, path, rem2, n1, n2, '+')
combine(target, solutions, path, rem2, n1, n2, '-')
if n2 > 1:
combine(target, solutions, path, rem2, n1, n2, '*')
if not n1 % n2:
combine(target, solutions, path, rem2, n1, n2, '//')
def combine(target, solutions, path, rem2, n1, n2, symb):
lst = list(rem2)
ans = eval("{0}{2}{1}".format(n1, n2, symb))
newpath = path + ["{0}{3}{1}={2}".format(n1, n2, ans, symb[0])]
if ans == target:
solutions += [newpath]
else:
lst.append(ans)
solve(target, lst, newpath, solutions)
if __name__ == "__main__":
driver()