如何构建具有O(n)空间复杂度的树?

时间:2016-07-16 19:11:15

标签: java algorithm tree space-complexity subset-sum

问题

给定一组整数,找到总和为100,000,000的整数的子集。

解决方案

我正在尝试构建一个包含给定集合的所有组合以及总和的树。例如,如果给定集看起来像0,1,2,我将构建以下树,检查每个节点的总和:

                    {}
        {}                      {0}
  {}         {1}         {0}          {0,1}
{}  {2}  {1}   {1,2}  {0}   {2}   {0,1}   {0,1,2}

由于我在每个节点都保留了整数数组和总和,我应该只需要内存中树的底部(当前)级别。

问题

我当前的实现将整个树保留在内存中,因此使用的堆空间太多了。

如何更改当前的实现,以便GC处理我的上层树级?

(目前我只是在找到目标总和时抛出RuntimeException,但这显然只是为了玩游戏)

public class RecursiveSolver {
    static final int target = 100000000;
    static final int[] set = new int[]{98374328, 234234123, 2341234, 123412344, etc...};

    Tree initTree() {
        return nextLevel(new Tree(null), 0);
    }

    Tree nextLevel(Tree currentLocation, int current) {
        if (current == set.length) { return null; }
        else if (currentLocation.sum == target) throw new RuntimeException(currentLocation.getText());
        else {
            currentLocation.left = nextLevel(currentLocation.copy(), current + 1);
            Tree right = currentLocation.copy();
            right.value = add(currentLocation.value, set[current]);
            right.sum = currentLocation.sum + set[current];
            currentLocation.right = nextLevel(right, current + 1);
            return currentLocation;
        }
    }

    int[] add(int[] array, int digit) {
        if (array == null) {
            return new int[]{digit};
        }
        int[] newValue = new int[array.length + 1];
        for (int i = 0; i < array.length; i++) {
            newValue[i] = array[i];
        }
        newValue[array.length] = digit;
        return newValue;
    }

    public static void main(String[] args) {
        RecursiveSolver rs = new RecursiveSolver();
        Tree subsetTree = rs.initTree();
    }
}

class Tree {
    Tree left;
    Tree right;
    int[] value;
    int sum;

    Tree(int[] value) {
        left = null;
        right = null;
        sum = 0;
        this.value = value;
        if (value != null) {
            for (int i = 0; i < value.length; i++) sum += value[i];
        }
    }

    Tree copy() {
        return new Tree(this.value);
    }
}

3 个答案:

答案 0 :(得分:1)

问题是NP-complete

如果您真的想提高性能,那么您必须忘记树的实现。您必须只生成所有子集并将它们相加或使用动态编程。

选择取决于要求的元素数量和要实现的总和。你知道它是100,000,000的总和,bruteforce指数算法在O(2^n * n)时运行,所以对于低于22的数字,这是有意义的。

在python中,您可以通过简单的方式实现此目的:

def powerset(iterable):
    "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"
    s = list(iterable)
    return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))

通过在中间技术中使用meet,可以显着提高这种复杂性(牺牲内存)(阅读wiki article)。这会将其降低到O(2^(n/2)),这意味着它将比n <~ 53

的DP解决方案表现更好

答案 1 :(得分:1)

在这里构建树所需的时间和空间绝对没有任何内容

原因是,如果你被给予

  • 树的节点
  • 节点的深度
  • 有序的输入元素数组

您可以使用O(1)操作计算其父,左和右子节点。并且当您遍历树时,您可以访问这些内容,因此您不需要任何其他内容。

答案 2 :(得分:0)

在仔细考虑了erip的评论之后,我意识到他是正确的 - 我不应该使用树来实现这个算法。

暴力通常为O(n*2^n),因为n子集有2^n个补充。 因为我每个节点只做一次加法,我提出的解决方案是O(2^n),其中n是给定集合的大小。此外,这个算法只有O(n)个空格复杂。由于我的特定问题中原始集合中的元素数量很少(大约25)O(2^n)复杂性不是太大的问题。

此问题的动态解决方案是O(t*n),其中t是目标总和,n是元素数量。因为t在我的问题中非常大,所以动态解决方案最终会有很长的运行时间和高内存使用率。

这在我的机器上大约311毫秒完成了我的特定解决方案,这比我在这类特殊问题上看到的动态编程解决方案有了很大的改进。

public class TailRecursiveSolver {
    public static void main(String[] args) {
        final long starttime = System.currentTimeMillis();
        try {
            step(new Subset(null, 0), 0);
        }
        catch (RuntimeException ex) {
            System.out.println(ex.getMessage());
            final long endtime = System.currentTimeMillis();
            System.out.println(endtime - starttime);
        }
    }

