【问题标题】:Generate cartesian product of lists in order of decreasing product of entries (entries are positive numbers, lists are sorted)按条目的递减顺序生成列表的笛卡尔积(条目为正数,列表已排序)
【发布时间】:2020-02-19 10:25:21
【问题描述】:

假设我有几个排序的正数列表,例如:

double[] a1 = new double[]{0.70, 0.20, 0.10};
double[] a2 = new double[]{0.80, 0.10, 0.05, 0.05};
double[] a3 = new double[]{0.60, 0.15, 0.14, 0.10, 0.01};

我想按条目乘积递减的顺序遍历这些数组的笛卡尔积,如下所示:

0000: Combo[product=3.36e-01, vals=[0.70, 0.80, 0.60], indexes=[0, 0, 0]]
0001: Combo[product=9.60e-02, vals=[0.20, 0.80, 0.60], indexes=[1, 0, 0]]
0002: Combo[product=8.40e-02, vals=[0.70, 0.80, 0.15], indexes=[0, 0, 1]]
0003: Combo[product=7.84e-02, vals=[0.70, 0.80, 0.14], indexes=[0, 0, 2]]
0004: Combo[product=5.60e-02, vals=[0.70, 0.80, 0.10], indexes=[0, 0, 3]]
0005: Combo[product=4.80e-02, vals=[0.10, 0.80, 0.60], indexes=[2, 0, 0]]
...

在上面的示例中,第一个条目很明显(因为数组已排序),它是第一个值的组合:[0.70, 0.80, 0.60] 与产品 0.70*0.80*0.60 = 3.36e-01 和数组中对应的值索引 a1, a2, a3[0, 0, 0] .现在第二个条目不太明显,我们应该将0.70 更改为0.20 吗?还是0.600.15?还是0.800.10?第二个应该是[0.20, 0.80, 0.60],产品9.60e-02,索引[1, 0, 0]

这是一个用 Java 生成/打印它们的程序:https://repl.it/repls/FilthyGreatRotation(所有逻辑都在 printWholeCartesianProduct() 方法中)
该程序按字典顺序生成它们,然后按产品对整个集合进行排序。

问题:有没有一种简单的方法可以首先以正确的顺序实际生成组合?

原因:首先我没有列表,只有一些排序的数字集合的迭代器。可能很长,长度未知,但已知每个迭代器中的数字都已排序。

