【问题标题】:Can brute force algorithms scale?蛮力算法可以扩展吗?
【发布时间】:2011-11-08 02:20:45
【问题描述】:

我有一个数学问题,我通过反复试验解决了(我认为这称为蛮力),当有几个选项时,程序运行良好,但随着我添加更多变量/数据,它需要的时间越来越长运行。

我的问题是,虽然原型有效,但它对数千个变量和大型数据集很有用;所以,我想知道是否可以扩展蛮力算法。如何进行缩放?

我开始学习和玩Hadoop(和HBase);虽然看起来很有希望,但我想验证我正在尝试做的事情并非不可能。

如果有帮助,我会用 Java 编写程序(如果可能的话也可以使用),但最后还是将它移植到 Python,因为我觉得它更舒服。

更新:为了提供更多见解,我想我会添加一个简化版本的代码来了解这个想法。基本上,如果我知道总和是 100,我会尝试找到可以等于它的变量的所有组合。这很简单,在我的版本中,我可能会使用更大的数字和更多的变量。这是丢番图,我相信没有算法可以在没有蛮力的情况下解决它。

int sum = 100;
int a1 = 20;
int a2 = 5;
int a3 = 10;
for (int i = 0; i * a1 <= sum; i++) {
    for (int j = 0; i * a1 + j * a2 <= sum; j++) {
        for (int k = 0; i * a1 + j * a2 + k * a3 <= sum; k++) {
            if (i * a1 + j * a2 + k * a3 == sum) {
              System.out.println(i + "," + j + "," + k);
            }
        }
    }   
}

我是编程新手,如果我没有正确地提出这个问题,我很抱歉。这是一个比较笼统的问题。

【问题讨论】:

  • 暴力破解算法的扩展性通常很差,你暴力破解了哪个问题?
  • 嗨,bjarneh..我正在尝试对丢番图方程进行处理。我在问题中添加了一些代码。我认为不存在解决它的算法。

标签: algorithm hadoop scalability


【解决方案1】:

通常,您可以通过使用大 O 表示法分析算法的增长率来量化算法的扩展能力。当您说您的算法通过“蛮力”运行时,尚不清楚它将扩展到何种程度。如果您的“蛮力”解决方案通过列出一组数据的所有可能子集或组合来工作,那么它几乎肯定不会扩展(它将具有渐近复杂度 O(2n) 或 O(n !), 分别)。如果您的蛮力解决方案通过查找所有元素对并检查每个元素来工作,它可能会很好地扩展 (O(n2))。但是,如果没有关于您的算法如何工作的更多信息,就很难说。

您可能希望以this excellent post about big-O 为起点,了解如何推断您的程序的长期可扩展性。通常来说,任何增长率为 O(n log n)、O(n)、O(log n) 或 O(1) 的东西都可以很好地扩展,任何增长率为 O(n2 ) 或 O(n3) 将扩大到一个点,任何增长率为 O(2n) 或更高的东西都不会扩大。

另一种选择是查找您要解决的问题,以了解它的研究程度。众所周知,有些问题有很好的解决方案,如果您的问题是其中之一,那么可能值得看看其他人的想法。也许有一个非常干净、非暴力的解决方案可以很好地扩展!其他一些问题被推测为根本没有可扩展的算法(所谓的NP-hard problems)。如果是这种情况,那么您应该非常确信没有办法获得可扩展的方法。

最后,您可以随时在 Stack Overflow 上提出一个新问题,描述您正在尝试做什么并征求意见。也许社区可以比您最初预期的更有效地帮助您解决问题!

编辑: 鉴于您要解决的问题的描述,现在您正在为每个变量执行一个 for 循环,从 0 到您尝试定位的数字。该算法的复杂度为 O(Uk),其中 k 是变量的数量,U 是总和。这种方法根本不会很好地扩展。在上述情况下引入每个新变量会使算法运行速度慢 100 倍,如果你想要 100 个变量,它肯定不会很好地扩展!

