这个问题相当长。这可能需要很长时间,所以如果你没有我理解的时间。
首先让我解释一下我想要实现的目标: 我和一些朋友玩这个数学游戏,我们从一组可能的数字中得到6个随机数:1到10,25,50,75和100.从中选择6个数字,不允许重复。然后将在[100,999]的范围内选择目标号码。使用上述6个数字,我们只能使用基本操作(加法,减法,乘法和除法)来达到目标。只允许使用整数,并且不需要所有6个整数来达到解决方案。
一个例子:我们从数字4,8,6,9,25,100开始,需要找到328。 一个可能的解决方案是:((4 x 100) - (9 x 8))= 400 - 72 = 328.这样,我只使用了6个初始数中的4个而没有使用过两次。这是一个有效的解决方案。
我们并不总能找到自己的解决方案,这就是为什么我认为一个程序会很有用。我编写了一个程序(用Java编写),它已经过几次测试,并且已经有效了。它并不总是提供所有可能的解决方案,但它在自己的限制范围内工作。现在我已经尝试扩展它,所以所有的解决方案都会显示出来。
关于主要问题: 我试图执行的程序运行时间非常长。就像在,我会让它运行15分钟,它看起来不像是在任何接近完成。所以我想到了它,选项确实是无穷无尽的。我从6个数字开始,我将第一个与其他5个进行比较,然后将第二个与其他5个进行比较,依此类推,直到我完成了6次(每次比较我与每个运算符进行比较,再次进行4次)。在最初的6个数字的单个状态中,我现在有5次6次4 = 120个状态(每个5个数字)。所有这些都必须经历相同的仪式,所以难怪它花了这么长时间。
该程序实际上太大了,无法在此列出,因此我会将其上传给感兴趣的人: http://www.speedyshare.com/ksT43/MathGame3.jar (单击下载旁边的MathGame3.jar标题)
以下是发生的事情的总体概述:
-6 integers + goal number are initialized
-I use the class StateNumbers that are acting as game states
-> in this class the remaining numbers (initially the 6 starting numbers)
are kept as well as the evaluated expressions, for printing purposes
此方法是主要操作发生的地方:
StateNumbers stateInProcess = getStates().remove(0);
ArrayList<Integer> remainingNumbers = stateInProcess.getRemainingNumbers();
for(int j = 0; j < remainingNumbers.size(); j++){
for(int i = 0; i < remainingNumbers.size(); i++){
for(Operator op : Operator.values()){ // Looping over different operators
if(i == j) continue;
...
}
}
}
我为第一个元素评估所有可能的操作以及该状态的所有剩余数字。然后我用自己写的等于检查它是否已经在状态的arraylist中(它充当队列,但顺序并不重要)。如果它不在那里,那么状态将被添加到列表中,然后我对其他元素也这样做。在那之后,我放弃了状态并从不断增长的列表中选择另一个。
该列表在10分钟内增加到8万个状态,并且变得越来越慢。那是因为当我想要添加新状态时,要比较的状态数量越来越多。这让我想知道是否与其他州进行比较以防止重复是一个好主意。
这个项目的完成并不是那么重要,但我希望将其视为一种学习体验。我不是要求任何人为我编写代码,但是对我能够处理得更好的友好建议将非常感激。这意味着如果您想要提及有关该计划另一方面的内容,请执行此操作。由于大多数主题处理程序的特定部分,我不确定这个论坛上是否要求太多。虽然我的问题也是具体的,但原因可能很多。
编辑:我不是要找到最快的单一解决方案,而是每个解决方案。因此,如果我找到解决方案,我的程序将不会停止。然而,它会尝试忽略双打,如: ((4 + 5) 7)和(7 (5 + 4))。只接受两个中的一个,因为加法和乘法的equals方法不关心操作数的定位。答案 0 :(得分:3)
您所做的基本上是广度优先搜索解决方案。当我看到问题时,这也是我最初的想法,但我会添加一些内容。
首先,您对ArrayList
执行的主要操作是从中删除元素,并测试元素是否已存在。由于您的范围较小,我会使用单独的HashSet
或BitSet
进行第二次操作。
其次,更重要的是,您还可以将最终状态添加到初始点,然后向后搜索。由于您的所有操作都具有反转(加法和减法,乘法和除法),因此您可以执行此操作。根据上面Set
的想法,您可以有效地将需要访问的状态数减半(这个技巧称为“在中间相遇”)。
其他小事情将是:
状态总数为999(1到999之间的整数个数),因此您不应该在这里遇到性能问题。我认为你最大的消耗是你正在测试包含在ArrayList
中的O(n)。
希望这有帮助!
编辑:刚注意到这一点。你说你检查一个数字是否已经在列表中,但然后删除它。如果你删除它,你很有可能再次添加它。使用单独的数据结构(Set
在这里工作完美)来存储您访问过的状态,您应该没问题。
编辑2:根据其他答案和评论(感谢@kutschkem和@meriton),正确的Queue
更适合弹出元素(ArrayList
的常量与线性)。在这种情况下,您的状态太少而无法显着,但在执行BFS时使用LinkedList
或ArrayDeque
。
对不起我之前的误解。要解决countdown,您可以执行以下操作:
假设你的6个初始数字是 a1,a2,...,a6 ,你的目标数字是T.你想检查是否有办法分配运算符 o1, o2,...,o5 这样
a1 o1 a2 ... o5 a6 = T
有5个运算符,每个运算符可以取4个值中的一个,因此有4 ^ 5 = 2 ^ 10种可能性。您可以使用少于整个6,但如果您递归地构建解决方案,您将在最后检查所有这些(稍后将详细介绍)。 6个初始数字也可以在6中进行置换! = 720种方式,这导致2 ^ 10 * 6的解决方案总数!这大概是720,000。
由于这个很小,我要做的是遍历最初的6个数字的每个排列,并尝试递归地分配运算符。为此,定义一个函数
void solve(int result, int index, List<Integer> permutation)
其中result
是到目前为止的计算值,index
是置换列表中的索引。然后循环遍历每个操作员并调用
solve(result op permutation.get(index), index + 1, permutation)
如果您在任何时候找到了解决方案,请检查以前是否找不到它,如果没有,请添加。
道歉之前如此密集。我希望这更加重要。
答案 1 :(得分:3)
使用递归(即深度优先搜索)来编写它可能会更容易,因为这会简化中间状态的簿记。
如果您想保持呼吸优先方法,请确保状态列表支持有效删除第一个元素,即使用java.util.Queue
等java.util.ArrayDeque
。我之所以提到这一点,是因为最常用的List
实现(即java.util.ArrayList
)需要复制其全部内容以删除第一个元素,这使得如果列表很大,则删除第一个元素非常昂贵。
120个州(每个5个数字)。所有这些都必须经历相同的仪式,所以难怪它花了这么长时间。
实际上,它会非常令人惊讶。毕竟,2GHz CPU每秒执行20亿个时钟周期。即使检查状态需要多达100个时钟周期,这仍然意味着每秒2000万个状态!
另一方面,如果我正确理解游戏规则,候选解决方案的集合由6个数字(其中有6个!= 720)的所有顺序给出,其中4个运算符中的一个中间有5个空格,以及运营商定义的评估顺序。也就是说,我们共有6个! * 4 ^ 5 * 5! = 88 473 600候选解决方案,因此处理应在几秒钟内完成。
PS:完整的解决方案可能不会非常耗时,所以如果你愿意,我也可以发布代码 - 我只是不想破坏你的学习经验。更新:我已经编写了代码。它比我想象的要难,因为找到所有解决方案的要求意味着我们需要在不展开堆栈的情况下打印解决方案。因此,我保留了每个州的历史。经过测试,我对性能不太满意(大约10秒),所以我加了memoization,即每组数字只处理一次。随着运行时间下降到大约3秒。
由于Stackoverflow没有扰流标记,因此我增加了缩进,因此您必须向右滚动才能看到任何内容:-)
package katas.countdown;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
enum Operator {
plus("+", true),
minus("-", false),
multiply("*", true),
divide("/", false);
final String sign;
final boolean commutes;
Operator(String sign, boolean commutes) {
this.sign = sign;
this.commutes = commutes;
}
int apply(int left, int right) {
switch (this) {
case plus:
return left + right;
case minus:
return left - right;
case multiply:
return left * right;
case divide:
int mod = left % right;
if (mod == 0) {
return left / right;
} else {
throw new ArithmeticException();
}
}
throw new AssertionError(this);
}
@Override
public String toString() {
return sign;
}
}
class Expression implements Comparable<Expression> {
final int value;
Expression(int value) {
this.value = value;
}
@Override
public int compareTo(Expression o) {
return value - o.value;
}
@Override
public int hashCode() {
return value;
}
@Override
public boolean equals(Object obj) {
return value == ((Expression) obj).value;
}
@Override
public String toString() {
return Integer.toString(value);
}
}
class OperationExpression extends Expression {
final Expression left;
final Operator operator;
final Expression right;
OperationExpression(Expression left, Operator operator, Expression right) {
super(operator.apply(left.value, right.value));
this.left = left;
this.operator = operator;
this.right = right;
}
@Override
public String toString() {
return "(" + left + " " + operator + " " + right + ")";
}
}
class State {
final Expression[] expressions;
State(int... numbers) {
expressions = new Expression[numbers.length];
for (int i = 0; i < numbers.length; i++) {
expressions[i] = new Expression(numbers[i]);
}
}
private State(Expression[] expressions) {
this.expressions = expressions;
}
/**
* @return a new state constructed by removing indices i and j, and adding expr instead
*/
State replace(int i, int j, Expression expr) {
Expression[] exprs = Arrays.copyOf(expressions, expressions.length - 1);
if (i < exprs.length) {
exprs[i] = expr;
if (j < exprs.length) {
exprs[j] = expressions[exprs.length];
}
} else {
exprs[j] = expr;
}
Arrays.sort(exprs);
return new State(exprs);
}
@Override
public boolean equals(Object obj) {
return Arrays.equals(expressions, ((State) obj).expressions);
}
public int hashCode() {
return Arrays.hashCode(expressions);
}
}
public class Solver {
final int goal;
Set<State> visited = new HashSet<>();
public Solver(int goal) {
this.goal = goal;
}
public void solve(State s) {
if (s.expressions.length > 1 && !visited.contains(s)) {
visited.add(s);
for (int i = 0; i < s.expressions.length; i++) {
for (int j = 0; j < s.expressions.length; j++) {
if (i != j) {
Expression left = s.expressions[i];
Expression right = s.expressions[j];
for (Operator op : Operator.values()) {
if (op.commutes && i > j) {
// no need to evaluate the same branch twice
continue;
}
try {
Expression expr = new OperationExpression(left, op, right);
if (expr.value == goal) {
System.out.println(expr);
} else {
solve(s.replace(i, j, expr));
}
} catch (ArithmeticException e) {
continue;
}
}
}
}
}
}
}
public static void main(String[] args) {
new Solver(812).solve(new State(75, 50, 2, 3, 8, 7));
}
}
}
根据要求,每个解决方案仅报告一次(如果两个解决方案的中间结果集合相同,则认为两个解决方案相同)。根据维基百科的描述,并非所有数字都需要使用。但是,还有一个小错误,可能会多次报告此类解决方案。
答案 2 :(得分:1)
您的问题类似于硬币更改问题。首先进行所有减法组合,这样你就可以拥有你的“单位面额硬币”,这应该是所有的减法和加法,以及你给出的正常数字。然后使用更改制作算法来获得所需的数字。由于我们事先做过减法,结果可能不是你想要的,但它应该比你正在做的更接近和快得多。
假设我们给出6个数字作为集合S = {1,5,10,25,50,75,100}。然后我们进行减法和加法的所有组合,并将它们加到S,即{-99,-95,-90,...,1,5,10,......,101,105,...}。现在我们使用硬币更改算法,以S的元素作为面额。如果我们没有得到解决方案,那么就无法解决。
有很多方法可以解决硬币变化问题,这里有一些讨论: AlgorithmBasics-examples.pdf