从1到N的所有数字的总和始终为零

时间:2019-03-26 09:59:45

标签: java algorithm

问题是我必须打印序列的所有组合 1 to N中的数字将始终为零。允许 在每个之间插入"+"(用于加法)和"-"(用于减法) 数字,以便结果为零。

//Output
N = 7

1 + 2 - 3 + 4 - 5 - 6 + 7 = 0
1 + 2 - 3 - 4 + 5 + 6 - 7 = 0
1 - 2 + 3 + 4 - 5 + 6 - 7 = 0
1 - 2 - 3 - 4 - 5 + 6 + 7 = 0

那么我该如何实现呢?我不是要实际 代码来做到这一点,只是一个提示和想法来解决这个问题 做。谢谢你。

8 个答案:

答案 0 :(得分:10)

您还可以在此处使用递归。只要记住您当前的整数,您的最大整数,您的当前总和以及某种运算历史(也可以是您的最终序列)即可。 在每个级别中,您都按照两个方向进行操作:将总和加减。

我使用Python进行了快速实现,但是将其轻松转移到Java或任何您使用的东西应该很容易。

def zero_sum(curr, n, seq, sum):
    if curr == n and sum == 0:
        print(seq)
    elif curr < n:
        zero_sum(curr + 1, n, seq + " - " + str(curr + 1), sum - (curr + 1))
        zero_sum(curr + 1, n, seq + " + " + str(curr + 1), sum + (curr + 1))

zero_sum(1, 7, "1", 1)

希望您能明白。

答案 1 :(得分:4)

第一步是将问题转变为完全规则的问题:

 n
 ∑  ±i = -1
i=2

n-2
 ∑  ±(i+2) = -1
i=0

开头的术语1没有前缀+/-。使用Java数组时,步行索引最好从0开始。

因此,对于可能的值,它具有n-1个系数-1或+1。

蛮力方法是从最高值i = n-2开始。

j = 0,...,i的上限/下限为±(i + 1)*(2 + i + 2)/ 2,因此您可以在此处削减评估-计算直到,总和不能再达到-1。

要表示这些系数,可以制作一个new int[n - 1]或简单地制作一个new BitSet(n-1)

public void solve(int n) {
    int i = n-2;
    int sumDone = 0;
    BigSet negates = new BitSet(n - 1);
    solveRecursively(i, sumDone, negates);
}

private void solveRecursively(int i, int SumDone, BitSet negates) {
    if (i < 0) {
        if (sumDone == -1) {
            System.out.println("Found: " + negates);
        }
        return;
    }
    ...
}

我留给您的有趣,实际的(家庭)工作。 (使用BitSet更好,但i = n,...,2 by -1似乎更简单。)

答案 2 :(得分:4)

这里的问题是效率有多重要。如果您愿意采用蛮力方法,则可以使用holidayfun所示的回归方法,尽管随着n变大,它将变得笨拙。

如果性能速度很重要,那么可能首先需要做一些数学运算。最简单,最有意义的检查是是否还可以得出这样的总和:因为前n个自然数的总和为n(n + 1)/ 2,并且由于您希望将其分为两组(“正”组)和大小相等的“负”组),则必须使n(n + 1)/ 4为整数。因此,如果n和n + 1都不能被4整除,则停止。您找不到这样的序列加零。

如果速度至关重要,则此数学技巧和其他一些数学技巧可能会大大加快您的应用程序的速度。例如,找到一个解决方案通常会帮助您找到大n的其他解决方案。例如,如果n = 11,则{-11,-10,-7,-5}是一种解决方案。但是,我们可以将-5交换为任意组合,这些组合增加了5个不在我们集合中的组合。因此{-11,-10,-7,-3,-2}也是一个解决方案,对于-7同样,给出{-11,-10,-5,-4,-3}作为解决方案(我们不允许使用-1,因为1必须为正数)。我们可以继续替换-10,-11和它们的组件,以选择另外六个解决方案。

这可能是我解决这个问题的方式。使用贪婪算法查找“最大”解决方案(使用最大可能数的解决方案),然后继续将该解决方案的组成部分拆分为多个较小的解决方案。从根本上讲,这也是递归问题,但是它的运行时间随所考虑的组件的大小而减少,如果存在“较小”的解决方案,则在每个步骤中都会产生另一种解决方案。话虽如此,如果您想要每个解决方案,那么您仍然必须检查拆分的非贪婪组合(否则,您会在n中错过{-7,-4,-3}之类的解决方案= 7例)。如果您只想很多解决方案,那肯定会更快。但是要获得全部 ,它可能比暴力破解更好。