但是,我认为有一个相当好的算法,它的运行时间为 O(U2k),它使用 O(Uk) 内存来解决问题。直觉如下:假设我们想将1、2、4相加得到10。有很多方法可以做到:

2 * 4 +  1 * 2 +  0 * 1
2 * 4 +  0 * 2 +  2 * 1
1 * 4 +  3 * 2 +  0 * 1
1 * 4 +  2 * 2 +  2 * 1
1 * 4 +  1 * 2 +  4 * 1
1 * 4 +  0 * 2 +  6 * 1
0 * 4 +  5 * 2 +  0 * 1
0 * 4 +  4 * 2 +  2 * 1
0 * 4 +  3 * 2 +  4 * 1
0 * 4 +  2 * 2 +  6 * 1
0 * 4 +  1 * 2 +  8 * 1
0 * 4 +  0 * 2 + 10 * 1

关键的观察是,我们可以将所有这些写成总和,但更重要的是,总和中的每一项都不大于前一项:

2 * 4 +  1 * 2 +  0 * 1 = 4 + 4 + 2
2 * 4 +  0 * 2 +  2 * 1 = 4 + 4 + 1 + 1
1 * 4 +  3 * 2 +  0 * 1 = 4 + 2 + 2 + 2
1 * 4 +  2 * 2 +  2 * 1 = 4 + 2 + 2 + 1 + 1
1 * 4 +  1 * 2 +  4 * 1 = 4 + 2 + 1 + 1 + 1 + 1
1 * 4 +  0 * 2 +  6 * 1 = 4 + 1 + 1 + 1 + 1 + 1 + 1
0 * 4 +  5 * 2 +  0 * 1 = 2 + 2 + 2 + 2 + 2
0 * 4 +  4 * 2 +  2 * 1 = 2 + 2 + 2 + 2 + 1 + 1
0 * 4 +  3 * 2 +  4 * 1 = 2 + 2 + 2 + 1 + 1 + 1 + 1
0 * 4 +  2 * 2 +  6 * 1 = 2 + 2 + 1 + 1 + 1 + 1 + 1 + 1
0 * 4 +  1 * 2 +  8 * 1 = 2 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1
0 * 4 +  0 * 2 + 10 * 1 = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1

所以这给出了一个有趣的想法,即如何生成所有可能的方法来总结目标。这个想法是固定第一个系数,然后生成所有可能的方法来使其余的总和计算出来。换句话说,我们可以递归地思考这个问题。如果我们按 x1, x2, ..., xn 的顺序列出变量,那么我们可以尝试固定一些特定的系数对于 x1,然后仅使用 x2, ..., xn 解决对 sum - c_1 x_1 求和的问题。

到目前为止,这似乎并不那么花哨 - 事实上,这正是您在上面所做的 - 但我们可以使用一个技巧。只要我们要递归地思考这个问题,让我们以相反的方式思考这个问题。与其从 sum 开始并尝试将其分解,不如从 0 开始并尝试构建我们所能做的一切呢?

这就是想法。假设我们已经预先知道我们可以仅使用 x1 的总和得出的所有数字。然后对于介于 0 和 sum(含)之间的每个数字 k,我们可以将 x2 和 x1 中的 k 取自 k - c 的任意组合2 x2 可以由 x1 的组合组成。但是由于我们已经预先计算了这个,我们可以迭代所有可能的 c2 合法值,计算 k - c2 x2 ,看看我们是否知道如何制作它。假设我们存储了一个巨大的 U x (k + 1) 布尔值表,使得表条目 [x, y] 存储“我们能否以精确总结为 U 的方式总结第一个 y 值,包括在内?”我们可以有效地填写表格。这称为动态规划,是一种强大的算法工具。

