【问题标题】:Reduce time complexity of a program (in Java)?降低程序的时间复杂度(在 Java 中)?
【发布时间】:2013-12-22 21:32:31
【问题描述】:

这个问题是一个很长的问题。这可能需要很长时间,所以如果你没有时间,我理解。

首先让我解释一下我想要实现的目标: 我和一些朋友玩这个数学游戏,我们从可能的数字池中得到 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;
           ...

    }
  }
}

我为第一个元素评估所有可能的操作以及该状态的所有剩余数字。然后我用一个自写的 equals 检查它是否已经在状态数组列表中(它作为一个队列,但顺序并不重要)。如果它不存在,那么状态将被添加到列表中,然后我对其他元素执行相同的操作。之后,我丢弃该状态并从不断增长的列表中选择另一个。

列表在 10 分钟内增长到 80k 个状态,并且增长速度越来越慢。那是因为与我想添加新状态时相比,要比较的状态数量越来越多。这让我想知道与其他州进行比较以防止重复是否是个好主意。

完成这个计划并不是那么重要,但我希望将其视为一次学习经历。我不是要求任何人为我编写代码,但是非常感谢您对我可以更好地处理的内容提出友好的建议。这意味着,如果您想提及该计划的另一个方面,请提出。我不确定在这个论坛上是否要求太多,因为大多数主题都处理程序的特定部分。虽然我的问题也很具体,但原因可能很多。

编辑:我不是试图找到最快的单一解决方案,而是每个解决方案。因此,如果我找到解决方案,我的程序将不会停止。但是,它会尝试忽略双打,例如: ((4+5)7) 和 (7(5+4))。只接受两者之一,因为equals方法加法和乘法不关心操作数的位置。

【问题讨论】:

标签: java complexity-theory


【解决方案1】:

使用递归(即深度优先搜索)可能会更容易编写,因为这将简化中间状态的簿记。

如果您想保持呼吸优先的方法,请确保状态列表支持有效删除第一个元素,即使用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));
                                                                                                            }
                                                                                                        }
                    }

根据要求,每个解决方案只报告一次(如果两个解决方案的中间结果集相等,则认为两个解决方案相等)。根据维基百科的描述,并非所有数字都需要使用。但是,存在一个小错误,即此类解决方案可能会被多次报告。

【讨论】:

  • 如果你这样说,我想每次检查我的arraylist是否包含一个元素是最耗时的操作。我的电脑有一个 2.5GHz 的 cpu,10 分钟后还远远没有完成。我将尝试使用一个集合并首先应用深度以防止列表增长太多。随时欢迎您发布解决方案!这确实表明我缺乏技能,因为我已经为此工作了将近一周。如果你把它贴在这里,你能用扰流板标签覆盖它吗?我想把它作为最后的手段或与我所拥有的进行比较。
  • 阅读维基百科页面,我注意到可以选择运算符的评估顺序。我已经更新了性能估计来解决这个问题。我还发布了一些代码:-)
  • 非常感谢!我尚未检查您的解决方案,因为我仍在研究我的解决方案。还感谢您为自学技术提供了适当的术语。事实上,虽然所有数字只能使用一次,但并非所有数字都是解决方案所必需的。对于我的 OP 中缺乏细节,我深表歉意。 (现在正在编辑)
【解决方案2】:

您所做的基本上是广度优先搜索解决方案。这也是我看到问题时最初的想法,不过还是补充几点。

首先,您对ArrayList 所做的主要工作是从中删除元素并测试元素是否已经存在。由于您的范围很小,我将使用单独的HashSetBitSet 进行第二次操作。

其次,更重要的是,您还可以将最终状态添加到初始点,并向后搜索。由于您的所有运算都有逆运算(加法和减法、乘法和除法),因此您可以这样做。使用上面的Set 想法,您可以有效地将需要访问的状态数量减半(这个技巧被称为中间相遇)。

其他小事是:

  • 除非得到的数字是整数,否则不要除法
  • 不要将超出范围的数字(例如 >999)添加到您的集合/队列中

