每日一题-在区间范围内统计奇数数目🟢
给你两个非负整数 low 和 high 。请你返回 low 和 high 之间(包括二者)奇数的数目。
示例 1:
输入:low = 3, high = 7 输出:3 解释:3 到 7 之间奇数数字为 [3,5,7] 。
示例 2:
输入:low = 8, high = 10 输出:1 解释:8 到 10 之间奇数数字为 [9] 。
提示:
0 <= low <= high <= 10^9
给你两个非负整数 low 和 high 。请你返回 low 和 high 之间(包括二者)奇数的数目。
示例 1:
输入:low = 3, high = 7 输出:3 解释:3 到 7 之间奇数数字为 [3,5,7] 。
示例 2:
输入:low = 8, high = 10 输出:1 解释:8 到 10 之间奇数数字为 [9] 。
提示:
0 <= low <= high <= 10^9$[\textit{low},\textit{high}]$ 中的正奇数个数,等于 $[1,\textit{high}]$ 中的正奇数个数,减去 $[1,\textit{low}-1]$ 中的正奇数个数。(这个想法类似 前缀和)
正奇数可以表示为 $2k-1$,其中 $k$ 是正整数。
$[1,n]$ 中的正奇数满足 $1\le 2k-1\le n$,解得
$$
1\le k \le \left\lfloor\dfrac{n+1}{2}\right\rfloor
$$
这有 $\left\lfloor\dfrac{n+1}{2}\right\rfloor$ 个整数 $k$。
所以答案为
$$
\left\lfloor\dfrac{\textit{high}+1}{2}\right\rfloor - \left\lfloor\dfrac{\textit{low}}{2}\right\rfloor
$$
###py
class Solution:
def countOdds(self, low: int, high: int) -> int:
return (high + 1) // 2 - low // 2
###java
class Solution {
public int countOdds(int low, int high) {
return (high + 1) / 2 - low / 2;
}
}
###cpp
class Solution {
public:
int countOdds(int low, int high) {
return (high + 1) / 2 - low / 2;
}
};
###c
int countOdds(int low, int high) {
return (high + 1) / 2 - low / 2;
}
###go
func countOdds(low, high int) int {
return (high+1)/2 - low/2
}
###js
var countOdds = function(low, high) {
return Math.floor((high + 1) / 2) - Math.floor(low / 2);
};
###rust
impl Solution {
pub fn count_odds(low: i32, high: i32) -> i32 {
(high + 1) / 2 - low / 2
}
}
欢迎在评论区分享你的思路/代码。
欢迎关注 B站@灵茶山艾府
假设一个区间【0,3】,序列是0,1,2,3 奇数个数是3+1/2=2,区间【0,4】,序列是0,1,2,3,4 奇数个数4+1/2=2。
所以,所以,所以,high为3或者4,加个1,然后除以2,奇数个数都是2,然后,请自己推【0,5】和【0,6】,奇数个数都是3。
得出公式 high+1/2是区间【0,high】的奇数个数
因为low,左边界是可以改变的,所以先求【0,high】的奇数个数,然后在求【0,low】的奇数个数,然后做差得到总奇数个数。
注意,注意,注意,把+1看成右边区间增大,这里low是相当于在【0,high】里面的,你别加1,右边界high增大就行了。
**举个例子:**区间【3,7】,high的奇数个数 7+1/2=4,,如果此时3+1/2=2,4-2=2,答案就错了,要3/2=1,最后答案才等于3。
high+1奇数个数 - low奇数个数 **=**总奇数个数。
用公式表示 (high+1)/2 - low/2
int countOdds(int low, int high){
return ((high+1)/2)-((low)/2); //严谨
}
![]()
思路与算法
如果我们暴力枚举 ${\rm [low, high]}$ 中的所有元素会超出时间限制。
我们可以使用前缀和思想来解决这个问题,定义 ${\rm pre}(x)$ 为区间 $[0, x]$ 中奇数的个数,很显然:
$${\rm pre}(x) = \lfloor \frac{x + 1}{2} \rfloor$$
故答案为 $\rm pre(high) - pre(low - 1)$。
代码
###C++
class Solution {
public:
int pre(int x) {
return (x + 1) >> 1;
}
int countOdds(int low, int high) {
return pre(high) - pre(low - 1);
}
};
###Java
class Solution {
public int countOdds(int low, int high) {
return pre(high) - pre(low - 1);
}
public int pre(int x) {
return (x + 1) >> 1;
}
}
###Python
class Solution:
def countOdds(self, low: int, high: int) -> int:
pre = lambda x: (x + 1) >> 1
return pre(high) - pre(low - 1)
###C#
public class Solution {
public int Pre(int x) {
return (x + 1) >> 1;
}
public int CountOdds(int low, int high) {
return Pre(high) - Pre(low - 1);
}
}
###Go
func pre(x int) int {
return (x + 1) >> 1
}
func countOdds(low int, high int) int {
return pre(high) - pre(low - 1)
}
###C
int pre(int x) {
return (x + 1) >> 1;
}
int countOdds(int low, int high) {
return pre(high) - pre(low - 1);
}
###JavaScript
var countOdds = function(low, high) {
return pre(high) - pre(low - 1);
};
function pre(x) {
return (x + 1) >> 1;
}
###TypeScript
function pre(x: number): number {
return (x + 1) >> 1;
}
function countOdds(low: number, high: number): number {
return pre(high) - pre(low - 1);
}
###Rust
impl Solution {
fn pre(x: i32) -> i32 {
(x + 1) >> 1
}
pub fn count_odds(low: i32, high: i32) -> i32 {
Self::pre(high) - Self::pre(low - 1)
}
}
复杂度分析
时间复杂度:$O(1)$。
空间复杂度:$O(1)$。
给你一个整数数组 nums 和一个整数 k。你的任务是将 nums 分割成一个或多个 非空 的连续子段,使得每个子段的 最大值 与 最小值 之间的差值 不超过 k。
返回在此条件下将 nums 分割的总方法数。
由于答案可能非常大,返回结果需要对 109 + 7 取余数。
示例 1:
输入: nums = [9,4,1,3,7], k = 4
输出: 6
解释:
共有 6 种有效的分割方式,使得每个子段中的最大值与最小值之差不超过 k = 4:
[[9], [4], [1], [3], [7]][[9], [4], [1], [3, 7]][[9], [4], [1, 3], [7]][[9], [4, 1], [3], [7]][[9], [4, 1], [3, 7]][[9], [4, 1, 3], [7]]示例 2:
输入: nums = [3,3,4], k = 0
输出: 2
解释:
共有 2 种有效的分割方式,满足给定条件:
[[3], [3], [4]][[3, 3], [4]]
提示:
2 <= nums.length <= 5 * 1041 <= nums[i] <= 1090 <= k <= 109本题是标准的划分型 DP,见 DP 题单 的「§5.2 最优划分」。
一般定义 $f[i+1]$ 表示前缀 $\textit{nums}[0]$ 到 $\textit{nums}[i]$ 在题目约束下,分割出的最少(最多)子数组个数,本题是定义成分割方案数。这里 $i+1$ 是为了把 $f[0]$ 当作初始值。
枚举最后一个子数组的左端点 $j$,那么问题变成前缀 $\textit{nums}[0]$ 到 $\textit{nums}[j-1]$ 在题目约束下的分割方案数,即 $f[j]$。
当子数组右端点 $i$ 固定时,由于子数组越长,最大值越大,最小值越小,最大最小的差值越可能大于 $k$。所以符合要求的左端点 $j$ 一定在一个连续区间 $[L,i]$ 中。累加 $f[j]$ 得
$$
f[i+1] = \sum_{j=L}^{i} f[j]
$$
初始值 $f[0] = 1$,空子数组算一个方案。也可以从递归的角度理解,递归到空子数组,就表示我们找到了一个合法分割方案。
答案为 $f[n]$。
由于 $i$ 越大,$L$ 也越大,可以用 滑动窗口【基础算法精讲 03】。
同时,我们需要计算 239. 滑动窗口最大值 和滑动窗口最小值,这可以用 单调队列【基础算法精讲 27】解决。
维护窗口中的 $\displaystyle\sum_{j=L}^{i} f[j]$,记作 $\textit{sumF}$,转移方程优化成
$$
f[i+1] = \textit{sumF}
$$
注意取模。关于模运算的知识点,见 模运算的世界:当加减乘除遇上取模。
本题视频讲解,欢迎点赞关注~
###py
class Solution:
def countPartitions(self, nums: List[int], k: int) -> int:
MOD = 1_000_000_007
n = len(nums)
min_q = deque()
max_q = deque()
f = [0] * (n + 1)
f[0] = 1
sum_f = 0 # 窗口中的 f[i] 之和
left = 0
for i, x in enumerate(nums):
# 1. 入
sum_f += f[i]
while min_q and x <= nums[min_q[-1]]:
min_q.pop()
min_q.append(i)
while max_q and x >= nums[max_q[-1]]:
max_q.pop()
max_q.append(i)
# 2. 出
while nums[max_q[0]] - nums[min_q[0]] > k:
sum_f -= f[left]
left += 1
if min_q[0] < left:
min_q.popleft()
if max_q[0] < left:
max_q.popleft()
# 3. 更新答案
f[i + 1] = sum_f % MOD
return f[n]
###java
class Solution {
public int countPartitions(int[] nums, int k) {
final int MOD = 1_000_000_007;
int n = nums.length;
Deque<Integer> minQ = new ArrayDeque<>(); // 更快的写法见【Java 数组】
Deque<Integer> maxQ = new ArrayDeque<>();
int[] f = new int[n + 1];
f[0] = 1;
long sumF = 0; // 窗口中的 f[i] 之和
int left = 0;
for (int i = 0; i < n; i++) {
// 1. 入
sumF += f[i];
int x = nums[i];
while (!minQ.isEmpty() && x <= nums[minQ.peekLast()]) {
minQ.pollLast();
}
minQ.addLast(i);
while (!maxQ.isEmpty() && x >= nums[maxQ.peekLast()]) {
maxQ.pollLast();
}
maxQ.addLast(i);
// 2. 出
while (nums[maxQ.peekFirst()] - nums[minQ.peekFirst()] > k) {
sumF -= f[left];
left++;
if (minQ.peekFirst() < left) {
minQ.pollFirst();
}
if (maxQ.peekFirst() < left) {
maxQ.pollFirst();
}
}
// 3. 更新答案
f[i + 1] = (int) (sumF % MOD);
}
return f[n];
}
}
###java
class Solution {
public int countPartitions(int[] nums, int k) {
final int MOD = 1_000_000_007;
int n = nums.length;
int[] minQ = new int[n];
int[] maxQ = new int[n];
int minHead = 0, minTail = -1;
int maxHead = 0, maxTail = -1;
int[] f = new int[n + 1];
f[0] = 1;
long sumF = 0; // 窗口中的 f[i] 之和
int left = 0;
for (int i = 0; i < n; i++) {
// 1. 入
sumF += f[i];
int x = nums[i];
while (minHead <= minTail && x <= nums[minQ[minTail]]) {
minTail--;
}
minQ[++minTail] = i;
while (maxHead <= maxTail && x >= nums[maxQ[maxTail]]) {
maxTail--;
}
maxQ[++maxTail] = i;
// 2. 出
while (nums[maxQ[maxHead]] - nums[minQ[minHead]] > k) {
sumF -= f[left];
left++;
if (minQ[minHead] < left) {
minHead++;
}
if (maxQ[maxHead] < left) {
maxHead++;
}
}
// 3. 更新答案
f[i + 1] = (int) (sumF % MOD);
}
return f[n];
}
}
###cpp
class Solution {
public:
int countPartitions(vector<int>& nums, int k) {
const int MOD = 1'000'000'007;
int n = nums.size();
deque<int> min_q, max_q;
vector<int> f(n + 1);
f[0] = 1;
long long sum_f = 0; // 窗口中的 f[i] 之和
int left = 0;
for (int i = 0; i < n; i++) {
int x = nums[i];
// 1. 入
sum_f += f[i];
while (!min_q.empty() && x <= nums[min_q.back()]) {
min_q.pop_back();
}
min_q.push_back(i);
while (!max_q.empty() && x >= nums[max_q.back()]) {
max_q.pop_back();
}
max_q.push_back(i);
// 2. 出
while (nums[max_q.front()] - nums[min_q.front()] > k) {
sum_f -= f[left];
left++;
if (min_q.front() < left) {
min_q.pop_front();
}
if (max_q.front() < left) {
max_q.pop_front();
}
}
// 3. 更新答案
f[i + 1] = sum_f % MOD;
}
return f[n];
}
};
###go
func countPartitions(nums []int, k int) int {
const mod = 1_000_000_007
n := len(nums)
var minQ, maxQ []int
f := make([]int, n+1)
f[0] = 1
sumF := 0 // 窗口中的 f[i] 之和
left := 0
for i, x := range nums {
// 1. 入
sumF += f[i]
for len(minQ) > 0 && x <= nums[minQ[len(minQ)-1]] {
minQ = minQ[:len(minQ)-1]
}
minQ = append(minQ, i)
for len(maxQ) > 0 && x >= nums[maxQ[len(maxQ)-1]] {
maxQ = maxQ[:len(maxQ)-1]
}
maxQ = append(maxQ, i)
// 2. 出
for nums[maxQ[0]]-nums[minQ[0]] > k {
sumF -= f[left]
left++
if minQ[0] < left {
minQ = minQ[1:]
}
if maxQ[0] < left {
maxQ = maxQ[1:]
}
}
// 3. 更新答案
f[i+1] = sumF % mod
}
return f[n]
}
更多相似题目,见下面动态规划题单的「§5.2 最优划分」和「§11.3 单调队列优化 DP」。
维护 $f(i)$ 表示前 $i$ 个元素的分割方案。转移时,枚举上一个子段的末尾在哪,有转移方程
$$
f(i) = \sum f(j)
$$
其中 $j$ 满足 $\max(a_{j + 1}, a_{j + 2}, \cdots, a_i) - \min(a_{j + 1}, a_{j + 2}, \cdots, a_i) \le k$
直接计算 DP 方程的复杂度为 $\mathcal{O}(n^2)$,还需要进一步观察合法的 $j$ 有什么特征。
注意到,如果某个子数组的极值之差小于等于 $k$,那么它的子数组的极值之差也小于等于 $k$。这是典型的双指针特征,因此合法的 $j$ 就是一段连续值,从某个值一直取到 $(i - 1)$。用双指针算出最小的合法 $j$,再用前缀和计算区间和即可。复杂度 $\mathcal{O}(n\log n)$,这里的 $\log n$ 主要是我们需要用数据结构(比如 multiset)动态维护滑动窗口里的最小值和最大值。
class Solution {
public:
int countPartitions(vector<int>& nums, int K) {
int n = nums.size();
const int MOD = 1e9 + 7;
// f[i]:前 i 个元素的分割方案数
// g[i]:f 的前缀和
long long f[n + 1], g[n + 1];
f[0] = g[0] = 1;
// 用 multiset 记录滑动窗口里的数,方便求出最小值和最大值
multiset<int> ms;
// 枚举双指针的右端点 i,计算合法子段左端点的最小值 j
for (int i = 1, j = 1; i <= n; i++) {
ms.insert(nums[i - 1]);
while (j < i && *prev(ms.end()) - *ms.begin() > K) {
ms.erase(ms.find(nums[j - 1]));
j++;
}
// j 是最小的左端点
// 那么上一个子段最小的右端点就是 j - 1
// 前缀和就得减去 j - 2 的值
f[i] = (g[i - 1] - (j - 2 >= 0 ? g[j - 2] : 0) + MOD) % MOD;
g[i] = (g[i - 1] + f[i]) % MOD;
}
return f[n];
}
};