更具体地说,这可能是如何工作的。给定 k 个变量,创建一个 U x (k + 1) 值表 T。然后,为所有 x > 0 设置 T[0][0] = true 和 T[x][0] = false。这里的基本原理是 T[0][0] 的意思是“我们可以使用第一个零变量的线性组合?”答案肯定是肯定的(空总和为零!),但对于任何其他由没有变量的线性组合组成的总和,我们肯定做不到。

现在,对于 i = 1 .. k,我们将尝试填充 T[x][i] 的值。请记住,T[x][i] 的意思是“我们可以将 x 作为前 i 个变量的线性组合吗?”好吧,我们知道如果有某个系数 c,我们可以做到这一点,使得 k - cxi 可以使用 x1, x 的线性组合2, ..., xi - 1。但是对于任何 c,这只是 T[x - c xi][i - 1] 是否为真。因此我们可以说

for i = 1 to k
    for z = 0 to sum:
        for c = 1 to z / x_i:
            if T[z - c * x_i][i - 1] is true:
                set T[z][i] to true

检查循环,我们看到外部循环运行 k 次,内部循环每次迭代运行 sum 次,最里面的循环每次迭代最多运行 sum 次。他们的产品是(使用我们上面的符号)O(U2 k),这比您最初使用的 O(Uk) 算法要好得多。

但是您如何使用这些信息列出所有可能的方法来总结目标?这里的诀窍是要意识到您可以使用该表来避免浪费大量精力来搜索所有可能的组合,而其中许多组合都不起作用。

让我们看一个例子。假设我们已经完全计算了这个表并且想要列出所有的解决方案。一个想法是考虑列出最后一个变量的系数为零的所有解决方案,然后当最后一个变量为 1 时,等等。您之前使用的方法的问题是,对于某些系数,可能根本没有任何解决方案.但是通过我们上面构建的表格,我们可以修剪掉那些分支。例如,假设我们想看看是否有任何以 xk 开头且系数为 0 的解。这意味着我们要问是否有任何方法可以总结前 k - 1 个变量,因此这些值的总和为 sum。当且仅当 T[sum][k - 1] 为真时,这是可能的。如果是真的,那么我们可以递归地尝试将系数分配给其余的值,总和为sum。如果没有,那么我们跳过这个系数并继续下一个。

递归地,这看起来像这样:

function RecursivelyListAllThatWork(k, sum) // Using last k variables, make sum
    /* Base case: If we've assigned all the variables correctly, list this
     * solution.
     */
    if k == 0:
        print what we have so far
        return

    /* Recursive step: Try all coefficients, but only if they work. */
    for c = 0 to sum / x_k:
       if T[sum - c * x_k][k - 1] is true:
           mark the coefficient of x_k to be c
           call RecursivelyListAllThatWork(k - 1, sum - c * x_k)
           unmark the coefficient of x_k

这将递归地列出所有有效的解决方案,使用我们刚刚构建的表中的值来跳过大量浪费的工作。建立此表后,您可以将任务分配给多台计算机,让它们各自列出全部解决方案的一个子集,然后并行处理它们。

希望这会有所帮助!

【讨论】:

  • 非常感谢您的回答。我会阅读你的链接。我已经开始研究算法,试图看看我是否可以用一种不那么暴力的方式来解决它,但我不抱希望,因为我记得读过丢番图方程没有算法。我一直在想,如果我把它分成不同的服务器并运行它,那么也许我可以在我的有生之年完成它:-) 不确定它是否是一种现实的方法。
  • @Lostsoul- 实际上,我根本不相信这可以扩展,因为在最坏的情况下,可能会有大量的解决方案(大约 U^k 个),而你需要花时间来生成它们中的每一个。您真的需要所有答案,还是只需要数一下答案?
  • 那太棒了。如果这个过程能在合理的时间内完成,我会哭(高兴的眼泪:-)。问题是它生成了太多的数据,但我需要所有的可能性(我将它们保存到数据库并在之后分析它们)。
  • 我需要它们。随着变量和数据的增多,处理时间也会增加,但我可以使用 hadoop 将其拆分到不同的机器上吗?
  • @Lostsoul- 一般来说,如果不花费至少 n 时间来生成它们,您就无法显式生成 n 个项目。这意味着您可能无法获得解决该问题的快速算法。但是,您可能能够构建这些解决方案的隐式表示,从中您可以重建您需要的所有答案。你真的必须有每一个可用的答案吗?或者只是能够一次列出一个?