状态总数为 999(1 到 999 之间的整数个数),因此您不应该在这里遇到性能问题。我认为您最大的消耗是您正在测试包含在 ArrayList 中,即 O(n)。

希望这会有所帮助!

编辑:刚刚注意到这一点。你说你检查一个号码是否已经在列表中,然后删除它。如果你删除它,你很有可能会再次添加它。使用单独的数据结构(Set 在这里完美运行)来存储您访问过的状态,您应该没问题。

编辑 2:根据其他答案和 cmets(感谢 @kutschkem 和 @meriton),正确的 Queue 更适合弹出元素(ArrayList 的常数与线性)。在这种情况下,您的状态太少而无法引起注意,但在执行 BFS 时请使用 LinkedListArrayDeque


更新了解决倒计时的答案

对不起,我之前的误解。要解决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)

如果您在任何时候找到解决方案,请检查您之前是否没有找到它,如果没有,请添加它。

抱歉之前这么密集。我希望这更重要。

【讨论】:

  • 另外,当你删除很多时,ArrayList 不是一个好的选择,LinkedList 在这种情况下实际上可能会更好。
  • @Sebii 非常感谢!这里有好主意。我觉得很愚蠢,以至于我没有想到一个集合来跟踪访问状态。我确实应该使用集合而不是数组列表,因为我不允许重复。我很难理解你为什么认为只有 999 个州。虽然如果我理解正确的话,最好先让剩余数量较少的状态消失,以减少状态的数量。最后一件事:我在 OP 中并不清楚这一点,但我不想找到最快的解决方案,而是所有解决方案。我将在 OP 中对此进行编辑。
  • @Babyburger 所有解决方案都会稍微改变解决方案。一件事是有无限多的解决方案。因为如果 a1 'op' a2 ... 'op' an 是一个解,那么 a1 'op' a2 ... 'op' an + a1 - a1 (或 * a1 / a1)也是一个解。这或许可以解释为什么需要这么长时间。至于为什么你只有 999 个州,我假设你只想找到一个解决方案,在这种情况下,访问一个数字两次对你没有帮助。
  • @Sebii 但是我最初只有 6 个整数,而不是 999。我只提到这些整数只能取值 1..10,25,50,75,100 并且一旦选择,在游戏的剩余时间里,它们不会改变。这些是您用来找到目标的 6 个数字(一个介于 100-900 之间的数字,一旦确定也不会改变)。所以答案肯定是有限的。
  • @Babyburger 你确定吗?也许我又误会了(对不起),但是假设您最初选择了1、2、3、4、5、6并想找到200。您肯定可以找到解决方案(5 + 5 + ... + 5 ),但是为什么 (5 * 40) + 1 - 1 也不是解决方案呢?如果那是一, (5 * 40) + 1 - 1 + 1 - 1 也是一,你可以永远继续下去。
【解决方案3】:

您的问题类似于硬币找零问题。首先做所有的减法组合,这样你就可以得到你的“单位面额硬币”,它应该是所有的减法和加法,以及给你的正常数字。然后使用更改算法来获得您想要的数字。由于我们事先进行了减法,因此结果可能并不完全符合您的要求,但它应该很接近并且比您正在做的要快得多。

假设我们有 6 个数字作为集合 S = {1, 5, 10, 25, 50, 75, 100}。然后我们进行所有减法和加法的组合并将它们添加到 S 即 {-99, -95, -90,..., 1, 5, 10,..., 101, 105,...}。现在我们使用以 S 的元素为面额的硬币找零算法。如果我们没有得到解决方案,那么它是无法解决的。

解决硬币找零问题的方法有很多,这里只讨论几种: AlgorithmBasics-examples.pdf

【讨论】:

    猜你喜欢
    • 2020-09-15
    • 1970-01-01
    • 2016-01-08
    • 2015-12-01
    • 2011-06-15
    • 1970-01-01
    • 1970-01-01
    • 2012-07-28
    • 2016-10-07
    相关资源
    最近更新 更多