    static final int target = 100000000;
    static final int[] set = new int[]{ . . . };

    static void step(Subset current, int counter) {
        if (current.sum == target) throw new RuntimeException(current.getText());
        else if (counter == set.length) {}
        else {
            step(new Subset(add(current.subset, set[counter]), current.sum + set[counter]), counter + 1);
            step(current, counter + 1);
        }
    }

    static int[] add(int[] array, int digit) {
        if (array == null) {
            return new int[]{digit};
        }
        int[] newValue = new int[array.length + 1];
        for (int i = 0; i < array.length; i++) {
            newValue[i] = array[i];
        }
        newValue[array.length] = digit;
        return newValue;
    }
}

class Subset {
    int[] subset;
    int sum;

    Subset(int[] subset, int sum) {
        this.subset = subset;
        this.sum = sum;
    }

    public String getText() {
        String ret = "";
        for (int i = 0; i < (subset == null ? 0 : subset.length); i++) {
            ret += " + " + subset[i];
        }
        if (ret.startsWith(" ")) {
            ret = ret.substring(3);
            ret = ret + " = " + sum;
        } else ret = "null";
        return ret;
    }
}

编辑 -

上述代码仍在O(n*2^n)时间运行 - 因为add方法在O(n)时间内运行。以下代码将在真正的O(2^n)时间运行,并且性能更高,在我的机器上完成大约20毫秒。

由于将当前子集存储为long中的位,因此限制为少于64个元素。

public class SubsetSumSolver {
    static boolean found = false;
    static final int target = 100000000;
    static final int[] set = new int[]{ . . . };

    public static void main(String[] args) {
        step(0,0,0);
    }

    static void step(long subset, int sum, int counter) {
        if (sum == target) {
            found = true;
            System.out.println(getText(subset, sum));
        }
        else if (!found && counter != set.length) {
            step(subset + (1 << counter), sum + set[counter], counter + 1);
            step(subset, sum, counter + 1);
        }
    }

    static String getText(long subset, int sum) {
        String ret = "";
        for (int i = 0; i < 64; i++) if((1 & (subset >> i)) == 1) ret += " + " + set[i];
        if (ret.startsWith(" ")) ret = ret.substring(3) + " = " + sum;
        else ret = "null";
        return ret;
    }
}

编辑2 -

这是另一个版本在中间攻击中使用一次会议,以及一点点移动,以便将复杂度从O(2^n)降低到O(2^(n/2))

如果要将其用于32到64个元素之间的集合,则应将表示步骤函数中当前子集的int更改为long,但性能会明显显着降低设定的大小增加。如果要将其用于具有奇数个元素的集合,则应该在集合中添加0以使其成为偶数。

import java.util.ArrayList;
import java.util.List;

public class SubsetSumMiddleAttack {
    static final int target = 100000000;
    static final int[] set = new int[]{ ... };

    static List<Subset> evens = new ArrayList<>();
    static List<Subset> odds = new ArrayList<>();

    static int[][] split(int[] superSet) {
        int[][] ret = new int[2][superSet.length / 2]; 

        for (int i = 0; i < superSet.length; i++) ret[i % 2][i / 2] = superSet[i];

        return ret;
    }

    static void step(int[] superSet, List<Subset> accumulator, int subset, int sum, int counter) {
        accumulator.add(new Subset(subset, sum));
        if (counter != superSet.length) {
            step(superSet, accumulator, subset + (1 << counter), sum + superSet[counter], counter + 1);
            step(superSet, accumulator, subset, sum, counter + 1);
        }
    }

    static void printSubset(Subset e, Subset o) {
        String ret = "";
        for (int i = 0; i < 32; i++) {
            if (i % 2 == 0) {
                if ((1 & (e.subset >> (i / 2))) == 1) ret += " + " + set[i];
            }
            else {
                if ((1 & (o.subset >> (i / 2))) == 1) ret += " + " + set[i];
            }
        }
        if (ret.startsWith(" ")) ret = ret.substring(3) + " = " + (e.sum + o.sum);
        System.out.println(ret);
    }

    public static void main(String[] args) {
        int[][] superSets = split(set);

        step(superSets[0], evens, 0,0,0);
        step(superSets[1], odds, 0,0,0);

        for (Subset e : evens) {
            for (Subset o : odds) {
                if (e.sum + o.sum == target) printSubset(e, o);
            }
        }
    }
}

class Subset {
    int subset;
    int sum;

    Subset(int subset, int sum) {
        this.subset = subset;
        this.sum = sum;
    }
}