4

我正在寻找一种方法来生成按单个属性排序的对象组合。我不认为字典顺序是我正在寻找的......我会尝试举一个例子。假设我有一个对象 A、B、C、D 的列表,其中我想要按 3、3、2、1 排序的属性值。这给出了 A3、B3、C2、D1 对象。现在我想生成 2 个对象的组合,但它们需要按降序排列:

  • A3 B3
  • A3 C2
  • B3 C2
  • A3 D1
  • B3 D1
  • C2 D1

生成所有组合并对它们进行排序是不可接受的,因为现实世界的场景涉及大集合和数百万个组合。(40 个,8 个订单),我只需要高于特定阈值的组合。

实际上,我需要按给定属性的总和分组的阈值以上的组合计数,但我认为这样做要困难得多 - 所以我会满足于开发高于阈值的所有组合并计算它们。如果有可能的话。

编辑 - 我最初的问题不是很精确......我实际上并不需要订购这些组合,只是认为它有助于隔离高于阈值的组合。更准确地说,在上面的例子中,给定阈值 5,我正在寻找一个信息,即给定集合产生 1 个总和为 6 ( A3 B3 ) 和 2 总和为 5 ( A3 C2, B3 C2)。我实际上并不需要组合本身。

我正在研究子集和问题,但如果我正确理解给定的动态解决方案,它只会给你信息是否有给定的总和,而不是总和的计数。

谢谢

4

6 回答 6

2

实际上,我认为您确实想要字典顺序,但要降序而不是升序。此外:

  • 从您的描述中我不清楚 A,B,... D 在您的答案中扮演任何角色(可能作为值的容器除外)。
  • 我认为您的问题示例只是“对于每个至少 5 个整数,最多两个值的最大可能总数,{3, 3, 2, 1} 集合中有多少不同的对具有该整数的总和?”
  • 有趣的部分是早期救助,一旦无法达成任何可能的解决方案(剩余可实现的金额太小)。

稍后我将发布示例代码。

这是我承诺的示例代码,以下是一些注释:

public class Combos {

    /* permanent state for instance */
    private int values[];
    private int length;

    /* transient state during single "count" computation */
    private int n;
    private int limit;
    private Tally<Integer> tally;
    private int best[][];  // used for early-bail-out

    private void initializeForCount(int n, int limit) {
        this.n = n;
        this.limit = limit;
        best = new int[n+1][length+1];
        for (int i = 1; i <= n; ++i) {
            for (int j = 0; j <= length - i; ++j) {
                best[i][j] = values[j] + best[i-1][j+1];
            }
        }
    }

    private void countAt(int left, int start, int sum) {
        if (left == 0) {
            tally.inc(sum);
        } else {
            for (
                int i = start;
                i <= length - left
                && limit <= sum + best[left][i];  // bail-out-check
                ++i
            ) {
                countAt(left - 1, i + 1, sum + values[i]);
            }
        }
    }

    public Tally<Integer> count(int n, int limit) {
        tally = new Tally<Integer>();
        if (n <= length) {
            initializeForCount(n, limit);
            countAt(n, 0, 0);
        }
        return tally;
    }

    public Combos(int[] values) {
        this.values = values;
        this.length = values.length;
    }

}

前言备注:

这使用了一个名为 Tally 的小助手类,它只是隔离了列表(包括对从未见过的键进行初始化)。我会把它放在最后。

为了保持简洁,我采用了一些对“真实”代码来说不是好的做法的捷径:

  • 这不会检查空值数组等。
  • 我假设值数组已经按降序排序,这是早期救助技术所必需的。(好的生产代码将包括排序。)
  • 我将瞬态数据放入实例变量中,而不是将它们作为参数传递给支持count. 这使得这个类是非线程安全的。

解释:

的实例Combos是使用要组合的(降序)整数数组创建的。该value数组为每个实例设置一次,但count可以使用不同的人口规模和限制进行多次调用。

count方法触发(大部分)标准递归遍历n来自values. 该limit论点给出了利息总和的下限。

countAt方法检查来自 的整数组合valuesleft参数是剩余多少整数来构成n总和中的整数,是startvalues搜索的位置,并且sum是部分总和。

早期救助机制基于计算best,一个二维数组,指定从给定状态可达到的“最佳”总和。in的值是从原始位置开始best[n][p]的值的最大总和。npvalues

countAt当积累了正确的人口时,触底的递归;这会将当前sumn值)添加到tally. 如果countAt还没有触底,values则从start-ing 位置扫过增加当前的 partial sum,只要:

  • 留有足够的职位values以达到指定的人口数量,并且
  • 剩余的best(最大)小计足够大,可以使limit.

使用您的问题数据运行的示例:

    int[] values = {3, 3, 2, 1};
    Combos mine = new Combos(values);
    Tally<Integer> tally = mine.count(2, 5);
    for (int i = 5; i < 9; ++i) {
        int n = tally.get(i);
        if (0 < n) {
            System.out.println("found " + tally.get(i) + " sums of " + i);
        }
    }

产生您指定的结果:

found 2 sums of 5
found 1 sums of 6

这是理货代码:

