今天遇到一道有趣的LeetCode题目(编号964),题目要求我们找到用最少的运算符表达特定数字的方法。具体来说,给定一个目标值target和基础值x,我们需要用x的幂次(如x^0, x^1, x^-1, x^2等)通过加减乘除运算组合出target,同时要求使用的运算符数量最少。
这个问题看似简单,但深入思考后发现它涉及动态规划、数学推导和边界条件处理等多个方面。我在解题过程中踩了不少坑,也总结出一些实用的优化技巧,下面就把完整的解题思路和实现过程分享给大家。
首先明确题目要求:给定整数x和target,我们可以使用x的整数次幂(包括负指数)和加减乘除运算符来构造表达式,目标是找到使用运算符数量最少的表达式。
举个例子:
通过分析发现几个重要性质:
这个问题适合用动态规划解决,因为:
定义dp[t]表示表示数字t所需的最少运算符数量。状态转移需要考虑:
基于上述观察,可以建立递归关系:
c复制int leastOpsExpressTarget(int x, int target) {
if (target == 0) return 0;
if (target == 1) return 1; // x/x or x^0
int k = log(target) / log(x);
int power = pow(x, k);
if (power == target) {
return k == 0 ? 1 : k - 1;
}
int option1 = k + leastOpsExpressTarget(x, target - power);
int option2 = (k + 1) + leastOpsExpressTarget(x, power * x - target);
return min(option1, option2) + (k == 0 ? 1 : 0);
}
几个关键边界需要注意:
原始递归会有大量重复计算,需要添加记忆化:
c复制#define MAX_TARGET 1000000
int memo[MAX_TARGET];
int leastOpsExpressTarget(int x, int target) {
if (target < MAX_TARGET && memo[target] != -1) {
return memo[target];
}
// ...原有逻辑...
if (target < MAX_TARGET) {
memo[target] = result;
}
return result;
}
c复制#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#define min(a,b) ((a)<(b)?(a):(b))
#define MAX_TARGET 1000000
int memo[MAX_TARGET];
void initMemo() {
memset(memo, -1, sizeof(memo));
memo[0] = 0;
memo[1] = 1;
}
c复制int leastOpsExpressTarget(int x, int target) {
if (x == 1) {
return target - 1; // 1+1+...+1 (target-1 operators)
}
if (target < MAX_TARGET && memo[target] != -1) {
return memo[target];
}
int k = log(target) / log(x);
int power = pow(x, k);
while (power > target) {
k--;
power = pow(x, k);
}
if (power == target) {
int res = (k == 0) ? 1 : (k - 1);
if (target < MAX_TARGET) memo[target] = res;
return res;
}
int option1 = (k == 0 ? 1 : k) + leastOpsExpressTarget(x, target - power);
int option2 = (k + 1) + leastOpsExpressTarget(x, power * x - target);
int result = min(option1, option2);
if (target < MAX_TARGET) {
memo[target] = result;
}
return result;
}
c复制int main() {
initMemo();
printf("x=3, target=19: %d\n", leastOpsExpressTarget(3, 19)); // Expected: 5
printf("x=5, target=501: %d\n", leastOpsExpressTarget(5, 501)); // Expected: 8
printf("x=100, target=100000000: %d\n", leastOpsExpressTarget(100, 100000000)); // Expected: 3
return 0;
}
原始递归:O(2^n)
记忆化后:O(n) (n为target大小)
实际运行:由于k的取值限制,实际复杂度约为O(log n)
记忆化数组:O(n)
递归栈:O(log n)
忽略x=1的特殊情况:
浮点数精度问题:
记忆化数组越界:
打印递归路径:
c复制printf("target=%d, k=%d, power=%d\n", target, k, power);
验证中间结果:
c复制assert(pow(x,k) <= target);
assert(pow(x,k+1) > target);
边界测试:
如果允许使用括号,可以进一步减少运算符数量。例如:
如果允许使用多个基数(如同时用2和3),问题会变得更加复杂,可能需要使用多维动态规划。
如果不同运算符有不同的成本(如乘法比加法昂贵),需要调整状态转移方程中的成本计算方式。
在实际编码中,我发现几个关键点值得注意:
幂次计算的准确性:
记忆化大小的选择:
运算符计数的细节:
这个题目很好地展示了如何将数学洞察与算法设计相结合。通过分析问题的数学结构,我们可以设计出高效的动态规划解法。同时,实现过程中的各种边界条件和优化技巧也让我对动态规划有了更深的理解。