【问题标题】:Algorithm to generate a subset in order of its sum按总和的顺序生成子集的算法
【发布时间】:2017-09-01 02:11:30
【问题描述】:

我对一种算法感兴趣,该算法可以按总和的递增顺序生成或排序某些集合的子集。我已经查看了一些类似的问题,但他们只讨论按线性顺序生成子集,例如 Algorithm to generate k element subsets in order of their sumAlgorithm wanted: Enumerate all subsets of a set in order of increasing sums

有没有一种聪明的方法可以更快地做到这一点?

我之前尝试从所有子集生成一个区间树,然后沿着它搜索,其中根节点是集合中最右边的整数,左边的节点将最左边的整数向下移动一个,右边的节点附加下一个最大的整数.所以{1,3,5,8}

8 5 5,8 3 3,5 3,8 3,5,8 1 1,3 1,5 1,3,5 1,8 1,3,8 1,5,8 1,3,5,8

在任何节点上,左侧区间将是子集中的最小值,替换为左侧节点子集中的集合中的最小值,其中包括该左侧节点子集中最大元素左侧的所有元素。正确的间隔是相同的逻辑但镜像。如果目标总和不在某个区间内,则不要搜索子树。如果它在两个子树中,则搜索两者。这实际上可以在无需生成任何子树的情况下检索范围的情况下完成,因此不需要实际构建树,只需在每个步骤中构建每个节点。这种方法在一般情况下似乎有效,但在最坏的情况下却是指数级的。

有没有沿着这些思路的方法?

【问题讨论】:

  • 任何算法都需要指数级的时间:有 2^n 个子集。

标签: algorithm data-structures complexity-theory


【解决方案1】:

根据您的区间树示例和链接的答案,听起来好像您想要生成 k-th 最大子集而不生成先前的子集并且这样做的时间少于 O(k),如果我理解正确的话,因为你提到想要比以前的线性方法更快的东西。对此的解决方案将证明 P=NP,因为您可以通过在亚指数时间内生成每个 k-th 最大子集来对所有子集进行二分搜索。

几年前我曾解决过这个问题,尝试按总和顺序生成k-th 最大子集,然后按总和的顺序对子集进行二分搜索,所以也许我可以尝试解释一个基本问题这种方法,即某些子集组本质上是不可比较的,并且在最坏情况下必须比较的不可比较子集的数量随着输入大小的增长呈指数增长。

子集和问题的搜索空间是输入集的幂集。更具体地说,搜索空间是输入集的幂集的每个子集的总和。例如,对于输入集{1, 2, 3},搜索空间为{{1}, {2}, {1, 2}, {3}, {1, 3}, {2, 3}, {1, 2, 3}},或者简称为{1, 2, 3, 3, 4, 5, 6}。无论输入集如何,最小子集总和将始终是第一个元素的单例,下一个最小子集总和将始终是第二个元素的单例,并且下一个最小子集总和将始终是前两个元素的子集输入集的元素。类似地,最大子集总和将始终是整个输入集的总和,而下一个最大子集总和将始终是输入集没有第一个元素的总和,下一个最大子集总和将始终是输入集没有第二个元素,下一个最大的子集总和将始终是输入集没有第一个和第二个元素的总和。

但是回到之前的输入集{1, 2, 3},那么像{1, 2, 2}这样大小相同的输入集呢?搜索空间变为{{1}, {2}, {1, 2}, {2}, {1, 2}, {2, 2}, {1, 2, 2}}{1, 2, 3, 2, 4, 5, 6}。如果您尝试按集合{a, b, c} 的总和的顺序对幂集进行排序,那么您必须比较{a, b}{c},因为有些输入集{a, b} 较大,而另一些输入集@987654347 @ 更伟大。这两个子集是不可比的。如果你能保证一个总是大于另一个,那么你可以适当地设计一个搜索算法,但你不能,所以你至少必须检查这两个元素。

对于大小为 4 的输入集{a, b, c, d},还有两个不可比的子集:{a, d}{b, c};比较{1, 2, 4, 8}{1, 3, 3, 3} 的这些子集的总和。实际上,还有其他几组对偶不可比子集,例如{a, b, c}{b, d}。如果我们画一个Hasse diagram,我们会得到(为ASCII艺术道歉):