【解决方案2】:

根据定义,蛮力算法是愚蠢的。使用更聪明的算法(如果有的话)会更好。更好的算法将减少已经完成的工作,希望在某种程度上你可以在不需要“横向扩展”到多台机器的情况下完成。

无论算法如何,总有一天,所需的数据量或计算能力如此之大,以至于您需要使用 Hadoop 之类的东西。但通常,我们在这里真正谈论的是大数据。如今,您已经可以使用一台 PC 完成很多工作。

【讨论】:

  • 有些问题只能通过蛮力解决,即使它们可以很好地扩展。此外,一些蛮力算法实际上确实可以很好地扩展(检查所有对对于合理大小的数据集也可以正常工作)。
  • 就像我说的“如果你有(更好的算法)”。 Hadoop 的常见示例(迭代一个巨大的数据集以产生一些指标或索引)让我觉得相当暴力(当然没有必要)。
  • 我用我在做什么的样本更新了这个问题。但是如果总和增加到一百万并且您添加了 10 个变量,它将永远持续下去(或者至少在我的笔记本电脑上一天)。我认为我试图解决的问题不存在算法,因为它只会产生太多的可能性。我的问题是我需要这些可能性,所以我想也许我可以使用 hadoop 在多台服务器上运行该作业,然后将其全部保存到 hbase 或类似的东西。我不确定它是否可能(否则我会因为服务器而破产)​​span>
  • 如果有 100 台服务器,它的速度仍然只有 100 倍。所以它只是不能很好地扩展。人们使用集群来破解加密密钥和东西(也只能是暴力破解),但这一切都非常昂贵,并且可以通过在您的情况下添加另一个变量或在他们的情况下添加更长的密钥来变得更加昂贵。这就是所谓的难计算问题。没办法。
  • 这是我得出的结论,所以我首先尝试看看使用 hadoop 是否可以减少计算时间(这是我最初的问题),然后是否可以减少时间然后我会研究更多变量的成本与收益(例如,如果需要 24 小时来处理并添加另一台服务器可以使其在 12 小时内运行......也许它值得另一台 EC2 服务器的成本)。但如果它不能缩放(即使是少量),那么我知道它已经死在水中,不必继续。
【解决方案3】:

解决此问题的算法与我们学习的手动数学除法或从十进制转换为八进制或十六进制等其他基数的过程相近 - 除了两个示例仅寻找一个规范解决方案。

为确保递归结束,对数据数组进行排序很重要。为了提高效率并限制递归次数,从更高的数据值开始也很重要。

具体来说,这是针对这个问题的 Java 递归实现 - 理论上预期的每次递归都有一个结果向量 coeff 的副本。

import java.util.Arrays;

public class Solver
{
    public static void main(String[] args)
    {
        int target_sum = 100;
        // pre-requisite: sorted values !!
        int[] data = new int[] { 5, 10, 20, 25, 40, 50 };
        // result vector, init to 0
        int[] coeff = new int[data.length];
        Arrays.fill(coeff, 0);
        partialSum(data.length - 1, target_sum, coeff, data);
    }

    private static void printResult(int[] coeff, int[] data) {
        for (int i = coeff.length - 1; i >= 0; i--) {
            if (coeff[i] > 0) {
                System.out.print(data[i] + " * " + coeff[i] + "   ");
            }
        }
        System.out.println();
    }