答案 3 :(得分:3)

如果我是您,我将寻求图形实现和DFS算法。假设您有N个节点代表您的数字。每个数字都通过“加”边或“减”边连接到另一个。因此,您有一个完全连接的图。您可以从节点开始计算所有导致零的dfs路径。

有关DFS算法的更多信息,请参见wikipage

编辑:为了阐明我的解决方案,您最终得到的图将是多图,这意味着它在节点之间具有多个边。多重图中的DFS稍微复杂一些,但并不难。

答案 4 :(得分:2)

我建议一个简单的解决方案,因为正如您提到的,您要处理的是固定的1到N的连续整数。唯一不同的是两者之间的运算符。

在实施通用解决方案之前,让我们看一下您的示例:

对于n = 7,您需要以某种方式产生所有可能的组合:

1+2+3+4+5+6+7
1+2+3+4+5+6-7
1+2+3+4+5-6+7
1+2+3+4+5-6-7
...
1-2-3-4-5-6+7
1-2-3-4-5-6-7

如果我们从上面的字符串/表达式中删除数字,那么我们将:

++++++
+++++-
++++-+
++++--
...
----+-
-----+
------

提醒二进制数字;如果我们将+解释为0,将-解释为1,则可以将以上内容映射到从000000111111的二进制数。

对于输入n,您之间将有n-1个运算符,这意味着所有可能组合的计数将为2^n-1

将以上所有内容放在一起,如下所示可用于打印总和为零的内容:

public static void main(String args[]) throws IOException{
    permute(7);
}
public static void permute(int n){
    int combinations = (int)Math.pow(2, n-1);
    for(int i = 0; i < combinations; i++){
        String operators =String.format("%"+(n-1)+"s", Integer.toBinaryString(i)).replace(' ', '0');

        int totalSum = 1;
        StringBuilder sb = new StringBuilder();

        for(int x = 0; x< operators.length(); x++){
            sb.append(x+1);
            if(operators.charAt(x)=='0'){
                sb.append("+");
                totalSum = totalSum + (x+2);
            }
            else{
                sb.append("-");
                totalSum = totalSum-(x+2);
            }                
        }
        sb.append(n);
        if(totalSum == 0){
            System.out.println(sb.toString() + " = " + totalSum);
        }
    }
}

注释/示例:String.format("%6s", Integer.toBinaryString(13)).replace(' ', '0')将由13的二进制表示形式生成一个长度为6的字符串,其前导零,即001101而不是1101,以便我们获得所需的运算符长度。

答案 5 :(得分:1)

这是一个有趣的问题。与编程相比,它涉及的数学更多,因为只有发现数学部分,然后才能实现高效的算法。

但是,即使在上数学之前,我们也必须真正理解问题的确切含义。这个问题可以改写为

给出数组[1..n],找到总和相等的所有可能的两组(2个子数组)。

