【问题标题】:Generate combinations ordered by an attribute生成按属性排序的组合
【发布时间】:2009-02-07 13:52:43
【问题描述】:

我正在寻找一种方法来生成按单个属性排序的对象组合。我不认为字典顺序是我正在寻找的......我会试着举一个例子。假设我有一个对象 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)。我实际上并不需要这些组合本身。

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

谢谢

【问题讨论】:

  • 你能解释一下阈值吗?例如:3+2 等?
  • 阈值是属性的总和。在这种情况下,它将是,例如5,所以我只需要组合A3 B3,A3 C2,B3 C2。谢谢,Željko
  • 那么您的实际需求是什么?超过特定阈值的组合计数,还是实际组合本身?
  • 我明白了,我还不够清楚,抱歉。我需要超过阈值的组合计数,但按总和分组。在上述情况下,我感兴趣的信息是我有 1 个组合给出的总和为 6,而 2 的总和为 5。除了找到组合之外,我找不到其他方法。
  • 很抱歉再问一次,您能解释一下“40 个一组,8 个顺序”的事情吗?

标签: sum combinatorics combinations


【解决方案1】:

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

  • 根据您的描述,我不清楚 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 方法触发(大部分)标准递归遍历来自valuesn 整数的唯一组合。 limit 参数给出了利息总和的下限。

countAt 方法检查来自values 的整数组合。 left 参数是剩余多少整数来构成一个总和中的 n 整数,start 是要搜索的 values 中的位置,sum 是部分总和。

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

countAt 的递归在积累了正确的人口时触底;这会将当前的sumn 值)添加到tally。如果countAt还没有触底,则从start-ing位置扫过values,增加当前部分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

这是 Tally 代码:

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();
    }
}

【讨论】:

  • 嗨,A,B...D 仅用于澄清我可以拥有多个具有相同值的对象(另一件事,为什么我认为子集总和的动态解决方案不起作用本例)。
  • 另外 - 你重新定义我的问题几乎是正确的:) 稍作修正 - 对于集合 (5,6) 中的每个整数,集合 {3, 3, 2 中有多少不同的对, 1} 具有每个整数的总和。
  • 哇,谢谢!对于代码和全面的解释!写这篇文章花费了大量的精力 :-) 我现在就试试看 :-) 正如我所说的,我是新来的,只有 11 分(投票需要 15 分),但我相信你的答案可能是能想到的最好的。
【解决方案2】:

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

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

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

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

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

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

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

要了解该课程并下载代码,请参阅Tablizing The Binomial Coeffieicent

【讨论】:

    【解决方案3】:

    在 stackoverflow 中查看这个问题:Algorithm to return all combinations

    我也只是使用下面的 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;
    }
    

    【讨论】:

    • 您好,感谢您的评论。不幸的是,找到所有组合是不可接受的,因为这需要很多时间,而且这是时间关键的操作......
    • 对不起,我没有很好地传达这段代码。此代码将为每个 num (索引)生成一个唯一组合,该 num 在所有组合的集合中具有一个整数值。因此,如果您使用 x 个唯一 num 调用此函数,您将获得 x 个唯一排列。
    • 嗨,是的 - 这很清楚 :) 但是,由于我需要隔离高于阈值的所有组合(我最初的问题不是很精确 - 请参阅编辑),我将不得不多次调用该函数来获取所有组合,然后隔离我需要的组合。我一直在寻找避免这种情况的方法。
    【解决方案4】:

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

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

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

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

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

    【讨论】:

    • 感谢您的尝试 :) 我实际上正在做与您上一个建议类似的事情,但希望我能找到更好的解决方案...
    • 哈哈。与我们共同创造的价值相比,投票算不了什么。
    【解决方案5】:

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

    【讨论】:

      【解决方案6】:

      这是一种计算这些子集数量的递归方法:我们定义了一个函数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 的子集运行了上述操作,并且始终在不到一秒的时间内获得了结果。

      【讨论】:

      • 你好 Zach,是的 - 但我想找到这些子集而不枚举它们
      • @zljk:没错。这种方法只计算它们而不枚举实际的子集。这使我们能够“短路”许多递归步骤。
      • @Niyaz:我一般同意,但是对于这个问题大小,递归似乎做得很好。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-02
      • 1970-01-01
      相关资源
      最近更新 更多