public static class Tally<T> {
    private Map<T,Integer> tally = new HashMap<T,Integer>();
    public Tally() {/* nothing */}
    public void inc(T key) {
        Integer value = tally.get(key);
        if (value == null) {
            value = Integer.valueOf(0);
        }
        tally.put(key, (value + 1));
    }
    public int get(T key) {
        Integer result = tally.get(key);
        return result == null ? 0 : result;
    }
    public Collection<T> keys() {
        return tally.keySet();
    }
}
于 2009-02-07T15:45:06.533 回答
1

我编写了一个类来处理处理二项式系数的常用函数,这是您的问题所属的问题类型。它执行以下任务:

  1. 将任何 N 选择 K 的所有 K 索引以良好的格式输出到文件。K-indexes 可以替换为更具描述性的字符串或字母。这种方法使得解决这类问题变得非常简单。

  2. 将 K 索引转换为已排序二项式系数表中条目的正确索引。这种技术比依赖迭代的旧已发布技术快得多。它通过使用帕斯卡三角形固有的数学属性来做到这一点。我的论文谈到了这一点。我相信我是第一个发现并发表这种技术的人,但我可能是错的。

  3. 将已排序二项式系数表中的索引转换为相应的 K 索引。

  4. 使用Mark Dominus方法计算二项式系数,它不太可能溢出并且适用于更大的数字。

  5. 该类是用 .NET C# 编写的,并提供了一种通过使用通用列表来管理与问题相关的对象(如果有)的方法。此类的构造函数采用一个名为 InitTable 的 bool 值,当它为 true 时,将创建一个通用列表来保存要管理的对象。如果此值为 false,则不会创建表。无需创建表即可执行上述 4 种方法。提供了访问器方法来访问表。

  6. 有一个关联的测试类,它显示了如何使用该类及其方法。它已经用 2 个案例进行了广泛的测试,并且没有已知的错误。

要了解此类并下载代码,请参阅制表二项式系数

于 2012-09-25T08:07:51.703 回答
0

在stackoverflow中查看这个问题:返回所有组合的算法

我也只是使用下面的 java 代码来生成所有排列,但它可以很容易地用于生成给定索引的唯一组合。

public static <E> E[] permutation(E[] s, int num) {//s is the input elements array and num is the number which represents the permutation

    int factorial = 1;

    for(int i = 2; i < s.length; i++)
        factorial *= i;//calculates the factorial of (s.length - 1)

    if (num/s.length >= factorial)// Optional. if the number is not in the range of [0, s.length! - 1] 
        return null;

    for(int i = 0; i < s.length - 1; i++){//go over the array

        int tempi = (num / factorial) % (s.length - i);//calculates the next cell from the cells left (the cells in the range [i, s.length - 1])
        E temp = s[i + tempi];//Temporarily saves the value of the cell needed to add to the permutation this time 

        for(int j = i + tempi; j > i; j--)//shift all elements to "cover" the "missing" cell
            s[j] = s[j-1];

        s[i] = temp;//put the chosen cell in the correct spot

        factorial /= (s.length - (i + 1));//updates the factorial

    }

    return s;
}
于 2009-02-07T14:23:28.617 回答
0

我非常抱歉(在评论中进行了所有这些澄清之后)说我找不到这个问题的有效解决方案。我尝试了过去一个小时没有结果。

原因(我认为)是这个问题与旅行商问题非常相似。除非您尝试所有组合,否则无法知道哪些属性将加起来达到阈值。

似乎没有什么聪明的技巧可以解决这类问题。

您仍然可以对实际代码进行许多优化。

尝试根据属性对数据进行排序。当您发现较高的值不能满足阈值时,您可以避免处理列表中的某些值(因此可以消除所有较低的值)。

于 2009-02-07T15:41:13.903 回答
0

这是一种计算这些子集数量的递归方法:我们定义一个函数count(minIndex,numElements,minSum)返回大小为 的子集的数量,numElements其总和至少为minSum,包含具有索引minIndex或更大的元素。

在问题陈述中,我们按降序对元素进行排序,例如 [3,3,2,1],并将第一个索引称为零,元素总数为 N。我们假设所有元素都是非负数。要找到总和至少为 5 的所有 2 子集,我们调用count(0,2,5).

示例代码(Java):

int count(int minIndex, int numElements, int minSum)
{
    int total = 0;

    if (numElements == 1)
    {
        // just count number of elements >= minSum
        for (int i = minIndex; i <= N-1; i++)
            if (a[i] >= minSum) total++; else break;
    }
    else
    {
        if (minSum <= 0)
        {
            // any subset will do (n-choose-k of them)
            if (numElements <= (N-minIndex))
                total = nchoosek(N-minIndex, numElements);
        }
        else
        {
            // add element a[i] to the set, and then consider the count
            // for all elements to its right
            for (int i = minIndex; i <= (N-numElements); i++)
                total += count(i+1, numElements-1, minSum-a[i]);
        }
    }

    return total;
}

顺便说一句,我已经用 40 个元素的数组和大小为 8 的子集运行了上述内容,并且始终在不到一秒的时间内获得了结果。

于 2009-02-07T15:46:55.220 回答
0

如果您使用 C#,这里有一个相当不错的泛型。请注意,尽管某些排列的生成不是按字典顺序

于 2009-02-07T15:54:30.040 回答