A B C D | b,c,d | a,c,d--------| | | a,b,d---|---c,d | | a,b,c b,d | | b,c a,d | | a,c----d --------| | a,b c-----| | | |------b | 一种

换句话说,你可以设计一个算法在{{a}, {b}, {a, b}, {a, c}, {b, c}, {a, b, c}, {a, b, d}, {a, c, d}, {b, c, d}, {a, b, c, d}}链上进行二分搜索,在{{c}, {d}, {a, d}, {b, d}, {c, d}}链上进行另一个二分搜索,或者在这些链的另一个配置上进行两次二分搜索,但最终你将必须执行至少两次二进制搜索。您始终可以保证a+b <= a+cb+d <= a+c+d(如b+d <= c+d <= a+c+d),但您不能保证例如b+c <= a+d。在最坏的情况下,您必须进行这些比较。

更进一步,对于大小为 5 的输入集 {a, b, c, d, e}{a, d}{b, c}{e} 的子集是无法比较的。例如:

{1, 2, 4, 8, 16}{b, c} <= {a, d} <= {e}

{1, 2, 2, 2, 5} 拥有{a, d} <= {b, c} <= {e}

{5, 5, 5, 6, 6} 拥有{e} <= {b, c} <= {a, d}

{1, 5, 5, 6, 6} 拥有{e} <= {a, d} <= {b, c}

{1, 4, 4, 4, 6} 拥有{a, d} <= {e} <= {b, c}

{1, 2, 2, 6, 6} 拥有{b, c} <= {e} <= {a, d}

将另一个 ASCII 哈斯图放在一起:

a,b,c,d,e | b,c,d,e | a,c,d,e--------------| | | a,b,d,e-------------------|---c,d,e | | a,b,c,e------------|----b,d,e | | | a,b,c,d a,d,e b,c,e | | | b,c,d d,e a,c,e | | | a,c,d--------|---c,e-----|---a,b,e | | | | a,b,d---|---c,d b,e-----------| | | | a,b,c b,d --a,e | | / | b,c -a,d---/ e | / | | a,c d----------| --------| | a,b c-----| | | |------b | 一种

在最坏的情况下,您将不得不进行 3 次二分搜索,因为不可比较集的最大分组是 3。

这里有一个模式。子集的总和形成偏序。对于大小为 3 的集合,此偏序的宽度(也称为maximum antichain)为 2。对于大小 4,它也是 2。对于大小 5,宽度为 3。对于大小为 6 的集合,宽度是5。7号的宽度是8。8号的宽度是14。9号的宽度是23。10号的宽度是40。11号的宽度是70。

事实上,这个整数序列是已知的。它在Online Encyclopedia of Integer Sequences A025591 中作为 +- 1 +- 2 +- 3 +- ... +- n = 0 或 1 的解数。这个整数序列也在Robert A. Proctor's 1982 paper "Solution of Two Difficult Combinatorial Problems with Linear Algebra," 中讨论过,其中的问题找到一组 n 个不同的正实数,其中包含尽可能多的具有相同总和的子集的集合显示为前 n 个正整数:{1,2,...,n}。 Proctor 给出了这个结果的第一个基本证明,只需要线性代数背景即可。与n=1, 2, ... 具有相同和的{1,2,...,n} 子集的最大数量是1、1、2、2、3、5、8、14、23 等,或OEIS A025591,与上面讨论的相同整数序列。事实上,这个序列在论文中的构建方式与上面讨论的相同。

回到识别可比较子集的问题,这个排序可以概括为三个案例的输入集的所有子集。给定任意输入集S 的两个子集AB

  1. 考虑cardinalityA 大于B 的基数的情况。那么就不能保证B之和大于A之和。
  2. 考虑A 的基数等于B 的基数的情况。对于i=0 to cardinality(A)中的每个元素A[i]B[i],如果S中从中提取A[i]的索引大于S中提取B[i]的索引,则不能保证B 的总和大于A 的总和。否则,可以保证B之和大于A之和。
  3. 考虑A 的基数小于B 的基数的情况。删除集合B 的最少元素,使得AB 的基数相等。现在可以应用第二种情况。

