给你一个整数数组 nums ,和一个表示限制的整数 limit,请你返回最长连续子数组的长度,该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit 。
如果不存在满足条件的子数组,则返回 0 。
示例 1:
输入:nums = [8,2,4,7], limit = 4 输出:2 解释:所有子数组如下: [8] 最大绝对差 |8-8| = 0 <= 4. [8,2] 最大绝对差 |8-2| = 6 > 4. [8,2,4] 最大绝对差 |8-2| = 6 > 4. [8,2,4,7] 最大绝对差 |8-2| = 6 > 4. [2] 最大绝对差 |2-2| = 0 <= 4. [2,4] 最大绝对差 |2-4| = 2 <= 4. [2,4,7] 最大绝对差 |2-7| = 5 > 4. [4] 最大绝对差 |4-4| = 0 <= 4. [4,7] 最大绝对差 |4-7| = 3 <= 4. [7] 最大绝对差 |7-7| = 0 <= 4. 因此,满足题意的最长子数组的长度为 2 。
比赛时这破题真就卡了一个小时= = 然后第四题没来记得看= = 我好菜啊
解法1、树状数组/线段树 求区间极值 + 二分
树状数组求区间最大最小值 logn 二分长度然后枚举起点 时间复杂度 O(logn * logn * n)
线段树 太久没写了 练下手。不过这题比较简单,没有更新,没有什么pushup pushdown的操作
#define lson (o<<1) #define rson (o<<1|1) #define mid ((l+r)>>1) const int N = 100005; int max_tr[N * 3], min_tr[N * 3]; class Solution { public: int longestSubarray(vector<int>& nums, int limit) { int n = nums.size(); build(1, 1, n, nums); int l = 1, r = n; int ans = 0; // 二分长度 while (l <= r) { int m = (l + r) >> 1; bool f = false; for (int i = 0, j; (j = i + m - 1) < n; i++) { int maxv = query_max(1, 1, n, i + 1, j + 1); int minv = query_min(1, 1, n, i + 1, j + 1); if (maxv - minv <= limit) { f = true; break; } } if (f) ans = m, l = m + 1; else r = m - 1; } return ans; } void build(int o, int l, int r, vector<int>& nums) { if (l == r) { max_tr[o] = min_tr[o] = nums[l - 1]; return ; } build(lson, l, mid, nums); build(rson, mid + 1, r, nums); pushup(o); } void pushup(int o) { max_tr[o] = max(max_tr[lson], max_tr[rson]); min_tr[o] = min(min_tr[lson], min_tr[rson]); } int query_min(int o, int l, int r, int L, int R) { if (l >= L && r <= R) return min_tr[o]; int res = INT_MAX; if (L <= mid) res = min(res, query_min(lson, l, mid, L, R)); if (R > mid) res = min(res, query_min(rson, mid + 1, r, L, R)); return res; } int query_max(int o, int l, int r, int L, int R) { if (l >= L && r <= R) return max_tr[o]; int res = 0; if (L <= mid) res = max(res, query_max(lson, l, mid, L, R)); if (R > mid) res = max(res, query_max(rson, mid + 1, r, L, R)); return res; } };
树状数组 求区间极值我一直不会。。。so。。比赛时现搜的代码,然后找了一个错误的,调了半个小时。。。又换了一个博客才AC。QAQ
const int N = 100005; int arrmi[N], arrmx[N]; class Solution { public: int longestSubarray(vector<int>& nums, int limit) { int n = nums.size(); init(nums); int l = 1, r = n; int ans = 0; // 二分长度 while (l <= r) { int m = (l + r) >> 1; bool f = false; for (int i = 0, j; (j = i + m - 1) < n; i++) { int maxv = querymx(i + 1, j + 1, nums); int minv = querymi(i + 1, j + 1, nums); if (maxv - minv <= limit) { f = true; break; } } if (f) ans = m, l = m + 1; else r = m - 1; } return ans; } void init(vector<int>& nums) { for(int i = 1; i <= nums.size(); ++i) arrmi[i] = INT_MAX; for(int i = 1; i <= nums.size(); ++i) for(int j = i; j <= nums.size() && arrmi[j] > nums[i - 1]; j += lowbit(j)) arrmi[j] = nums[i - 1]; memset(arrmx, 0, sizeof arrmx); for(int i = 1; i <= nums.size(); ++i) for(int j = i; j <= nums.size() && arrmx[j] < nums[i - 1]; j += lowbit(j)) arrmx[j] = nums[i - 1]; } int querymx(int L, int R, vector<int>& nums) { int res = 0; for (--L; L < R; ){ if (R - lowbit(R) >= L) { res = max(res, arrmx[R]); R -= lowbit(R); } else { res = max(res, nums[R - 1]); --R; } } return res; } int querymi(int L, int R, vector<int>& nums) { int res = INT_MAX; for (--L; L < R; ){ if (R - lowbit(R) >= L) { res = min(res, arrmi[R]); R -= lowbit(R); } else { res = min(res, nums[R - 1]); --R; } } return res; } int lowbit(int x) { return x & -x; } };
树状数组(1032 ms)比线段树(1944 ms)快一倍
解法2、multiset + 滑动窗口
我只能说STL是真的好用,但是我不太会用啊。。。。
multiset 能在 logn 的事件复杂度求出最大最小值,也能在 O(logn) 的复杂度增加删除元素
class Solution { public: int longestSubarray(vector<int>& nums, int limit) { int n = nums.size(); multiset<int> st; int l = 0, r = 0, ans = 0; while (r < n) { st.insert(nums[r++]); while (*st.rbegin() - *st.begin() > limit) { st.erase(st.find(nums[l++])); } ans = max(ans, r - l); } return ans; } };
md 10行代码,我看着我比赛写的100行代码真的好气。。然后只需要 260 ms。。。
解法3、双端队列 + 滑动窗口
class Solution { public: int longestSubarray(vector<int>& nums, int limit) { int n = nums.size(); deque<int> deq; // 降序队列 记录最大值 deque<int> inq; // 升序队列 记录最小值 int l = 0, r = 0, ans = 0; while (r < n) { int x = nums[r]; while (deq.size() && nums[deq.back()] <= x) { deq.pop_back(); } deq.push_back(r); while (inq.size() && nums[inq.back()] >= x) { inq.pop_back(); } inq.push_back(r); while (nums[deq.front()] - nums[inq.front()] > limit) { l++; if (deq.front() < l) deq.pop_front(); if (inq.front() < l) inq.pop_front(); } ans = max(ans, r - l + 1); r++; } return ans; } };
感觉这种解法更妙一些,应该是 O(n) 的时间复杂度吧。132 ms 我之前应该是写过这种题,但是应该是很久以前了,只有浅浅的印象==
用两个双端队列记录每一个对最大最小值有贡献的位置。
比如输入 [8,2,4,7] 4
初始化,l=0, r=0
deq [0] inq [0] // 都是[8]
最大值-最小值 = 8-8 = 0 <= 4 所以 子数组 [0,0] 长度为 1
右端向右移动,r=1
deq [0,1] inq [1] // 下标对应值 deq [8,2] inq [2]
对于inq,要知道计算是从前到后的 1 这个位置比 0 小,所以 0 这个位置对于最小值是没有贡献的,如果你想向前移动到最小值变大,一定要移动到 1 后面。这就是 inq 记录的下标的意义。
而对于 deq 我们记录的是对最大值右贡献的位置,如果你想最大值变小,那么 由 0->1 是由变化的,所以要留下 0 这个位置。
此时最大值 8 最小值 2,8-2>4 所以 l 应该向右移动。
l 向右移动 1,此时 l=1 所以 deq和inq中小于1的下标都应该被删除。
deq [1] inq [1] 最大值-最小值=1-1=0
此时子数组[1,1] 长度为1
r继续向右移动,r=2
deq[2] inq[1,2] // 对于值 deq[4] inq[2,4]
最大值-最小值 = 4-2 = 2<=4
此时子数组[1,2] 长度为2
r继续向右移动,r=3
deq[3] inq[1,2,3] // 对于值 deq[7] inq[2,4,7]
最大值-最小值 = 7-2 = 5>4,所以l需要向右移动
l=2, deq和inq中小于2的下标都应该被删除。
deq[3] inq[2,3] // 对于值 deq[7] inq[4,7]
最大值-最小值 = 7-4 = 3<=4
此时子数组[2,3] 长度为2
所以最长的子数组长度为2。