    private static void partialSum(int k, int sum, int[] coeff, int[] data) {
        int x_k = data[k];
        for (int c = sum / x_k; c >= 0; c--) {
            coeff[k] = c;
            if (c * x_k == sum) {
                printResult(coeff, data);
                continue;
            } else if (k > 0) {
                // contextual result in parameters, local to method scope
                int[] newcoeff = Arrays.copyOf(coeff, coeff.length);
                partialSum(k - 1, sum - c * x_k, newcoeff, data);
                // for loop on "c" goes on with previous coeff content
            }
        }
    }
}

但现在代码处于特殊情况:每个系数的最后一个值测试为 0,因此不需要复制。

作为复杂度估计,我们可以使用递归调用的最大深度为data.length * min({ data })。当然,它不会很好地扩展,并且有限的因素是堆栈跟踪内存(-Xss JVM 选项)。对于大型 data 集,代码可能会失败并出现堆栈溢出错误。

为了避免这个缺点,“derecursion”过程很有用。它包括用程序堆栈替换方法调用堆栈以存储执行上下文以供以后处理。这是代码:

import java.util.Arrays;
import java.util.ArrayDeque;
import java.util.Queue;

public class NonRecursive
{
    // pre-requisite: sorted values !!
    private static final int[] data = new int[] { 5, 10, 20, 25, 40, 50 };

    // Context to store intermediate computation or a solution
    static class Context {
        int k;
        int sum;
        int[] coeff;
        Context(int k, int sum, int[] coeff) {
            this.k = k;
            this.sum = sum;
            this.coeff = coeff;
        }
    }

    private static void printResult(int[] coeff) {
        for (int i = coeff.length - 1; i >= 0; i--) {
            if (coeff[i] > 0) {
                System.out.print(data[i] + " * " + coeff[i] + "   ");
            }
        }
        System.out.println();
    }

    public static void main(String[] args)
    {
        int target_sum = 100;
        // result vector, init to 0
        int[] coeff = new int[data.length];
        Arrays.fill(coeff, 0);

        // queue with contexts to process
        Queue<Context> contexts = new ArrayDeque<Context>();
        // initial context
        contexts.add(new Context(data.length - 1, target_sum, coeff));

        while(!contexts.isEmpty()) {
            Context current = contexts.poll();
            int x_k = data[current.k];
            for (int c = current.sum / x_k; c >= 0; c--) {
                current.coeff[current.k] = c;
                int[] newcoeff = Arrays.copyOf(current.coeff, current.coeff.length);
                if (c * x_k == current.sum) {
                    printResult(newcoeff);
                    continue;
                } else if (current.k > 0) {
                    contexts.add(new Context(current.k - 1, current.sum - c * x_k, newcoeff));
                }
            }
        }
    }
}

在我看来,很难在单线程执行中提高效率——堆栈机制现在需要 coeff 数组副本。

【讨论】:

  • 非常感谢 Yves,这是一个很好的答案。它定义了更多的变量(我在数据中尝试了 20 个整数并在几分钟内完成。).. 印象深刻,但它似乎非常消耗内存。它使用了我 100% 的 cpu,这太棒了..但是 30 个变量超过了我 15Gigs 的内存,所以我怀疑做 100 个变量会崩溃。有没有办法让它不受内存限制?
  • 这似乎是空间的广度优先搜索,而不是空间的递归DFS。我不认为这本质上比我的方法更有效。在这两种情况下,我们都会探索整个空间。还有一个内存权衡; BFS 搜索在任何时候都会占用更多内存,而 DFS 搜索只会使用与树的最大深度成比例的内存进行搜索。
  • @Lostsoul 在CPU消耗、内存使用和全局响应时间之间总是存在一个折衷方案……非递归的提案是不是也消耗这么多内存?
  • @YvesMartin 是的,非递归的需要大约 15 gigs 的内存来完成 30 个项目..
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-06-22
  • 1970-01-01
  • 2015-01-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多