【问题标题】:Iterative algorithm for generating compositions生成合成的迭代算法
【发布时间】:2017-09-13 22:20:27
【问题描述】:

我已经实现了一个算法,它在 Y 框中写出 X 项的所有可能组合(在组合学中,我相信它们被称为组合)。我使用递归函数编写了这个算法,因为它更容易跟踪我们正在放入项目的当前盒子以及剩余的项目数量。 这个算法给了我以下输出(4个盒子和3个项目):

[3, 0, 0, 0], [2, 1, 0, 0], [2, 0, 1, 0], [2, 0, 0, 1], [1, 2, 0, 0], [1, 1, 1, 0], ..., [0, 0, 1, 2], [0, 0, 0, 3]

您可以想象,对于较大的 X 和 Y,合成的数量会增加很多。由于我不关心输入的顺序,我想并行化进一步的计算,但我不能这样做在递归函数上(我尝试通过保存递归状态来做到这一点,但这相当混乱,尽管它可以工作,但它真的很慢)。 所以,我想知道是否可以迭代地重写相同的函数(使用for 循环),以便我可以并行化它。 (要记住的一点是,优化也很重要,因为我需要运行大 X 和 Y 的函数)我的递归函数写在下面:

public void charArrangements (int boxes, int items, String strBuild) {
        if (boxes > 1) {
            for (int i = 0; i <= items; i++) {
                String ss = strBuild + (items - i) + ";";
                charArrangements(boxes - 1, i, ss);
            }
        }
        else {
            String ss = strBuild + items;
            List<Integer> charArrangement = new ArrayList<Integer>();
            charArrangement = Stream.of(ss.split(";")).map(Integer::parseInt).collect(Collectors.toList());

            doCalculations(charArrangement);
        }
} 

【问题讨论】:

  • 假设 fw 是一个文件,你很可能会受到 IO 性能的限制,所以并行化可能没有多大帮助。
  • 这不是重点。我正在通过写出来测试该功能。无论如何我都不应该把它写出来,因为对于大 X 和 Y,文件会变得非常庞大,而我没有足够的空间来写。实际上,在该 try/catch 块中会有计算。
  • 很公平!虽然你不能在初始调用charArrangements 时并行化循环吗?即你说你有 11 个项目,这意味着你可以有 11 个线程,每个线程处理不同的初始值 items
  • 这似乎类似于 zmbq 的答案,但我不完全确定如何去做。如果我像你说的那样在最初的电话中这样做,我会得到 [2, 0], [1, 1], [0, 2] for 2 box, 2 items, [1, 0], [0, 1 ] 表示 2 个框,1 项,[0, 0] 表示 2 框,0 项。如果我对 2 个盒子和 2 件商品感兴趣,那么 1 或 0 件商品对我来说无关紧要。这意味着,我需要以某种方式在较低级别上并行化它,但我不完全确定如何做到这一点。
  • 您需要在输出中包含所有结果吗?或者你能用一个一次吐出一个的迭代器来解决吗?还是随机均匀地随机选择一个结果的东西?还是只是可能性的总数?

标签: java algorithm recursion iteration


【解决方案1】:

我认为你需要这个。让我们试试

public void charArrangements (int boxes, int balls, String strBuild) {
    for(int j=boxes;j>1;j--){
        for (int i = 0; i <= balls; i++) {
            String ss = strBuild + (balls - i) + ";";
            }
    }
}

在此之后,您可以执行 else 子句中的逻辑。

