根据您的区间树示例和链接的答案,听起来好像您想要生成 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+c 或b+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 的两个子集A 和B:
- 考虑cardinality 的
A 大于B 的基数的情况。那么就不能保证B之和大于A之和。
- 考虑
A 的基数等于B 的基数的情况。对于i=0 to cardinality(A)中的每个元素A[i]和B[i],如果S中从中提取A[i]的索引大于S中提取B[i]的索引,则不能保证B 的总和大于A 的总和。否则,可以保证B之和大于A之和。
- 考虑
A 的基数小于B 的基数的情况。删除集合B 的最少元素,使得A 和B 的基数相等。现在可以应用第二种情况。
为了帮助说明这一点,我整理了一些代码,这些代码从输入集的幂集构建有向无环图,其中每条边将具有较小子集和的节点连接到具有较大子集和的所有节点。这个过程形成了一个传递闭包,因为所有较小的节点都将连接到所有较大的节点。然后将传递归约应用于该图,并返回最大反链的大小以及构成该反链的子集,格式为[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)。因此,对于长度为w、log(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 中定义的指数速率增长。
我将通过假设输入集中使用的所有数字都是正数并且输入集已排序来结束这一点。事实上,如果您使用的是未排序的列表或正负数的混合体,则无法保证两个元素具有可比性。
如果这个答案冗长且漫无边际,我深表歉意,但我希望这有助于您深入了解您要解决的问题。