Java功能流:在距离A和B之间生成一组随机点

时间:2015-12-04 12:30:44

标签: java functional-programming java-stream

在从事玩具项目时,我遇到了生成一组N 2d点的问题,其中每个点都在距离A和B之间,与集合中的每个其他点之间(以及在某些绝对边界内)。

我更喜欢使用java stream和lambdas进行练习,因为它们的优雅和易于并行化的可能性,所以我询问如何以命令式方式解决这个问题!

首先想到的解决方案是:

  1. 使用随机向量播种集(或列表)
  2. 直到集合达到N:
    1. 创建一个长度介于A和B之间的随机向量,并将其添加到随机“父”向量
    2. 如果它超出界限或者比A更接近集合中的任何向量,则丢弃它,否则将其添加到集合
    3. 重复
  3. 对于命令式编程(循环)来说,这对我来说是微不足道的,但是在执行此功能时我感到困惑,因为流中新生成的元素依赖于先前在同一流中生成的元素。

    这是我想出的 - 注意开头的icky循环。

    while (pointList.size() < size) {
        // find a suitable position, not too close and not too far from another one
        Vec point =
                // generate a stream of random vectors
                Stream.generate(vecGen::generate)
                        // elongate the vector and add it to the position of one randomly existing vector
                        .map(v -> listSelector.getRandom(pointList).add(v.mul(random.nextDouble() * (maxDistance - minDistance) + minDistance)))
                        // remove those that are outside the borders
                        .filter(v -> v.length < diameter)
                        // remove those that are too close to another one
                        .filter(v -> pointList.stream().allMatch(p -> Vec.distance(p, v) > minDistance))
                        // take the first one
                        .findAny().get();
    
    
        pointList.add(point);
    }
    

    我知道这个循环可能永远不会终止,具体取决于参数 - 真正的代码有额外的检查。

    我想到的一个有效的功能解决方案是生成完全随机的N个向量集,直到其中一个集合满足条件,但性能会很糟糕。此外,这将绕过我面临的问题:是否可以使用流中已经生成的元素同时向流中添加新元素(非常肯定会违反某些基本原则,所以我猜答案是否定的) ?

    有没有办法以功能性而非浪费的方式做到这一点?

1 个答案:

答案 0 :(得分:1)

下面显示了一个简单的解决方案。 Pair类可以在Apache commons lang3中找到。

public List<Pair<Double, Double>> generate(int N, double A, double B) {
    Random ySrc = new Random();

    return new Random()
            .doubles(N, A, B)
            .boxed()
            .map(x -> Pair.of(x, (ySrc.nextDouble() * (B - A)) + A))
            .collect(Collectors.toList());
}

我的原始解决方案(上图)错过了A和B表示任意两点之间的最小和最大距离的点。因此,我会提出一种不同的解决方案(更复杂的方法),它依赖于在单位圆上生成点。我使用最小为-1/2 B且最大为1/2 B的随机距离来缩放(乘)表示该点的单位向量。该方法在由半径1/2 B的圆所界定的区域中均匀分布点。这解决了点约束之间的最大距离。给定A和B之间的足够差异,其中A <1。 B和N不是太大,也可能满足最小距离约束。满足最大距离约束可以用纯函数代码完成(即没有副作用)。

为了确保满足最小约束,需要一些命令性代码(即副作用)。为此,我使用带副作用的谓词。谓词累积满足最小约束条件的点,并在累积N个点时返回true。

请注意,运行时间未知,因为随机生成了点。当N = 100,A = 1.0,B = 30.0时,测试代码快速运行。我为B尝试了10和20的值,并没有等到它结束。如果你想要一个更紧密的点集,你可能需要加快这个代码或开始查看线性求解器。

public class RandomPoints {
    /**
     * The stop rule is a predicate implementation with side effects. Not sure
     * about the wisdom of this approach. The class does not support concurrent
     * modification.
     * 
     * @author jgmorris
     *
     */
    private class StopRule implements Predicate<Pair<Double, Double>> {
        private final int N;
        private final List<Pair<Double, Double>> points;

        public StopRule(int N, List<Pair<Double, Double>> points) {
            this.N = N;
            this.points = points;
        }

        @Override
        public boolean test(Pair<Double, Double> t) {
            // Brute force test. A hash based test would work a lot better.
            for (int i = 0; i < points.size(); ++i) {
                if (distance(t, points.get(i)) < dL) {
                    // List size unchanged, continue
                    return false;
                }
            }
            points.add(t);
            return points.size() >= N;
        }

    }

    private final double dL;
    private final double dH;
    private final double maxRadius;
    private final Random r;

    public RandomPoints(double dL, double dH) {
        this.dL = dL;
        this.dH = dH;
        this.maxRadius = dH / 2;
        r = new Random();
    }

    public List<Pair<Double, Double>> generate(int N) {
        List<Pair<Double, Double>> points = new ArrayList<>();
        StopRule pred = new StopRule(N, points);

        new Random()
                // Generate a uniform distribution of doubles between 0.0 and
                // 1.0
                .doubles()
                // Transform primitive double into a Double
                .boxed()
                // Transform to a number between 0.0 and 2&piv;
                .map(u -> u * 2 * Math.PI)
                // Generate a random point
                .map(theta -> randomPoint(theta))
                // Add point to points if it meets minimum distance criteria.
                // Stop when enough points are gathered.
                .anyMatch(p -> pred.test(p));

        return points;
    }

    private final Pair<Double, Double> randomPoint(double theta) {
        double x = Math.cos(theta);
        double y = Math.sin(theta);
        double radius = randRadius();
        return Pair.of(radius * x, radius * y);
    }

    private double randRadius() {
        return maxRadius * (r.nextDouble() - 0.5);
    }

    public static void main(String[] args) {
        RandomPoints rp = new RandomPoints(1.0, 30.0);
        List<Pair<Double, Double>> points = rp.generate(100);

        for (int i = 0; i < points.size(); ++i) {
            for (int j = 1; j < points.size() - 1; ++j) {
                if (i == j) {
                    continue;
                }

                double distance = distance(points.get(i), points.get(j));

                if (distance < 1.0 || distance > 30.0) {
                    System.out.println("oops");
                }
            }
        }
    }

    private static double distance(Pair<Double, Double> p1, Pair<Double, Double> p2) {
        return Math.sqrt(Math.pow(p1.getLeft() - p2.getLeft(), 2.0) + Math.pow(p1.getRight() - p2.getRight(), 2.0));
    }

}