1. 引言

在本教程中,我们将探讨如何解决一个经典问题:在数字字符串的数字之间插入二元运算符(+、-、*),生成所有可能的表达式,使其计算结果等于给定的目标值。这个问题融合了字符串操作、递归和回溯技术,是面试中的高频题。

我们将采用回溯算法实现解决方案,深入分析其递归原理并验证功能。最后通过时间/空间复杂度分析确保算法效率。

2. 问题解析

先拆解问题核心:给定数字字符串和目标整数,需在数字间插入运算符(+、-、*)使表达式结果匹配目标值。关键约束包括:

输入限制:字符串长度1-10,目标值在32位整数范围内
数字规则:操作数不能有前导零(除非是"0"本身),如"05"无效但"0"有效
核心任务:探索所有操作数与运算符的组合,筛选出匹配目标值的表达式

例如:输入"123"和目标6,有效解为"1+2+3"和"123"。下面用回溯算法高效求解。

3. 回溯算法原理

回溯本质是系统化探索所有可能解的暴力优化技术。它通过递归构建部分解,并在违反约束时立即剪枝回溯,避免无效计算。

典型应用场景包括:

本问题中,回溯将尝试所有可能的运算符插入位置,并动态计算表达式值。

4. 递归回溯实现

算法核心思路:递归尝试每个数字间的运算符插入,动态维护当前表达式和计算值,匹配目标时记录解

4.1 表达式求值框架

入口方法process()初始化参数并启动递归:

List<String> process(String digits, int target) {
    final List<String> validExpressions = new ArrayList<>();
    evaluateExpressions(validExpressions, new Equation(digits, target), 0, new StringBuilder(), 0, 0);
    return validExpressions;
}

递归方法evaluateExpressions()处理两种状态:

  1. 终止状态:所有数字处理完毕时,检查结果是否匹配目标
  2. 探索状态:继续尝试后续数字组合
void evaluateExpressions(List<String> validExpressions, Equation equation,
  int index, StringBuilder currentExpression, long currentResult,
  long lastOperand) {

    if (allDigitsProcessed(equation.getDigits(), index)) {
        if (currentResult == equation.getTarget()) {
            validExpressions.add(currentExpression.toString());
        }
        return;
    }

    exploreExpressions(validExpressions, equation, index, currentExpression, currentResult, lastOperand);
}

4.2 算术运算处理

关键方法exploreExpressions()遍历所有可能的操作数组合:

void exploreExpressions(List<String> validExpressions, Equation equation,
  int index, StringBuilder currentExpression, long currentResult,
  long lastOperand) {

    for (int endIndex = index; endIndex < equation.getDigits().length(); endIndex++) {
        if (isWithLeadingZero(equation.getDigits(), index, endIndex)) {
            break; // 跳过无效前导零
        }

        long currentOperandValue = Long.parseLong(equation.getDigits()
          .substring(index, endIndex + 1));

        if (isFirstOperand(index)) {
            processFirstOperand(validExpressions, equation, endIndex, 
              currentExpression, currentOperandValue);
        } else {
            applyAddition(validExpressions, equation, endIndex, 
              currentExpression, currentResult, currentOperandValue);
            applySubtraction(validExpressions, equation, endIndex, 
              currentExpression, currentResult, currentOperandValue);
            applyMultiplication(validExpressions, equation, endIndex, 
              currentExpression, currentResult, currentOperandValue, lastOperand);
        }
    }
}