为了帮助说明这一点,我整理了一些代码,这些代码从输入集的幂集构建有向无环图,其中每条边将具有较小子集和的节点连接到具有较大子集和的所有节点。这个过程形成了一个传递闭包,因为所有较小的节点都将连接到所有较大的节点。然后将传递归约应用于该图,并返回最大反链的大小以及构成该反链的子集,格式为[index, value],通过遍历 Hasse 图并存储每个级别的宽度。最终图的最大反链等于整数序列A025591

(此代码很快被拼凑在一起,以证明我想说的话。对于做出的任何糟糕的编码决定,我提前道歉!)

import com.google.common.graph.*;
import java.util.*;

public class AntichainDecomposition {
  MutableGraph<Subset> graph;

  public static void main(String[] args) {
    // Input set. Modify this as needed.
    int[] set = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9};
    ArrayList<Subset> input = buildSubsets(set);
    AntichainDecomposition antichain = new AntichainDecomposition(input);
  }

  public AntichainDecomposition(ArrayList<Subset> input) {
    graph = GraphBuilder.directed().build();
    for (int i = 0; i < input.size(); ++i) {
      graph.addNode(input.get(i));
    }
    for (int i = 0; i < input.size(); ++i) {
      for (int j = 0; j < input.size(); ++j) {
        if (i != j && isTargetGreater(input.get(i), input.get(j))) {
          graph.putEdge(input.get(i), input.get(j));
        }
      }
    }
    graphReduction();
    int width = getWidth(input.get(input.size() / 2));
    System.err.println(width);
  }

  private int getWidth(Subset first) {
    HashMap<Integer, HashSet<Subset>> levelMap = new HashMap<Integer, HashSet<Subset>>();
    HashMap<Subset, Integer> subsetToLevel = new HashMap<Subset, Integer>();
    int level = 1;

    // Mark all the vertices as not visited
    HashMap<Subset, Boolean> visited = new HashMap<Subset, Boolean>();
    Iterator iter = graph.nodes().iterator();
    while (iter.hasNext()) {
      Subset node = (Subset)iter.next();
      visited.put(node, false);
    }

    // Create a queue for breadth first search
    LinkedList<Subset> queue = new LinkedList<Subset>();

    // Mark the current node as visited and enqueue it
    levelMap.put(level, new HashSet<Subset>());
    levelMap.get(level).add(first);
    subsetToLevel.put(first, level);
    visited.put(first, true);
    queue.add(first);

    while (queue.size() != 0) {
      // Dequeue a vertex from the queue and store it in the appropriate level
      Subset s = queue.poll();
      level = subsetToLevel.get(s);

      // Get all adjacent vertices of the dequeued vertex s
      // If a successor has not been visited, then mark it
      // visited and enqueue it
      iter = graph.successors(s).iterator();
      while (iter.hasNext()) {
        Subset n = (Subset)iter.next();
        if (!visited.get(n)) {
          if (!levelMap.containsKey(level + 1)) {
            levelMap.put(level + 1, new HashSet<Subset>());
          }
          levelMap.get(level + 1).add(n);
          subsetToLevel.put(n, level + 1);
          visited.put(n, true);
          queue.add(n);
        }
      }
    }

    int width = Integer.MIN_VALUE;
    iter = levelMap.values().iterator();
    Iterator subsetIter = null;
    while (iter.hasNext()) {
      HashSet<Subset> levelSet = (HashSet<Subset>)iter.next();
      if (levelSet.size() > width) {
        width = levelSet.size();
        subsetIter = levelSet.iterator();
      }
    }

    if (subsetIter != null) {
      while (subsetIter.hasNext()) {
        System.out.println((Subset)subsetIter.next());
      }
    }

    return width;
  }

  private void graphReduction() {
    // Reflexive reduction
    Iterator iter1 = graph.nodes().iterator();
    while (iter1.hasNext()) {
      Subset i = (Subset)iter1.next();
      graph.removeEdge(i, i);
    }

    // Transitive reduction
    iter1 = graph.nodes().iterator();
    while (iter1.hasNext()) {
      Subset j = (Subset)iter1.next();
      Iterator iter2 = graph.nodes().iterator();
      while (iter2.hasNext()) {
        Subset i = (Subset)iter2.next();
        if (graph.removeEdge(i, j)) {
          graph.putEdge(i, j);
          Iterator iter3 = graph.nodes().iterator();
          while (iter3.hasNext()) {
            Subset k = (Subset)iter3.next();
            if (graph.removeEdge(j, k)) {
              graph.putEdge(j, k);
              graph.removeEdge(i, k);
            }
          }
        }
      }
    }
  }

  private Stack<Subset> topologicalSort() {
    Stack<Subset> stack = new Stack<Subset>();
    int vertices = graph.nodes().size();

    // Mark all the vertices as not visited
    HashMap<Subset, Boolean> visited = new HashMap<Subset, Boolean>();
    Iterator iter = graph.nodes().iterator();
    while (iter.hasNext()) {
      Subset node = (Subset)iter.next();
      visited.put(node, false);
    }

    // Call the recursive helper function to store topological sort
    // starting from all vertices one by one
    iter = graph.nodes().iterator();
    while (iter.hasNext()) {
      Subset node = (Subset)iter.next();
      if (!visited.containsKey(node) || !visited.get(node)) {
        topologicalSortHelper(node, visited, stack);
      }
    }

    return stack;
  }

  private void topologicalSortHelper(Subset v, HashMap<Subset, Boolean> visited, Stack<Subset> stack) {
    visited.put(v, true);

    // Recurse for all the vertices adjacent to this vertex
    Iterator iter = graph.successors(v).iterator();
    while (iter.hasNext()) {
      Subset node = (Subset)iter.next();
      if (!visited.containsKey(node) || !visited.get(node)) {
        topologicalSortHelper(node, visited, stack);
      }
    }

    // Push current vertex to stack which stores topological sort
    stack.push(v);
  }

  private boolean isTargetGreater(Subset source, Subset target) {
    // An edge between two nodes exists if each index in the target subset is greater than or
    // equal to its respective index in the source subset. If the target subset size is greater
    // than the source subset size, then an edge between the two subsets exists if and only if
    // the target subset has indices that are greater than or equal to corresponding indices of
    // the source subset, ignoring the additional indices of the target subset.
    if (source.size() > target.size()) {
      return false;
    }
    SubsetEntry[] newSubset = new SubsetEntry[target.size()];
    System.arraycopy(target.getSubset(), 0, newSubset, 0, newSubset.length);
    Subset newTarget = new Subset(Arrays.asList(newSubset).subList(target.size() -
                                                                   source.size(), target.size()).
                                    toArray(new SubsetEntry[source.size()]));
    for (int i = 0; i < source.size(); ++i) {
      if (source.getEntry(i).getIndex() > newTarget.getEntry(i).getIndex()) {
        return false;
      }
    }
    return true;
  }

  private static ArrayList<Subset> buildSubsets(int[] set) {
    ArrayList<Subset> power = new ArrayList<Subset>();
    int elements = set.length;
    int powerElements = (int) Math.pow(2, elements);
    for (int i = 0; i < powerElements; ++i) {
      // Convert the binary number to a string containing n digits
      String binary = intToBinary(i, elements);

      // Create a new set
      ArrayList<SubsetEntry> innerSet = new ArrayList<SubsetEntry>();

      // Convert each digit in the current binary number to the corresponding element
      // in the given set
      for (int j = 0; j < binary.length(); ++j) {
        if (binary.charAt(j) == '1') {
          innerSet.add(new SubsetEntry(j, set[j]));
        }
      }

      // Add the new set to the power set
      if (!innerSet.isEmpty()) {
        power.add(new Subset(innerSet.toArray(new SubsetEntry[innerSet.size()])));
      }
    }
    return power;
  }

  private static String intToBinary(int binary, int digits) {
    String temp = Integer.toBinaryString(binary);
    int foundDigits = temp.length();
    String returner = temp;
    for (int i = foundDigits; i < digits; ++i) {
      returner = "0" + returner;
    }
    return returner;
  }
}

