这道题目要求我们使用给定的正整数x,通过加、减、乘、除四种运算符,构造一个等于目标值target的表达式,并且要使用尽可能少的运算符。表达式必须遵循特定的运算规则,包括不使用括号、遵循运算优先级、不允许使用一元负号等。
我在解决这个问题时发现,它实际上是一个典型的动态规划问题,但需要一些数学洞察力才能找到最优解。下面我将详细解析这个问题的解决思路和实现方法。
我们需要构建一个由x和运算符组成的表达式,使其计算结果等于给定的target值。表达式中的运算符只能是+、-、*、/四种,并且要遵循以下规则:
我们的目标是找到使用最少运算符的表达式,并返回这个最少的运算符数量。
这个问题可以通过递归+记忆化的方法解决,主要思路如下:
当目标值v <= x时,我们有两种构建表达式的方式:
加法方式:用x/x表示1,然后相加
减法方式:用x减去若干个1
我们需要在这两种方式中选择运算符较少的一种。
对于较大的目标值(v > x),算法步骤如下:
为了避免重复计算相同的子问题,我们使用一个记忆化表(memo)来存储已经计算过的结果。这在处理大数时能显著提高效率。
c复制typedef struct {
int key; // 目标值
int val; // 最小运算符数量
} Pair;
static Pair memo[10000]; // 记忆化表
static int memoSize; // 记忆化表当前大小
c复制static int dfs(int x, int v) {
// 基础情况处理
if (v <= x) {
int op_add = 2 * v - 1; // 加法方式运算符数量
int op_sub = 2 * (x - v); // 减法方式运算符数量
return op_add < op_sub ? op_add : op_sub;
}
// 检查记忆化表
for (int i = 0; i < memoSize; ++i) {
if (memo[i].key == v) return memo[i].val;
}
// 计算最小k使得x^k >= v
int k = 2;
long y = (long)x * x;
while (y < v) {
y *= x;
++k;
}
// 不足逼近策略
int op1 = (k - 1) + dfs(x, v - (int)(y / x));
int ans = op1;
// 过冲逼近策略(仅在差值小于v时考虑)
if (y - v < v) {
int op2 = k + dfs(x, (int)(y - v));
if (op2 < ans) ans = op2;
}
// 存储结果到记忆化表
memo[memoSize].key = v;
memo[memoSize].val = ans;
++memoSize;
return ans;
}
c复制int leastOpsExpressTarget(int x, int target) {
memoSize = 0; // 重置记忆化表
return dfs(x, target);
}
由于使用了记忆化技术,每个不同的目标值只会被计算一次。在最坏情况下,时间复杂度为O(target),但实际上由于幂次增长很快,实际运行时间远小于这个上界。
空间复杂度主要由记忆化表决定,为O(M),其中M是不同目标值的数量。在实现中我们设置了10000的上限,因此实际空间复杂度可以视为O(1)。
提示:在处理大数时,确保所有中间结果都能被正确表示,必要时使用更大的数据类型。
Q: 为什么在基础情况下要考虑两种策略?
A: 对于小数值,直接相加或从x减去都可能得到最优解,必须比较两种情况。
Q: 为什么过冲逼近只在差值小于v时考虑?
A: 如果差值大于等于v,这种策略会产生更大的子问题,不可能得到更优解。
Q: 记忆化表为什么能提高效率?
A: 很多子问题会被重复计算,记忆化避免了这种重复,显著提高性能。
在实际编码过程中,我发现以下几点特别重要:
这个问题的关键在于理解如何将大问题分解为小问题,并通过比较不同分解策略来找到最优解。动态规划的思想在这里得到了很好的体现。