每种运算符的处理逻辑:

  • 加法:直接累加操作数

    void applyAddition(List<String> validExpressions, Equation equation,
    int endIndex, StringBuilder currentExpression, long currentResult,
    long currentOperandValue) {
      appendToExpression(currentExpression, Operator.ADDITION, currentOperandValue);
      evaluateExpressions(validExpressions, equation, endIndex + 1, currentExpression,
        currentResult + currentOperandValue, currentOperandValue);
      removeFromExpression(currentExpression, Operator.ADDITION, currentOperandValue); // 回溯
    }
    
  • 减法:直接减去操作数

    void applySubtraction(List<String> validExpressions, Equation equation,
    int endIndex, StringBuilder currentExpression, long currentResult,
    long currentOperandValue) {
      appendToExpression(currentExpression, Operator.SUBTRACTION, currentOperandValue);
      evaluateExpressions(validExpressions, equation, endIndex + 1, currentExpression,
        currentResult - currentOperandValue, -currentOperandValue);
      removeFromExpression(currentExpression, Operator.SUBTRACTION, currentOperandValue);
    }
    
  • 乘法:特殊处理优先级(回溯修正前序操作)

    void applyMultiplication(List<String> validExpressions, Equation equation,
    int endIndex, StringBuilder currentExpression, long currentResult,
    long currentOperandValue, long lastOperand) {
      appendToExpression(currentExpression, Operator.MULTIPLICATION, currentOperandValue);
      // 关键:撤销前序操作结果,应用乘法优先级
      evaluateExpressions(validExpressions, equation, endIndex + 1, currentExpression,
        currentResult - lastOperand + (lastOperand * currentOperandValue),
        lastOperand * currentOperandValue);
      removeFromExpression(currentExpression, Operator.MULTIPLICATION, currentOperandValue);
    }
    

⚠️ 乘法优先级处理:通过currentResult - lastOperand + (lastOperand * currentOperandValue)修正计算顺序,确保乘法优先执行。

5. 实现测试

5.1 无效输入测试

验证无解情况返回空列表:

private static Stream<Arguments> equationsWithNoSolutions() {
    return Stream.of(Arguments.of("3456237490", 9191), Arguments.of("5", 0));
}

@ParameterizedTest
@MethodSource("equationsWithNoSolutions")
void givenEquationsWithNoSolutions_whenProcess_thenEmptyListIsReturned(String digits, int target) {
    final List<String> result = Backtracking.process(digits, target);
    assertTrue(result.isEmpty());
}

5.2 有效输入测试

验证典型用例的解集正确性:

Stream<Arguments> equationsWithValidSolutions() {
    return Stream.of(
        Arguments.of("1", 1, Collections.singletonList("1")), 
        Arguments.of("00", 0, Arrays.asList("0+0", "0-0", "0*0")),
        Arguments.of("123", 6, Arrays.asList("1+2+3", "1*2*3")), 
        Arguments.of("232", 8, Arrays.asList("2*3+2", "2+3*2")),
        Arguments.of("534", -7, Collections.singletonList("5-3*4")), 
        Arguments.of("1010", 20, Collections.singletonList("10+10")),
        Arguments.of("1234", 10, Arrays.asList("1+2+3+4", "1*2*3+4")), 
        Arguments.of("1234", -10, Collections.singletonList("1*2-3*4")),
        Arguments.of("12345", 15, Arrays.asList("1+2+3+4+5", "1*2*3+4+5", "1-2*3+4*5", "1+23-4-5"))
    );
}

@ParameterizedTest
@MethodSource("equationsWithValidSolutions")
void givenEquationsWithValidSolutions_whenProcess_thenValidResultsAreReturned(
  String digits, int target, List<String> expectedSolutions) {
    List<String> result = Backtracking.process(digits, target);
    assertEquals(expectedSolutions.size(), result.size());
    expectedSolutions.stream()
      .map(result::contains)
      .forEach(Assertions::assertTrue);
}

测试要点:

  1. 验证解的数量匹配
  2. 确保所有预期解均被包含

6. 复杂度分析

时间复杂度

  • 组合爆炸:n个数字有n-1个间隔,每个间隔有4种选择(+、-、*、无操作符)
  • 总组合数:O(4^(n-1))
  • 单次操作:表达式构建和求值O(n)
  • 总时间:O(4^(n-1) * n)

空间复杂度

  • 递归栈:深度O(n)
  • 结果存储:最多O(4^(n-1))个解,每个解长度O(n)
  • 总空间:O(n + 4^(n-1) * n)

⚠️ 实际注意:当n>10时,组合数爆炸,需考虑剪枝优化。

7. 总结

本文通过回溯算法实现了数字字符串表达式生成问题,核心在于:

  1. 递归探索所有运算符组合
  2. 动态维护表达式和计算值
  3. 特殊处理乘法优先级
  4. 严格校验前导零等约束

完整代码见GitHub仓库。此算法可扩展至更复杂的表达式生成场景,是解决组合问题的经典范式。


原始标题:Generate a Valid Expression From a String of Numbers to Get Target Number | Baeldung