class SubsetEntry {
  private int index;
  private int value;

  public SubsetEntry(int i, int v) {
    index = i;
    value = v;
  }

  public int getIndex() {
    return index;
  }

  public int getValue() {
    return value;
  }

  public String toString() {
    return "[" + index + ", " + value + "]";
  }
}

class Subset {
  private SubsetEntry[] entries;

  public Subset(SubsetEntry[] e) {
    entries = new SubsetEntry[e.length];
    System.arraycopy(e, 0, entries, 0, entries.length);
  }

  public void setSubset(SubsetEntry[] subset) {
    entries = new SubsetEntry[subset.length];
    System.arraycopy(subset, 0, entries, 0, subset.length);
  }

  public SubsetEntry[] getSubset() {
    return entries;
  }

  public SubsetEntry getEntry(int index) {
    return entries[index];
  }

  public int size() {
    return entries.length;
  }

  public String toString() {
    String s = "{";
    for (int i = 0; i < entries.length; ++i) {
      s += entries[i].toString();
    }
    s += "}";
    return s;
  }
}

根据Dilworth's Theorem,对于任何偏序集,最大反链的基数等于可用于覆盖偏序集的最小链数。对于任何输入集,在最坏的情况下,这会产生带有A025591 链的偏序。此外,搜索偏序的最坏情况时间是O(w*log(n)),其中 w 是图的宽度(等于最大反链的基数)。这可以通过以下事实来证明:反链被描述为一个无序列表,其中没有两个元素是可比的,搜索无序列表的最坏情况时间是O(n)。此外,链的特征是一个有序列表,其中每个元素都可以与列表中的所有其他元素进行比较,搜索有序列表的最坏情况时间是O(log n)。因此,对于长度为wlog(n) 的反链中的每个元素,必须在最坏的情况下进行相应链中的比较,从而在最坏的情况下对任何偏序进行O(w*log(n)) 的搜索时间。

