目录

题目描述

1523. 在区间范围内统计奇数数目

题意分析

给两个非负整数 lowhigh,返回闭区间 [low, high] 内奇数的个数。区间是的——两端都算在内,这一点直接决定了公式里的 +1 放在哪。

约束是全题最重要的信号:0 <= low <= high <= 10^9区间长度可以达到 $10^9$,所以任何逐个遍历的写法都会超时。题目保证了 low <= high,不必处理空区间;但 low 可以是 0,high 可以取到 $10^9$,high + 1 的中间值必须确认不溢出 32 位整数($10^9 + 1$ 远小于 $2.1 \times 10^9$,安全)。

这个范围把题目从「遍历计数」逼成了「$O(1)$ 公式」。所以真正要想清楚的只有一件事:如何用一个不含循环的表达式表示区间内的奇数个数

边界情形值得先列出来,它们是验证公式对错的试金石:lowhigh 都是奇数(如 [3, 7],答案 3);都是偶数(如 [2, 8],答案 3);一奇一偶([3, 8] 答案 3,[2, 7] 答案 3);low == high 且为奇数(答案 1);low == high 且为偶数(答案 0);low == 0(不能出现 low - 1 = -1 参与整数除法,否则不同语言的负数取整规则会导致结果不一致)。

解法:前缀奇数计数公式

核心思路

暴力是从 low 循环到 high 逐个判奇偶,$O(high - low)$。区间长度可达 $10^9$,必然超时。

转向公式的标准手法是前缀化:先定义一个只依赖单个端点的函数 f(x) = [0, x] 内的奇数个数,那么答案就是差值 f(high) - f(low - 1)。这是区间统计问题的通用套路——把「区间」拆成「两个前缀之差」,把二元问题降成一元问题。

接下来求 f(x)。从 0 开始的整数序列 0, 1, 2, 3, ... 中奇偶严格交替,且偶数打头。所以在 [0, x]x + 1 个数里,奇数恰好占一半(向下取整):f(x) = (x + 1) / 2(整数除法)。验证一下:f(0) = 1/2 = 0(只有 0,没有奇数);f(1) = 2/2 = 1(有 1);f(2) = 3/2 = 1f(3) = 4/2 = 2(1 和 3)。全部正确。

代入得 answer = (high + 1) / 2 - low / 2——注意 f(low - 1) = ((low - 1) + 1) / 2 = low / 2,这一步化简顺带消灭了 low - 1,从而避免了 low = 0 时出现 -1 参与整数除法的隐患(C/Java/Go 的整数除法都是向零取整,-1 / 2 = 0 碰巧也对,但把它化简掉更干净,也免去了跨语言的取整规则讨论)。

于是最终公式是:

\[answer = \left\lfloor \frac{high + 1}{2} \right\rfloor - \left\lfloor \frac{low}{2} \right\rfloor\]

它的直观含义可以这样读:(high + 1) / 2 是「不超过 high 的奇数个数」,low / 2 是「严格小于 low 的奇数个数」,两者相减正好是落在 [low, high] 内的奇数个数。整个推导没有任何分支——奇偶性的四种组合全部被整数除法的向下取整自动吸收,这是本题最值得欣赏的地方:好的公式让特判消失,而不是把特判写全。

解题步骤

  • 确认不能遍历:先从约束读出区间长度上界 $10^9$,明确目标是 $O(1)$。这一步在面试里要说出来,它解释了为什么要推公式。
  • 把区间拆成前缀差answer = f(high) - f(low - 1),其中 f(x)[0, x] 内的奇数个数。这是通法,不是本题特有的技巧。
  • 推导 f(x) = (x + 1) / 2[0, x]x + 1 个数,从偶数 0 开始奇偶交替,奇数占其中一半向下取整。这里的 +1 来自闭区间含 0 这个端点。
  • 化简 f(low - 1)low / 2:代入即得,同时消掉了 low - 1,避开 low = 0 时的负数除法。
  • 直接返回 (high + 1) / 2 - low / 2:整数除法自动向下取整,不需要任何奇偶判断,也不需要 Math.floor。本题范围仍能被 double 精确表示,但引入浮点只会增加类型转换和边界推理,没有收益。
  • 确认不溢出high + 1 最大 $10^9 + 1$,在 int 范围内。

low = 3high = 7 走一遍(期望答案 3,即 3、5、7)。

f(high) = (7 + 1) / 2 = 4,含义是 [0, 7] 内有 4 个奇数:1、3、5、7。
f(low - 1) = f(2) = low / 2 = 3 / 2 = 1,含义是 [0, 2] 内有 1 个奇数:1。
相减得 4 - 1 = 3,正是 3、5、7 这三个。

再用 low = 8high = 10 检验偶数端点(期望答案 1,即 9):
(10 + 1) / 2 = 11 / 2 = 5,即 [0,10] 内的奇数 1、3、5、7、9 共 5 个。
8 / 2 = 4,即 [0,7] 内的奇数 1、3、5、7 共 4 个。
相减得 1,正确。

最后检验 low = 0high = 0(期望 0):(0 + 1) / 2 = 00 / 2 = 0,相减得 0。如果公式写成含 (low - 1 + 1) / 2 而没有化简,这里会出现 f(-1),虽然 0 / 2 仍是 0 没出错,但一旦有人把它写成 (low - 1) / 2 + 1 之类的变体,负数就会引发跨语言的取整差异。化简后的形式彻底回避了这类风险。

