题目:编写一个在1,2,…,9(顺序不能变)数字之间插入+或-或什么都不插入,使得计算结果总是 100 的程序,并输出所有的可能性。
算法分析
本题算法不算太难,但是编码相对麻烦一些,某一些实现方式分支比较多,也不容易调试。
首看分析一下计算复杂度。1到9之间共有8个位置,每个位置有'+'、'-'和''(空字符)在三种选择,一共是3的8次方(6561种选择),所以即使是暴力解法,时间复杂度最多是O(6561),完全没有问题。再看看空间复杂度,我们先生成每种解法的字符串形式算术表达式,例如,"1+2+3+4+5-6+7+8+9",每个表达式应远小于1k,6561种表达式应该小于6M,内存也够用。
难点在于编码上的处理:
1、首先,如何暴力求解,分别对8个位置上的符号进行遍历,需要8层循环,写起来可能比较麻烦。而且如果题目稍作修改,提升一下难度,数字序列的长度不固定, 是个变量,从1到n,那这种就没有办法写了。所以即使是暴力求解,也需要一些小技巧,为简化编码和调试,最好使用递归。
2、其次,是数据存放形式的选择。是分别使用两个数组存放数字和运算符?还是使用字符串存放表达式,然后再解析?由于最终要输出或者存储所有表达式,第二种方式可能相对好些,避免了将最终结果转化成字符串的过程。而且后面可以看到,字符串存储数据更便于递归。
3、非递归写法,需要处理表达式的求值。这块其实是个简单的计算器,或者说是个最简单的编译器。可能会仅处理'+'、'-',或者处理'+'、'-'、''(合并)三种。采用字符串形式可以避免处理两数合并的情况,这样剩下的'+'和'-'运算优先级相同,编码进一步简化。
编程语言的选择:
如果可以选择,python是最佳选择。因为相比于java、C、C++、C#等语言,python处理字符串非常灵活、简便。此外,python内置库函数eval( ),功能正好是实现字符串表达式的求值。而由于python处理字符串方面的优势,暴力或递归(其实也是暴力方法)生成全部可能的表达式也很方便。javascript也有内置eval( )函数,处理本题也比较方便。
具体代码
首先来看一下相对简洁的纯递归解法,java实现:
第一种,基于字符串实现:
import java.util.ArrayList;
import java.util.List;
class Solution {
public static List<String> PossibleSum(String numSeq, int n, int N) {
List<String> result = new ArrayList<String>();
// 因为有""连接两个数字,所以以1到9为例,最外层需要考虑123456789, 23456789, 3456789,..., 9共9种情况
for (int i = 0, lastVal = 0; i < n; i++) {
lastVal = Integer.parseInt(numSeq.substring(i, n));
// 范围内没有运算符号,全是数字直接相连, 作为退出条件
if (i == 0) {
if (lastVal == N) result.add("" + lastVal);
continue; // 此时没有下一层递归
}
// 以下开始递归运算
// -
if (PossibleSum(numSeq, i, N - lastVal).size() != 0)
for (String s : PossibleSum(numSeq, i, N - lastVal))
result.add(s + "+" + lastVal);
// +
if (PossibleSum(numSeq, i, N + lastVal).size() != 0)
for (String s : PossibleSum(numSeq, i, N + lastVal))
result.add(s + "-" + lastVal);
}
return result;
}
public static void main(String[] args) {
List<String> resultList = PossibleSum("123456789", 9, 100);
for (String s : resultList) {
System.out.println(s + "=100");
}
System.out.println("Total : " + resultList.size());
}
}
其实当最后一个数字很大,例如,456789,前面1、2、3三个数字怎么组合都不可能行于100+456789或100-456789,于是可以利用这种特性进行粗略的剪枝。
进一步优化,增加剪枝,去掉不必要的递归调用:
import java.util.ArrayList;
import java.util.List;
class Solution {
public static List<String> PossibleSum(String numSeq, int n, int N) {
List<String> result = new ArrayList<String>();
int max, min;
// 因为有""连接两个数字,所以以1到9为例,最外层需要考虑123456789, 23456789, 3456789,..., 9共9种情况
for (int i = 0, lastVal = 0; i < n; i++) {
lastVal = Integer.parseInt(numSeq.substring(i, n));
// 范围内没有运算符号,全是数字直接相连, 作为退出条件
if (i == 0) {
if (lastVal == N)
result.add("" + lastVal);
continue; // 此时没有下一层递归
}
// 为剪枝作准备,目标值超过当子递归范围时,不再进行递归,剪枝可选,不是必须的,而且要注意逻辑的正确性
if (i > 1) {
max = Integer.parseInt(numSeq.substring(0, i));
min = Integer.parseInt(numSeq.substring(0, 1)) - Integer.parseInt(numSeq.substring(1, i));
} else {
max = min = Integer.parseInt(numSeq.substring(0, 1));
}
// 以下开始递归运算
// -
if (N - lastVal <= max && N - lastVal >= min && PossibleSum(numSeq, i, N - lastVal).size() != 0)
for (String s : PossibleSum(numSeq, i, N - lastVal))
result.add(s + "+" + lastVal);
// +
if (N + lastVal <= max && N + lastVal >= min && PossibleSum(numSeq, i, N + lastVal).size() != 0)
for (String s : PossibleSum(numSeq, i, N + lastVal))
result.add(s + "-" + lastVal);
}
return result;
}
public static void main(String[] args) {
List<String> resultList = PossibleSum("123456789", 9, 100);
for (String s : resultList) {
System.out.println(s + "=100");
}
System.out.println("Total : " + resultList.size());
}
}
基于数字的纯递归实现,缺点是递归过程数字处理逻辑相对复杂,而且只能处理1到n(n<=9)的增序或降序排列。而上述基于字符串的处理方法可以处理任意数字顺序的序列。但基于数字的处理也有好处,相对节省内存,不需要存储长为3的n次方的表达式列表。
import java.util.ArrayList;
import java.util.List;
class Solution {
public static List<String> PossibleSum(int n, int N) {
List<String> result = new ArrayList<String>();
int a = 0;
for (int i = 0; i < n; i++) {
// a代表表达式的最后一个数字,a的迭代处理是难点
a = (int) (a + (n - i) * Math.pow(10, i));
// System.out.println("i is: "+i+"|a is: "+a);
// 范围内没有运算符号,全是数字直接相连, 作为退出条件
if (i == n - 1 && a == N) {
result.add("" + a);
}
// -
if (PossibleSum(n - i - 1, N - a).size() != 0)
for (String s : PossibleSum(n - i - 1, N - a))
result.add(s + "+" + a);
// +
if (PossibleSum(n - i - 1, N + a).size() != 0)
for (String s : PossibleSum(n - i - 1, N + a))
result.add(s + "-" + a);
}
return result;
}
public static void main(String[] args) {
List<String> resultList = PossibleSum(9, 100);
for (String s : resultList) {
System.out.println(s + "=100");
}
System.out.println("Total : " + resultList.size());
}
}
递归实现有一点需要注意,最好是针对算术表达式“1+2-3...9"从后往前递归,即,前n位的算述表达式值'+'或'-'后面n+1位到9合并后的值等于100(目标值)。从前往后递归的话,由于运算优先级问题,会很难处理。
接下来看看基于递归的暴力解法,即先由递归生成所有可能的算术表达式,然后对每个算术表达式求值,保留值等于目标值的表达式作为结果。
首先来看一下最简单的python实现,代码简洁程度不亚于上述的递归写法
import time
import re
class PSum(object):
def all_possible_sum(self, n, s):
plist = self.all_strings(n)
result = []
for command in plist:
if eval(command) == s:
result.append(command + '=' + str(s))
return result
def all_strings(self, n):
if n == 1:
return ['1']
result = []
for s in self.all_strings(n - 1):
result.append(s + str(n))
result.append(s + "+" + str(n))
result.append(s + '-' + str(n))
return result
if __name__ == '__main__':
p = PSum()
start_time = time.time()
res = p.all_possible_sum(9, 100)
for i in res:
print(i)
end_time = time.time()
print('Total counts: ', len(res))
print('time cost: ', end_time - start_time)
javascript版本类似,都是基于eval( )函数
function gs(n) {
var result = new Array()
if (n == 1) return ['1']
x = gs(n - 1)
for (i in x) {
result.push(x[i] + n)
result.push(x[i] + '+' + n)
result.push(x[i] + '-' + n)
}
return result
}
function allPossibleSum(n){
exprs = gs(9)
c = 0
for(i in exprs){
if(eval(exprs[i])==100){
console.log(exprs[i]+"=100")
c = c + 1
}
}
console.log("Total : " + c)
}
allPossibleSum(100)
// console.log(gs(3))
同样算法的java实现,需要自己编写计算表达式的函数。
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class Solution {
// 还有一种就是字符串暴力解法,类似于Python编码,先生成字符串式子
public static List<String> PossibleSum(int N) {
List<String> result = new ArrayList<String>();
List<String> list = allPossibleString("123456789", 0);
for (String s : list) {
if (computeValue(s) == N)
result.add(s);
}
return result;
}
// 生成所有可能的字符串
public static List<String> allPossibleString(String s, int start) {
List<String> result = new ArrayList<String>();
if (start == s.length() - 1) {
result.add(s.substring(start));
return result;
}
for (String xs : allPossibleString(s, start + 1)) {
result.add(s.substring(start, start + 1) + xs);
result.add(s.substring(start, start + 1) + "-" + xs);
result.add(s.substring(start, start + 1) + "+" + xs);
}
return result;
}
// 需要自己编写表达式计算函数,也可以使用
public static int computeValue(String s) {
// 一次遍历,或者正则表达式直接搜索'+'、'-'
// int start = 0, x1, x2;
// Deque<Integer> operandStack = new LinkedList<Integer>();
// Deque<Character> symbolStack = new LinkedList<Character>();
// for (int i = 1; i < s.length(); i++) {
// if (!Character.isDigit(s.charAt(i))) {
// if (symbolStack.isEmpty()) {
// operandStack.push(Integer.parseInt(s.substring(start, i)));
// } else {
// x1 = operandStack.pop();
// x2 = Integer.parseInt(s.substring(start, i));
// operandStack.push(symbolStack.pop() == '+' ? x1 + x2 : x1 - x2);
// }
// symbolStack.push(s.charAt(i));
// start = i + 1;
// }
// }
// x2 = Integer.parseInt(s.substring(start, s.length()));
// return operandStack.isEmpty() ? x2 : operandStack.pop() + (symbolStack.pop() == '+' ? x2 : (0 - x2));
// 正则表达式方式,但java的正则表达式比较弱
Deque<Integer> operandStack = new LinkedList<Integer>();
Deque<Character> symbolStack = new LinkedList<Character>();
Pattern p = Pattern.compile("[1-9]+"); // 也可以找数字,逻辑上都差不多
Matcher matcher = p.matcher(s);
while (matcher.find()) {
if (symbolStack.isEmpty()) {
operandStack.push(Integer.parseInt(matcher.group()));
} else { // 只剩下加、减操作了
int x = operandStack.pop();
int y = Integer.parseInt(matcher.group());
operandStack.push(symbolStack.pop() == '+' ? x + y : x - y);
}
// end端是开区间,正好处于符号位的下标
if (matcher.end() < s.length())
symbolStack.push(s.charAt(matcher.end()));
}
return operandStack.pop();
}
public static void main(String[] args) {
for(String s : PossibleSum(100)) {
System.out.println(s+"==100");
}
System.out.println("Total is: "+PossibleSum(100).size());
}
}
或者强行使用已经要被移除的ScriptEngine,如下图:
最后一种方式,就是使用两个数组分别存储数字和运算符。为方便迭代,使用数字0、1、2代表'+'、'-'和''(空)三种运行符,计算出结果如果满足条件,再转换成数字。这种方法也相对省内存,但缺点前面已经说了,不灵活,数字数组长度必须固定。
class Solution {
public static void possibleSum(int N) {
// 缺点是如果数字序列长度不固定,就没有办法写了,优点是思路简单
int count = 0;
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int opers[] = new int[8];
for (opers[0] = 0; opers[0] < 3; opers[0]++)
for (opers[1] = 0; opers[1] < 3; opers[1]++)
for (opers[2] = 0; opers[2] < 3; opers[2]++)
for (opers[3] = 0; opers[3] < 3; opers[3]++)
for (opers[4] = 0; opers[4] < 3; opers[4]++)
for (opers[5] = 0; opers[5] < 3; opers[5]++)
for (opers[6] = 0; opers[6] < 3; opers[6]++)
for (opers[7] = 0; opers[7] < 3; opers[7]++) {
if (sum(a, opers) == N) {
count++;
printSum(a, opers);
System.out.println("=" + N);
}
}
System.out.println("Total : " + count);
}
public static void printSum(int a[], int oper[]) {
int i = 0;
for (i = 0; i < oper.length; i++) {
System.out.print(a[i]);
switch (oper[i]) {
case 0:
System.out.print("");
break;
case 1:
System.out.print("+");
break;
case 2:
System.out.print("-");
break;
default:
System.out.print("!");
}
}
System.out.print(a[i]);
}
public static int sum(int[] a, int[] opers) {
// 思路简单,就是边界比较多,编码繁琐还容易错,调试不太容易
/********************************************
* 加、减还有合并三种运算的简易计算器,由于涉及合并,而合并运算优先级高于'+'或'-',
* 所以处理当前运算符的时候,要考虑前一个运算符,preOper变量存储前一个运算符
*******************************************/
// 暂不考虑数据数组为空的无意义的边界情况
// if (a.length == 1)
// return a[0];
// //
// int x1 = a[0], x2 = a[1], preOper = opers[0];
// for (int i = 1, j = 1; i < opers.length; i++) { // i用作opers下标,j用作是a的下标
// if (opers[i] == 0) { // 合并拥有最高优先级。即使前一个运算符也是合并,先做本次合并也不影响结果。
// x2 = x2 * 10 + a[++j]; // 这里假设a和opers两个数组数据量严格对应,即a.length = opers.length + 1
// } else {
// if (preOper == 0)
// x1 = x1 * 10 + x2;
// else
// x1 = preOper == 1 ? x1 + x2 : x1 - x2;
// x2 = a[++j];
// preOper = opers[i];
// }
// if (i == opers.length - 1) { // 最后一个运算符
// if (preOper == 0)
// x1 = x1 * 10 + x2;
// else
// x1 = preOper == 1 ? x1 + x2 : x1 - x2;
// }
// }
// return x1;
if (a.length <= 0 || a.length != opers.length + 1)
return (Integer) null;
if (a.length == 1)
return a[0];
int x = a[0], y = a[1], oper = -1;
for (int i = 0, j = 1; i < opers.length; i++) {
if (oper == -1) { // 代表没有出现过'+'或'-'
if (opers[i] == 0) {
x = x * 10 + y;
if (++j < a.length)
y = a[j]; // 下标安全校验,如果都去掉,代码还是很简洁的
else
break;
} else
oper = opers[i];
} else {
if (opers[i] == 0) {
if (++j < a.length)
y = y * 10 + a[j];
else
break;
} else {
x = oper == 1 ? x + y : x - y;
if (++j < a.length)
y = a[j];
else
break;
oper = opers[i];
}
}
}
if (oper != -1) // 代表至少出现过一次'+'或'-'
x = oper == 1 ? x + y : x - y;
return x;
}
public static void main(String[] args) {
possibleSum(100);
}
}
上述的解答方案各有利弊,递归方式代码简单,但是调试时需要进入子递归,层数多了会有点乱。非递归方式编码相对复杂,逻辑分支也比较多,也容易出错。