资讯专栏INFORMATION COLUMN

示例:如何多线程遍历组合

iOS122 / 2477人阅读

摘要:如何用多线程遍历这棵树呢按一级节点不同的值,分别放到线程里面遍历即可。代码如下多线程遍历组合树根据一级节点拆分解空间对拆分的解空间多线程遍历第一个解和最后一个解遍历解区间取的组合遍历完成,共有个解。

这是一个再简单不过的组合问题:

编号 0-9 的 10 个球里面拿取任意 5 个,有多少种不同的组合?

答案是可以用公式算出来的,也就是 (10!) / ((5!) ^ 2) = 252 个。但是如果要把它们全部遍历出来呢?

下面是一种效率比较高的遍历方式,原理是将所有结果集看作是树节点(准确的说是叶子节点),然后去遍历这棵树即可。树的生成规则是:

一级节点的值是第一个球的编号,二级节点是第二个球的编号,依此类推;

任何一级节点的值必须大于上级节点的值。

这样能做到所有的叶子节点刚好覆盖所有的解,没有多余没有缺失。

如何用多线程遍历这棵树呢?按一级节点不同的值,分别放到线程里面遍历即可。每个节点代表一个子树,先计算该树的起始和终止节点,作为解空间的边界,然后从起始节点开始遍历直到终止节点为止即可。

代码如下:

import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.IntStream;

/**
 * 多线程遍历组合树
 */
public class CombinationIterator {

    public static void main(String[] args) throws Exception {

        int itemCount = 50;
        int pickCount = 10;
        AtomicLong answerCount = new AtomicLong();

        long start = System.currentTimeMillis();

        // 根据一级节点拆分解空间
        int[] level1Values = IntStream.range(0, itemCount - pickCount + 1).toArray();
        ExecutorService threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

        // 对拆分的解空间多线程遍历
        for (int level1Value : level1Values) {

            // 第一个解和最后一个解
            int[] firstPicks = IntStream.range(level1Value, pickCount + level1Value).toArray();
            int[] lastPicks =
                    IntStream.concat(
                            IntStream.range(level1Value, level1Value + 1),
                            IntStream.range(itemCount - pickCount + 1, itemCount)
                    ).toArray();

            // 遍历解区间
            threadPool.submit(() -> answerCount.addAndGet(iterateSubTree(firstPicks, lastPicks)));
        }

        threadPool.shutdown();
        threadPool.awaitTermination(1, TimeUnit.HOURS);

        System.out.printf("%d 取 %d 的组合遍历完成,共有 %d 个解。%n", itemCount, pickCount, answerCount.get());
        System.out.printf("执行时间 %dms", (System.currentTimeMillis() - start));
    }

    /**
     * 遍历区间的组合
     *
     * @param firstPicks 区间的第一个解
     * @param lastPicks  区间的最后一个解
     *
     * @return 区间的解数量
     */
    private static long iterateSubTree(int[] firstPicks, int[] lastPicks) {
        long counter = 0;
        int[] picks = firstPicks;

        do {
            if (picks != null) {
                // System.out.println(Arrays.toString(picks));
                counter++;
            }

            picks = getNextPick(picks, lastPicks);
        } while (picks != null);

        System.out.println("区间 " + Arrays.toString(lastPicks) + " 遍历完成,共 " + counter + " 个解");
        return counter;
    }

    /**
     * 根据当前解计算下一个解,直到遇到最终解,则返回 null
     *
     * @param picks     当前解
     * @param lastPicks 最终解
     *
     * @return 下一个解或 null
     */
    private static int[] getNextPick(int[] picks, int[] lastPicks) {

        if (Arrays.equals(picks, lastPicks)) {
            return null;
        }

        int[] nextPick = Arrays.copyOf(picks, picks.length);
        int movable = findMovable(nextPick, lastPicks);
        nextPick[movable]++;

        if (movable != nextPick.length - 1) {
            // 将 movable 后面的点移回到贴紧 movable 的右侧
            partialReset(nextPick, movable);
        }

        return nextPick;
    }

    // 在 picks 中寻找第一个可以右移的点
    private static int findMovable(int[] picks, int[] lastPicks) {

        for (int i = picks.length - 1; i >= 0; i--) {
            if (picks[i] < lastPicks[i]) {
                return i;
            }
        }

        return -1; // 实际上不会返回 -1
    }

    // 指定位置后面的点都移回到贴紧该位置的右侧
    private static void partialReset(int[] picks, int resetStart) {
        for (int i = resetStart + 1; i < picks.length; i++) {
            picks[i] = picks[i - 1] + 1;
        }
    }

}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68876.html

相关文章

  • [Java并发-11] 并发容器的使用

    摘要:同步容器及其注意事项中的容器主要可以分为四个大类,分别是和,但并不是所有的容器都是线程安全的。并发容器及其注意事项在版本之前所谓的线程安全的容器,主要指的就是同步容器,当然因为所有方法都用来保证互斥,串行度太高了,性能太差了。 Java 并发包有很大一部分内容都是关于并发容器的,因此学习和搞懂这部分的内容很有必要。 Java 1.5 之前提供的同步容器虽然也能保证线程安全,但是性能很差...

    legendaryedu 评论0 收藏0
  • Java知识点总结

    摘要:线程池中的和有什么不同直接提交的队列该功能由对象提供。若大于最大线程数,则执行拒绝策略。因为对于固定大小的线程池来说,不存在线程数量的动态变化,所以最大线程数等于核心线程数。返回核心线程数为,最大线程数为无穷大的线程池。 索引的实现方式 1、B+树 我们经常听到B+树就是这个概念,用这个树的目的和红黑树差不多,也是为了尽量保持树的平衡,当然红黑树是二叉树,但B+树就不是二叉树了,节点下...

    I_Am 评论0 收藏0
  • Java知识点总结

    摘要:线程池中的和有什么不同直接提交的队列该功能由对象提供。若大于最大线程数,则执行拒绝策略。因为对于固定大小的线程池来说,不存在线程数量的动态变化,所以最大线程数等于核心线程数。返回核心线程数为,最大线程数为无穷大的线程池。 索引的实现方式 1、B+树 我们经常听到B+树就是这个概念,用这个树的目的和红黑树差不多,也是为了尽量保持树的平衡,当然红黑树是二叉树,但B+树就不是二叉树了,节点下...

    Lorry_Lu 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<