要使用的 MVCE(与上面的 https://repl.it 链接相同):

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;
import java.util.function.Consumer;
import java.util.stream.Collectors;

public class Main {

    public static void main(String[] args) {
        List<List<Double>> data = createData();
        printWholeCartesianProduct(data);
    }

    public static List<List<Double>> createData() {
        double[] a1 = new double[]{0.70, 0.20, 0.10};
        double[] a2 = new double[]{0.80, 0.10, 0.05, 0.05};
        double[] a3 = new double[]{0.60, 0.15, 0.14, 0.10, 0.01};
        return createData(a1, a2, a3);
    }

    public static void  printWholeCartesianProduct(List<List<Double>> data) {
        final DecimalFormat df = new DecimalFormat("0.00");

        // print input data
        String matrix = data.stream()
            .map(l -> l.stream().map(df::format).collect(Collectors.joining(", ")))
            .map(row -> "[" + row + "]")
            .collect(Collectors.joining("\n"));
        System.out.println("Input data:\n" + matrix);

        // collect combos as they are generated
        final List<Combo> combos = new ArrayList<>();
        Consumer<int[]> callback = indexes -> {
            double[] v = new double[indexes.length];
            double prod = 1;
            for (int i = 0; i < indexes.length; i++) {
                List<Double> col = data.get(i);
                int index = indexes[i];
                v[i] = col.get(index);
                prod *= v[i];
            }
            combos.add(new Combo(prod, v, indexes.clone()));
        };

        // generate combos
        int[] c = new int[data.size()];
        int ptr = c.length - 1;
        while (ptr >= 0) {
            callback.accept(c);
            c[ptr]++; // increment
            if (c[ptr] == data.get(ptr).size()) { // carry
                do {
                    ptr--;
                } while(ptr >= 0 && c[ptr] == data.get(ptr).size() - 1);
                if (ptr < 0) {
                    break;
                }
                c[ptr]++;
                // zero out
                while (++ptr <= c.length - 1) {
                    c[ptr] = 0;
                }
                ptr = c.length - 1;
            }
        }

        // cheating - sort after generation and print result
        combos.sort((o1, o2) -> Double.compare(o2.product, o1.product));
        StringBuilder sb = new StringBuilder();
        double totalP = 0;
        for (int i = 0; i < combos.size(); i++) {
            sb.append(String.format("%04d: ", i)).append(combos.get(i)).append("\n");
            totalP += combos.get(i).product;
        }
        System.out.printf("Cartesian product in descending product (total p=%.3e):\n%s", totalP, sb.toString());
    }

    public static List<Double> asList(double[] a) {
        return Arrays.stream(a).boxed().collect(Collectors.toList());
    }

    public static List<List<Double>> createData(double[]... arrays) {
        final List<List<Double>> vals = new ArrayList<>();
        Arrays.stream(arrays).forEachOrdered(a -> vals.add(asList(a)));
        return vals;
    }

    static class Combo {
        final double product;
        final double[] vals;
        final int[] indexes;

        Combo(double product, double[] vals, int[] indexes) {
            this.product = product;
            this.vals = vals;
            this.indexes = indexes;
        }

        @Override
        public String toString() {
            return new StringJoiner(", ", Combo.class.getSimpleName() + "[", "]")
                .add("product=" + String.format("%.2e", product))
                .add("vals=[" + Arrays.stream(vals).boxed().map(v -> String.format("%.2f", v)).collect(
                    Collectors.joining(", ")) + "]")
                .add("indexes=" + Arrays.toString(indexes))
                .toString();
        }
    }
}

【问题讨论】:

  • 二次进入选择[0.20, 0.80, 0.60]的依据是什么?
  • 预期结果的顺序是什么?它似乎是按产品价值降序排列的。
  • 我相信你需要将你的数字限制为非负数。如果允许负数,则没有算法可以做到,因为两个大(绝对值大)负数的乘积会产生一个大正数。而且您无法在列表的末尾预见“大”负数。
  • @caisil 这正是要求中所说的,降低了产品价值
  • 请注意,既然每个人都是积极的,你可能想看看stackoverflow.com/questions/49417578/…

标签: java algorithm iteration cartesian-product


【解决方案1】:

我对Java不熟悉,但既然主要是算法,伪代码应该就够了:

Input:
Non-empty lists A, B, C: containing positive number(s).

Pseudo-code:
type-define tuple3 = (iterator, iterator, iterator);
function double value(tuple3 x) {
  return x.elm[0].value() * x.elm[1].value() * x.elm[2].value();
}
function boolean greater_than (tuple3 x, tuple3 y) {
  return (value(x) > value(y));
}
function void main() {
  iterator a = A.first();
  iterator b = B.first();
  iterator c = C.first();
  set<tuple3> Visit;
  PriorityQueue<tuple3, greater_than>  Q;
  Q.add((a,b,c));
  Visit.add((a,b,c));
  while (!Q.empty()) {
     tuple x = Q.pop_top();
     output(x);
     (a, b, c) = x;
     if (a.next() != null && !Visit.contains((a.next(), b, c))) {
         Q.add((a.next(), b, c));
         Visit.add((a.next(), b, c));
     }
     if (b.next() != null && !Visit.contains((a, b.next(), c))) {
         Q.add((a, b.next(), c));
         Visit.add((a, b.next(), c));
     }
     if (c.next() != null && !Visit.contains((a, b, c.next()))) {
         Q.add((a, b, c.next()));
         Visit.add((a, b, c.next()));
     }
  }
}

注意output() 函数会打印出一个输出行。我并没有真正处理索引打印,但这应该很容易,对吧? (例如,只需通过将 3-tuple 扩展到 6-tuple 来跟踪索引,以通过额外的 3 个元素保存索引。) 应该很容易将此算法扩展到列表数量大于 3 的问题。

更新

事实上,如果我们想优化速度,我们可以证明在最坏的情况下,需要 O(N^2) 的存储空间。由于 O(exploration boundary) = O(N^2),我们的存储使用量至少比最优解大一些。

不提供官方证明,但我想用 2D 来解释,即 2 列出乘法而不是 3。然后,很容易扩展解释。

假设我们有列表 A、B,其中 N 个正数按降序排列。 我们将这些 NxN 乘法结果排列在 2D 数组中。例如当 N = 4 时,它看起来像:

o > o > o > *
v   v   v   v
o > o > * > o
v   v   v   v
o > * > o > o
v   v   v   v
* > o > o > o

每个o* 代表一个乘法结果。 &gt; 表示“大于”。

左上角的o 代表A[0] * B[0]。向右每一步意味着对A[] 使用+1 索引,向下每一步意味着对B[] 使用+1 索引。对于同一列,A 的索引是相同的。对于同一行,B 的索引相同。

考虑*:我们只知道A[]B[] 是降序排序的。但我们不知道每一步是如何“下降”的。因此,那些* 可以按任何顺序排列!这4个中的任何一个!命令。如果您至少不将它们保存在一些预先排序的结构(堆、优先级队列等)中,我们必须一次又一次地读取和比较它(即对这 4 个产品进行排序),这会破坏优化速度假设。

因此我们已经解释了为什么需要 N 个存储空间。

现在我们需要证明我们的 2D 版本算法(即 2 个列表产品)最多只需要 2N 存储空间。

我只是想给个提示。完整的证明太长了。 例如,如果在我们算法的中间,优先级队列存储了 4 个*。假设*之一被访问,其中两个被插入到队列中,如下:

o > o > o > *
v   v   v   v
o > o > P > N
v   v   v   v
o > * > N > o
v   v   v   v
* > o > o > o

其中P 表示前一个,即从队列中弹出的最大值,N 表示接下来的两个,由与P 相邻的每个索引 +1 生成。很明显,这两个N 不能被选为最大值(因为* 之一的乘积比它们中的每一个都大)。直到那些更高的那些被弹出队列,那些N 不能生成新的进入队列。现在,至少有两个*的“进步”方向被挡住了! That means when one of the two is selected (i.e. highest value to pop-out), it can only generate one new product into the queue.然后队列最多维持在 2N 大小。

将此应用于 3D,我们知道存储应该是 O(N^2)。

更新“set”实现的存储使用情况

有人可能会问,“套”呢? Set 通常实现为哈希表,与使用的条目数成正比。一个简单的实现可能需要存储所有产品(即 O(N^2) 用于 2D 版本,O(N^3) 用于 3D 版本)。仔细调整以删除从未需要的条目,将使存储需求更小。考虑任何 2D 版本的产品,最多只能被其他 2 个产品访问。即 Set.Contains() 的测试次数每个产品最多执行两次。如果我们保持计数并删除那些未使用的哈希条目,它将使那些“需要”条目与我们队列中的那些产品非常接近。这意味着,在 2D 版本中,哈希表也使用 O(N) 存储,而在 3D 版本中使用 O(N^2)。

【讨论】:

  • 看起来与 cmets 中建议的另一个答案相同:stackoverflow.com/a/49421290/88814。我希望进行更直接的迭代,而不必存储整个探索边界。这可能是不可能的,我不知道。
猜你喜欢
  • 2018-08-31
  • 2019-07-23
  • 2014-05-27
  • 2012-05-31
  • 2012-03-24
  • 2022-11-17
  • 2015-12-05
  • 1970-01-01
  • 2012-08-17
相关资源
最近更新 更多