代码实现

class Solution {
    public int countOdds(int low, int high) {
        // (high+1)/2 是 [0, high] 内的奇数个数,low/2 是 [0, low-1] 内的奇数个数。
        // 整数除法自动向下取整,四种奇偶组合无需分支。
        return (high + 1) / 2 - low / 2;
    }
}
func countOdds(low int, high int) int {
	// (high+1)/2 是 [0, high] 内的奇数个数,low/2 是 [0, low-1] 内的奇数个数。
	// 整数除法自动向下取整,四种奇偶组合无需分支。
	return (high+1)/2 - low/2
}

复杂度分析

  • 时间复杂度:$O(1)$。两次整数除法、一次加法、一次减法,与区间长度完全无关。相比 $O(high - low)$ 的遍历,在 $10^9$ 长度下这是能否通过的分水岭。
  • 空间复杂度:$O(1)$。没有任何额外分配,连临时变量都不需要——整个函数体就是一个表达式。

关键点总结

  • 区间统计 = 两个前缀统计之差。这是本题最值得带走的通法:定义 f(x)[0, x] 的统计量,答案就是 f(high) - f(low - 1)。它同时适用于计数、求和、求积(改成除法)等各种可加统计量,303、304、1480 都是同一条思路的展开。
  • 让整数除法的向下取整替你做分类讨论low/high 的奇偶共四种组合,很多人会写成四个 if 分支;而 (high + 1) / 2 - low / 2 一次覆盖全部。写公式时优先寻找这种「取整自动吸收边界」的形式,代码短、错误面小。
  • 化简掉 low - 1,既避开 low = 0 时的负数除法,也让公式对称好记。凡是表达式里出现「端点减一」,都值得试试能不能代入化简掉。
  • 从约束反推算法量级。$10^9$ 的区间长度直接宣判了遍历解法的死刑,这类「范围大到只能上公式」的信号在数学题里非常普遍,读约束应当先于想解法。
  • 警惕中间值溢出。本题 high + 1 恰好安全,但若约束改成 high <= 2^31 - 1high + 1 就会溢出,届时要改写成 high / 2 + (high & 1) - low / 2 或改用 64 位。面试时主动核算一遍中间值范围是很好的习惯。
  • 不要用浮点Math.ceil/Math.floor 在 $10^9$ 量级虽然还在 double 的精确整数范围内,但引入浮点是毫无必要的风险,纯整数运算永远更可靠。

易错点总结

  • 逐个遍历判奇偶low = 0high = 10^9 时循环十亿次,直接超时。功能对但过不了。
  • 公式写成 (high - low) / 2 + 1low = 2high = 83 + 1 = 4,而正确答案是 3(3、5、7)。这个式子只在两端都是奇数时碰巧正确。
  • 公式写成 (high + 1) / 2 - (low + 1) / 2:这是 f(high) - f(low),把 low 自身排除在外了。low = 3high = 74 - 2 = 2,正确答案是 3——low 本身是奇数时会少数一个。
  • 公式写成 high / 2 - low / 2:这是 f(high - 1) - f(low - 1)high 自身被排除。low = 3high = 73 - 1 = 2,同样少一个。
  • lowhigh 的奇偶写四个分支却漏掉某一种组合:例如只处理了「都是奇数」和「都是偶数」,low = 2high = 7 落到未覆盖的分支返回默认值 0。用统一公式就不会有这个问题。
  • low - 1 直接参与除法且未化简low = 0 时出现 (-1 + 1) / 2 尚可,但若写成 (low - 1) / 2 这类变体,负数在向零取整的语言里得到 0、在向下取整的语言(如 Python)里得到 -1,结果不一致且难以复现。
  • 用浮点做除法再取整(high + 1) / 2.0 后再 Math.floor,除了引入不必要的精度依赖,还容易在忘记强转时返回 double 导致编译失败或类型不匹配。
  • 误以为区间是开区间或半开区间:按 (low, high] 计算会在 low 为奇数时少一个,[3, 7] 返回 2 而不是 3。题面是闭区间,两端都要算。
  • 交换了 lowhigh 的位置:题目已保证 low <= high,但若代码里写成 (low + 1) / 2 - high / 2[3, 7] 会返回 2 - 3 = -1 这种负数,明显不合理——答案必须非负,这也是一个很好的自检信号。
  • high 上限放大后仍用 int:约束若改到 $2^{31}-1$,high + 1 溢出成负数,返回值变成一个巨大的负数。

相似题目

题目 难度 考察点
1480. 一维数组的动态和 简单 前缀思想的最裸形态,把「区间求和」的地基先打好
303. 区域和检索 - 数组不可变 简单 同为「前缀差回答区间查询」,但前缀值要靠预处理数组而非闭式公式
233. 数字 1 的个数 困难 同样是 f(high) - f(low-1) 的思路,但 f(x) 需要按数位逐位推导
400. 第 N 位数字 中等 反过来由累计计数定位具体位置,考察分段计数与下标换算
204. 计数质数 中等 统计对象换成质数,没有闭式公式,必须用埃氏筛批量标记
172. 阶乘后的零 中等 同为把计数问题化归为整除表达式,靠不断除以 5 累加而非单个公式