【讨论】:

    【解决方案2】:

    你可以在不同的线程和join当前线程中调用递归调用,例如:

    public void charArrangements (int boxes, int balls, String strBuild) {
        if (boxes > 1) {
            for (int i = 0; i <= balls; i++) {
                String ss = strBuild + (balls - i) + ";";
                final int count = i;
                try{
                    Runnable r = new Runnable() {
                        @Override
                        public void run() {
                            charArrangements(boxes - 1, count, ss);
                        }
                    };
                    Thread t = new Thread(r);
                    t.start();
                    t.join();
                }catch(Exception e){
                    throw new RuntimeException(e);
                }
            }
        }
        else {
            String ss = strBuild + balls;
            List<Integer> charArrangement = new ArrayList<Integer>();
            charArrangement = Stream.of(ss.split(";")).map(Integer::parseInt).collect(Collectors.toList());
        }
    }
    

    附:删除了文件写入部分。

    【讨论】:

    • 我现在已经尝试过了,它可以工作,但实际上需要更长的时间。我相信这是因为递归的单次迭代计算非常快,所以管理线程需要更多时间。理想情况下,我会使用一个线程对数千个 charArrangements 进行计算,但是要像这样将它们汇集在一起​​,无论如何我都需要进行递归,所以我没有获得任何时间.. 有解决方案吗?跨度>
    • 看看这个例子 (stackoverflow.com/questions/33512762/…),它使用执行器服务,这是我在回答中提到的更高级别的抽象。
    【解决方案3】:

    线程就像

    public void charArrangements (int boxes, int balls, String strBuild) {
        ArrayList<myThread> arr=new ArrayList<>();//fill arraylist
        for(int j=boxes;j>1;j--){
            arr.get(j).start();
        }
    }
    
    
    
    //put this login in your threads run() method
    public void someFunc(int balls, String strBuild)
    {
        for (int i = 0; i <= balls; i++) {
            String ss = strBuild + (balls - i) + ";";
            }
    }
    

    【讨论】:

      【解决方案4】:

      为什么不能并行化递归函数?使用主线程遍历前 N 级(最好的 N 取决于您的问题空间),然后让并发工作人员处理 N+1 级及更高级别。

      【讨论】:

      • 我的最终问题空间最终将是 75 个盒子中的 11 个项目。通过不并行化函数,在 75 个盒子中完成 5 个项目大约需要 3 分钟。时间呈指数增长,所以并行化是我唯一的可能。所以,我应该先做第一个,在单个线程上说 N=5 级别,然后并行化级别 6、7、8 等等?如何处理 N+2 及以后?如果我的 N+1 级别占用了我所有的可用线程,我不能在较低级别上使用它们,可以吗?
      • 每个线程将处理 N+1 级直到结束。至于管理工作线程,我想你可以在 Java 中使用某种线程池,不是吗?
      • 是的,有。您能否提供某种伪代码(或 C# 实现,无论如何它都与 Java 足够相似)来处理如何用一个线程处理前 N 个级别,然后并行化和连接?
      【解决方案5】:

      听起来您希望能够更直接地访问累积解决方案的状态;例如,能够保存和恢复它,而不必每次都重建它。实现此目的的一种方法是使用字典生成。

      我建议首先创建没有任何零的数字分区,然后添加零并使用已知的简单算法进行下一个字典排列,您可以轻松地在线查找。这是一个将 5 件商品分装到 7 个盒子中的示例:

      Order partitions in decreasing order by number of parts
      
      [0,0,1,1,1,1,1] -> generate all lexicographic permutations
      [0,0,0,1,1,1,2] -> generate all lexicographic permutations
      [0,0,0,0,1,2,2] -> ...
      

      以下是我想出的一种用于反向下一个相等大小的字典分区的算法,也许它可以改进或更正(我们将其反转,以便我们可以在下一步中从排名最低的排列开始):

      (0) The first partition is either k (n/k)'s if k divides n,
          or (remainder of n/k) (n/k + 1)'s and (k - remainder of n/k)'s (n/k)'s
          to their left.
      
      (1) Find the leftmost part, l, greater than 1
      
      (2) If l is the rightmost part, we're done.
          Otherwise, decrement l and increment the leftmost part, r,
            right of l that is lower than its right neighbour.
            If r is not found, increment the rightmost part and
              set the rest of the partition according to
              instruction (0), where k and n now would be 
              (k - 1) and (n - rightmost part), respectively.
      
      [0,0,0,0,1,2,2] -> generate all lexicographic permutations
      [0,0,0,0,1,1,3] -> ...
      etc.
      

      【讨论】:

      • 那里没有错误吗?例如,如果我在 2 个盒子中有 5 件物品.. (k-1) (n/(k-1)) 给我一个 5,因此 [0,5]。也就是说l=5,也是最右边的部分,所以我们结束了。那么,所有其他可能的分区在哪里,例如 [1, 4] 和 [2, 3]?
      • @NoelAramis 你是对的,这是一个错误。感谢您指出了这一点。我相信我现在更正了。
      • 还有一点我不太明白——指令(2)。首先,r其实是l+1,只有r+1存在才存在?第二件事:如果 r 存在,递归如何继续?即使 r 不存在,使用 (k-1) 和 (n-rightmost 部分) 调用指令 (0) 也会给出一个较小的数组,(我假设)作为结果返回到上面的级别并附加到最右边的部分。但同样,递归如何继续?如果我从 k=2,n=5 开始,我的第一个结果是 [2,3]。然后我通过指令(2),其中没有找到 r,并得到 [1,4]。 [0,5] 呢?
      • @NoelAramis 该算法适用于下一个大小相等的字典分区,即相等数量的部分。 [2,3] => l 是 2;没有找到 r,所以我们增加最右边的部分并设置 分区的其余部分,其中 n 现在是 5 - 4 = 1,k 是 2 - 1 = 1,所以 [1,4],完毕。 [5] 将是一个不同的开始(和结束)点,将 5 划分为一个部分,然后您可以在其上附加任意数量的零,然后进入枚举字典排列的下一步。跨度>
      • 是的,但假设您的起点是 [3,4]。 l 是 3,没有找到 r,我们得到 [2,5]。算法不是在这里终止,我们永远不会得到 [1,6],还是我错了?在这里,[1,6] 仍然应该是 7 分成 2 部分的分区。
      【解决方案6】:

      一个简单但丑陋的迭代算法,用于以逆字典顺序枚举弱 k 组合,可能如下所示:

      static void weakCompositions(int n, int k, Consumer<int[]> callback) {
          assert n >= 0: n;
          assert k >= 0: k;
      
          if (k == 0)
              return;
      
          int C[], i, x;
          C = new int[k];
          C[0] = n;
      
          while (true) {
              callback.accept(C);
      
              i = k - 2;
      
              while (i >= 0 && C[i] == 0)
                  i--;
      
              if (i < 0)
                  break;
      
              C[i]--;
              x = C[k - 1] + 1;
              C[k - 1] = 0;
              C[i + 1] = x;
          }
      }
      

      其中n 是项目数,k 是盒子数。

      不确定它是否适合并行化。

      基本思路是在数组的前缀中找到最右边的非零值的位置(即不包括最后一项); “转移” 1 从那个位置到它的直接权利;并且,在那个新位置,吸收它右边的所有值(如果有的话,它应该只是数组中的最后一个值,通过构造)。

      我相信应该可以跟踪数组前缀的最右边的非零值,而不必为生成的每个合成进行扫描。这样做还可以更轻松地分析算法并查看它是否是常数摊销时间。

      最后,number of weak k-compositions(n + k - 1)! / n! / (k - 1)! 给出。您提到您的最终问题空间是 75 个盒子中的 11 个项目,即超过 21 万亿个对象!即使使用并行化,这似乎......令人生畏。祝你好运!

      【讨论】:

        猜你喜欢
        • 2011-06-18
        • 1970-01-01
        • 1970-01-01
        • 2014-04-12
        • 2018-05-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-11-01
        相关资源
        最近更新 更多