这个偏序搜索时间为任意输入集的每个子集的总和的排序提供了最坏情况的表征。这是因为,对于任何输入集,都需要观察反链中的每个总和才能推导出最优搜索树。回想一下,以您的原始集合 {1, 3, 5, 8} 为例,索引 1 和 2 处的子集 {3, 5} 的总和小于索引 0 和 3 处的子集 {1, 8} 的总和。但是,对于集合 @ 987654417@,子集 {2, 3} 在索引 1 和 2 处的总和大于子集 {1, 3} 在索引 0 和 3 处的总和。索引集 {1, 2}{0, 3} 随后是不可比较的。随着集合的扩展,这种不可比性的顺序以A025591 中定义的指数速率增长。

我将通过假设输入集中使用的所有数字都是正数并且输入集已排序来结束这一点。事实上,如果您使用的是未排序的列表或正负数的混合体,则无法保证两个元素具有可比性。

如果这个答案冗长且漫无边际,我深表歉意,但我希望这有助于您深入了解您要解决的问题。

【讨论】:

  • 哇!感谢您冗长、周到的回答和澄清!这很有帮助!
【解决方案2】:

如果您的集合只能包含 个整数,那么直接的方法是使用优先级队列,如下所示:

  1. 将空集 {} 添加到队列中
  2. 从队列中取出总和最小的集合并输出
  3. 枚举所有可以通过将一个元素添加到您删除的集合来创建的集合,如果之前没有添加它们,则将它们添加到队列中。
  4. 如果队列不是空的,返回2。

这基本上在子集图上使用了 Dijkstra 算法,其中每个集合都是一个顶点,边都是增量方式,以生成增加总和的新集合。

如果您的集合中可以有负整数,您仍然可以使用上述的变体。只需确保从可能的最小子集开始 - 包含所有负数的子集,然后在步骤 3 中,您将通过所有增量方式来获得更大的总和,这意味着要么添加一个正数,要么删除一个负数.

【讨论】:

  • 感谢您的回答!我知道这样的算法,并且如果可以更快地生成特定的子集,我更感兴趣。
  • 哪个特定子集?具有特定总和的那个(例如 0-1 背包问题)?
猜你喜欢
  • 1970-01-01
  • 2012-02-10
  • 1970-01-01
  • 1970-01-01
  • 2010-10-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多