所以规则;

    [1..n]
  1. 总和是n*n(+1)/2
  2. 如果n*(n+1)/2为奇数,则没有解决方案。
  3. 如果目标总和为t,则不应进一步迭代比Math.ceil((Math.sqrt(8*t+1)-1)/2)低的值(通过从n方程求解n(n+1)/2 = t

对不起...我知道这个问题需要Java代码,但是我不太熟练Java,因此下面的代码是JavaScript。很好,尽管我们可以看到结果。另外,如果您要转译,请随时编辑我的答案以包括Java版本。

这是代码;

function s2z(n){
    function group(t,n){                           // (t)arget (n)umber
        var e = Math.ceil((Math.sqrt(8*t+1)-1)/2), // don't try after (e)nd
            r = [],                                // (r)esult
            d;                                     // (d)ifference
        while (n >= e){
            d = t-n;
            r = d ? r.concat(group(d, d < n ? d : n-1).map(s => s.concat(n)))
                  : [[n]];
            n--;
        }
        return r;
    }
    var sum = n*(n+1)/2;              // get the sum of series [1..n] 
    return sum & 1 ? "No solution..!" // if target is odd then no solution
                   : group(sum/2,n);
}
console.log(JSON.stringify(s2z(7)));

因此结果应为[[1,6,7],[2,5,7],[3,4,7],[1,2,4,7],[3,5,6],[1,2,5,6],[1,3,4,6],[2,3,4,5]]

这是什么意思..?如果您仔细研究,您会发现

  1. 这些是所有可能的组,总计最多14个(28个一半,即[1..7]的总和。)
  2. 第一个组(索引为0)由最后一个组(索引为length-1)补充;第二个组以倒数第二个作为补充,依此类推...

现在我们有了中间结果,这取决于我们如何显示它们。这是次要和琐碎的问题。我的选择很简单,如下所示。

var arr = [[1,6,7],[2,5,7],[3,4,7],[1,2,4,7],[3,5,6],[1,2,5,6],[1,3,4,6],[2,3,4,5]],
    res = arr.reduce((r,s,i,a) => r+s.join("+")+"-"+a[a.length-1-i].join("-")+" = 0 \n","");
    
console.log(res);

当然,您可以将数字排序或停止一半,以防止第二个补码取正值,而第一个补码取负值。

此算法未经严格测试,我可能忽略了一些优势,但我认为这应该是一种非常有效的算法。我已经在非常合理的时间内计算出[1..28],导致将2399784个唯一身份组配对。尽管这是一种递归方法,但只为构造的结果集分配内存。

答案 6 :(得分:0)

这是一个很好的问题,但是首先您必须尝试解决它,并向我们展示您的尝试,这样我们才能在解决方案中为您提供帮助,这样您将可以更有效地进行改进。

但是,下面的代码是我几年前写的一个解决方案,我认为代码需要改进,但这会有所帮助。

public static void main(String[] args) {
    String plus = " + ", minus = " - ";
    Set<String> operations = new HashSet<>();
    operations.add("1" + plus);
    operations.add("1" + minus);

    // n >= 3
    int n = 7;

    for (int i = 1; i < n - 1; i++) {
        Set<String> newOperation = new HashSet<>();
        for (String opt : operations) {
            if ((i + 2) == n) {
                newOperation.add(opt + (i + 1) + plus + n);
                newOperation.add(opt + (i + 1) + minus + n);
            } else {
                newOperation.add(opt + (i + 1) + plus);
                newOperation.add(opt + (i + 1) + minus);
            }
        }
        operations.clear();
        operations.addAll(newOperation);
    }
    evalOperations(operations);
}

private static void evalOperations(Set<String> operations) {

    // from JDK1.6, you can use the built-in Javascript engine.
    ScriptEngineManager mgr = new ScriptEngineManager();
    ScriptEngine engine = mgr.getEngineByName("JavaScript");
    try {
        for (String opt : operations) {
            if ((int) engine.eval(opt) == 0) {
                System.out.println(opt + " = 0");
            }
        }
    } catch (ScriptException e) {
        e.printStackTrace();
    }
} 

答案 7 :(得分:0)

首先,问题是N的和的特殊情况。 第二,将一个列表加到N,可以划分为第一个元素加子列表和减子列表。 第三,如果列表中只有一个元素,请检查n是否等于该元素。 第四,进行递归。

这是scala的实现,请尝试完成Java版本:

def nSum(nums: List[Int], n: Int, seq: String, res: ListBuffer[String]): Unit =
  nums match {
    case Nil => if (n == 0) res.append(seq)
    case head :: tail => {
      nSum(tail, n - head, seq + s" + $head", res)
      nSum(tail, n + head, seq + s" - $head", res)
    }
  }

def zeroSum(nums: List[Int]): List[String] = {
  val res = ListBuffer[String]()
  nSum(nums.tail, -nums.head, s"${nums.head}", res)
  res.map(_ + " = 0").toList
}

val expected = List(
  "1 + 2 - 3 + 4 - 5 - 6 + 7 = 0",
  "1 + 2 - 3 - 4 + 5 + 6 - 7 = 0",
  "1 - 2 + 3 + 4 - 5 + 6 - 7 = 0",
  "1 - 2 - 3 - 4 - 5 + 6 + 7 = 0")

assert(expected == zeroSum((1 to 7).toList))