c++给定一个长度为 n 的序列 {an},求其最长上升子序列长度。O(n logn)
可以使用二分查找和动态规划的思想,时间复杂度为 O(n logn)。
具体的做法是,维护一个数组 dp,dp[i] 表示长度为 i 的最长上升子序列的末尾元素的最小值。初始时,dp[1] = a[1],其余为 INF。然后从 i = 2 到 n,对于每个 a[i],在 dp 数组中查找第一个大于等于 a[i] 的元素 dp[j],将其替换为 a[i]。如果不存在这样的元素,说明 a[i] 大于所有的 dp[j],此时应该将 dp 数组的长度加 1,即 dp[len+1] = a[i],其中 len 表示当前 dp 数组的最大下标。最终,dp 数组的长度就是最长上升子序列的长度。
代码如下:
const int INF = 0x3f3f3f3f;
int dp[N], len = 1; // dp 数组和最大下标 len
dp[1] = a[1];
for (int i = 2; i <= n; ++i) {
if (a[i] > dp[len]) { // 情况 1
dp[++len] = a[i];
} else { // 情况 2
int pos = lower_bound(dp + 1, dp + len + 1, a[i]) - dp;
dp[pos] = a[i];
}
}
其中 lower_bound 函数可以使用 STL 中的 lower_bound,也可以手写实现。
前缀和是算法竞赛中最基础的算法,但现在Chocola开始研究起了前前缀和。 前缀和(prefix sum)S i =∑ k=1 i a k 。 前前缀和(preprefix sum)则把 S i 作为原序列再进行前缀和。记再次求得前缀和第 i 个是 SS i 。 给定一个长度为 N 的序列 a,现需要你维护两种操作: 将 a i 改为 x。 查询 SS i 的值。 输入格式: 第一行给出两个整数 N,M。分别表示序列长度和操作个数。 (1≤N,M≤2×10 5 ) 接下来一行有 N 个数,即给定的序列 a 1 ,a 2 ,⋯,a n 。 (1≤a i ≤2×10 5 ) 接下来 M 行,每行对应一个操作,有两种操作,格式如下。 1 i x将 a i 改为 x。 (1≤i,x≤2×10 5 ) 2 i查询 SS i 的值。(1≤i≤2×10 5 ) 输出格式: 对于每个操作2,输出一行整数,表示答案。C++代码实现
用户提到需要两种操作:单点更新和查询SSi的值。常规的前缀和用数组就可以,但前前缀和涉及两次累积,直接计算每次查询可能需要O(n)时间,不够高效。所以需要数据结构来优化,比如树状数组或线段树。
先考虑如何将SSi表达式转换为可以用树状数组维护的形式。展开SSi:
SSi = Σ_{k=1}^i Σ_{j=1}^k a_j = Σ_{j=1}^i a_j * (i - j + 1)。比如,当j=1时,a1出现i次;j=2时,a2出现i-1次,依此类推。这可以拆分为iΣa_j (j=1~i) - Σa_j(j-1) (j=1~i)。所以,SSi = i * S_i - T_i,其中S_i是前缀和,T_i是Σa_j*(j-1)。这样,如果我们能维护S和T两个数组,就可以快速计算SSi。
因此,可以维护两个树状数组,一个维护a_j,另一个维护a_j*(j-1)。这样,每次单点更新时,两个树状数组都进行相应的更新。查询SSi时,计算i*sum1(i) - sum2(i),其中sum1是第一个树状数组的前缀和,sum2是第二个的前缀和。
具体步骤:
初始化两个树状数组,BIT1和BIT2。BIT1存储a_j,BIT2存储a_j*(j-1)。
单点更新操作:当a_i被修改为x时,delta = x - a[i]。更新BIT1在i位置的delta。同时,更新BIT2在i位置的delta*(i-1)。然后更新a数组中的值。
查询操作:计算SSi = i * BIT1.query(i) - BIT2.query(i)。
这样,每次操作的时间复杂度是O(logN),满足高效的要求。
接下来,需要用C++实现树状数组。树状数组的基本操作包括更新和查询前缀和。需要注意树状数组的下标通常从1开始。
代码的大致结构:
- 定义一个树状数组类,包含update和query方法。
- 初始化两个树状数组实例。
- 处理输入,初始化数组a,并初始化BIT1和BIT2。
- 处理操作,根据类型执行更新或查询。
测试样例需要考虑边界情况,比如i=0的情况,或者n=1的情况。但根据问题描述,数组可能从1开始索引。
例如,初始数组a的元素是a_1到a_N。当i=1时,SS1 = a1。当i=2时,SS2 = a1 + (a1+a2) = 2a1 + a2。这与展开式一致:i=2时,SS2 = 2*(a1+a2) - [a1*(0) +a2*(1)] = 2(a1+a2) -a2 = 2a1 + a2。
这说明公式是正确的。
因此,代码实现的关键是正确的树状数组维护和公式的应用。要支持单点更新和区间查询的前前缀和(即二次前缀和),可以通过维护两个树状数组(Fenwick Tree)来实现高效操作。以下是分步实现方法:
1. 前前缀和的数学推导
设原数组为 $a_1, a_2, \dots, a_N$,其前缀和为 $S_i = \sum_{j=1}^i a_j$,前前缀和为 $SS_i = \sum_{k=1}^i S_k$。展开后可得: $$ SS_i = \sum_{k=1}^i \sum_{j=1}^k a_j = \sum_{j=1}^i a_j \cdot (i - j + 1) $$ 进一步拆分为: $$ SS_i = i \cdot S_i - \sum_{j=1}^i a_j \cdot (j-1) $$ 因此,只需维护两个值:
- 前缀和 $S_i$(通过树状数组
BIT1
维护) - 加权和 $\sum_{j=1}^i a_j \cdot (j-1)$(通过树状数组
BIT2
维护)
2. C++ 实现代码
#include <iostream>
#include <vector>
using namespace std;
class FenwickTree {
private:
vector<long long> tree;
public:
FenwickTree(int size) : tree(size + 1, 0) {}
void update(int idx, long long delta) {
for (; idx < tree.size(); idx += idx & -idx) {
tree[idx] += delta;
}
}
long long query(int idx) {
long long sum = 0;
for (; idx > 0; idx -= idx & -idx) {
sum += tree[idx];
}
return sum;
}
};
int main() {
int N, Q;
cin >> N >> Q;
vector<long long> a(N + 1); // 下标从1开始
FenwickTree BIT1(N), BIT2(N);
// 初始化
for (int i = 1; i <= N; ++i) {
cin >> a[i];
BIT1.update(i, a[i]);
BIT2.update(i, a[i] * (i - 1));
}
// 处理操作
while (Q--) {
int op;
cin >> op;
if (op == 1) { // 单点更新 ai = x
int i, x;
cin >> i >> x;
long long delta = x - a[i];
BIT1.update(i, delta);
BIT2.update(i, delta * (i - 1));
a[i] = x;
} else if (op == 2) { // 查询 SSi
int i;
cin >> i;
long long S_i = BIT1.query(i);
long long T_i = BIT2.query(i);
cout << i * S_i - T_i << endl;
}
}
return 0;
}
3. 核心操作说明
- 单点更新:当 $a_i$ 被修改为 $x$ 时,计算差值 $\Delta = x - a_i$,并更新
BIT1
和BIT2
。 - 查询前前缀和:根据公式 $SS_i = i \cdot S_i - T_i$,通过两次树状数组查询完成。
4. 复杂度分析
- 时间复杂度:单次更新和查询均为 $O(\log N)$。
- 空间复杂度:$O(N)$。
给定一给定一组数列,求出满足以下条件的区间的长度最大值。 1. 区间左端点的数为区间最小值(不允许并列最小) 2. 区间右端点的数为区间最大值(不允许并列最大) C++实现 O(n)或O(nlogn)
首先,我得理解问题。比如给定一个数列,比如[3,1,4,2],可能的区间是比如左端点1,右端点4的那个子数组吗?比如子数组[1,4],左端点1是子数组的最小,右端点4是子数组的最大。这个区间的长度是2。或者比如整个数组是否满足条件?原数组左端是3,右端是2,显然不行。那可能的正确区间比如在某个位置找到最长的情况。
现在,我需要找这样的区间,使得左端点是最小且唯一,右端是最大且唯一,并且这个区间的长度最大。要找出这样的区间的最大长度。
怎么高效地处理这个问题呢?直接暴力法的话,枚举所有可能的区间,然后检查条件,这样时间复杂度是O(n^3),显然不满足要求。所以必须找更高效的方法。
可能需要找到每个元素作为左端点时,能够延伸的最远右端点,其中该左端点是最小值,且右端点是最大值。或者反过来,对于每个右端点,找左端点的情况。
或者,可能可以利用单调栈的结构。因为单调栈在处理类似寻找左边或右边第一个更大或更小元素的问题时很有用,这可能与当前问题相关。
考虑左端点必须是最小且唯一的,所以对于每个位置i作为左端点,我们需要找到右边的某个j,使得在i到j之间,a[i]是最小且唯一的,且a[j]是最大的且唯一的。并且,在所有这样的j中,找最大的j-i+1。
反过来,对于每个右端点j,a[j]必须是区间的最大且唯一,那么我们可以找到左边的某个i,使得在i到j之间,a[i]是最小且唯一的。同时,这样的i可能有很多,但需要找到最大的j-i+1。
可能需要预处理每个元素的左边和右边的相关信息。
比如,对于每个i,找到右边第一个比a[i]小或等于的元素的位置,这决定了当i作为左端点时,右端点不能超过这个位置,因为否则的话,左端点i可能不是区间的最小值了。或者,这可能不太直接。
或者,我们可以先预处理每个元素的左边第一个比它小的元素的位置,右边第一个比它小的元素的位置,这样可以得到每个i作为左端点时,可能的右端点范围?
另一个思路是,对于每个可能的左端点i,找到所有可能的j >= i,使得在i到j的区间内,a[i]是唯一的min,并且a[j]是唯一的max。然后在这些j中找最大的j-i+1。
那如何高效找到这些j呢?
可能需要将问题拆分成两个部分:
- 对于每个i,找到最大的j,使得在区间[i, j]中,a[i]是唯一的min,并且a[j]是唯一的max。
但这可能很难同时满足两个条件。或许可以分步骤处理。
或者,可以找到所有可能的候选区间,其中左端点是最小,右端点是最大,然后从中找出最长的那个。
另一个观察点是,这样的区间的左端点必须比区间内的所有其他元素小,并且右端点必须比区间内的所有元素大。因此,这样的区间的左端点应该是一个局部最小值,而右端点是一个局部最大值?
或者,可能的候选区间的左端点i和右端点j必须满足a[i] < a[j],因为在区间中,左端点是最小,右端点是最大,所以必然a[i] < a[j]。否则,如果a[i] > a[j],那么右端点j不可能是最大的,或者左端点i不可能是最小的?
这可能有帮助。比如,对于所有可能的i<j且a[i] <a[j]的情况,我们需要检查在区间[i,j]中,a[i]是否是唯一的最小,a[j]是否是唯一的最大。
但这样的检查如果逐个进行的话,时间复杂度可能很高,所以需要找到优化的方法。
可能需要预处理每个元素的左右边界,比如每个元素左边第一个比它小的元素的位置,右边第一个比它大的元素的位置等。这样,当确定i作为左端点时,右边不能超过某个范围,以确保a[i]是最小且唯一的。类似地,当确定j作为右端点时,左边不能超过某个范围,以确保a[j]是最大且唯一的。
或者,可以尝试将问题分解为两个部分:
首先,对于每个i,找到所有可能的j >=i,使得在区间[i,j]中,a[i]是唯一的最小。这一步可以通过预处理每个i的最远右边位置,即右边第一个小于等于a[i]的位置的前一个位置。这样,i的可能j的右边界是这个位置的前一个。例如,right_min[i]表示i之后第一个小于等于a[i]的位置,那么i的可行j只能在i到right_min[i)-1之间,这样在i到这个j之间的所有元素都大于a[i],从而保证a[i]是区间的最小且唯一。
类似地,对于每个j,找到左边第一个大于等于a[j]的位置,左边界的左边,以确保a[j]是最大的。
这样,对于每个i,我们可以找到所有可能的j在某个范围内,其中a[i]是唯一的最小。然后,对于这些j,我们还需要确保a[j]是最大的。或者,反过来,对于每个j,找到其可能的i的范围,然后在这些i中,找到满足条件的i。
这样可能需要将问题拆分成两部分:
预处理每个i的右边范围,使得在i到该范围内的任何j,a[i]是该区间的最小且唯一。
预处理每个j的左边范围,使得在该范围内的任何i到j,a[j]是该区间的最大且唯一。
然后,对于每个i,在它的右边范围内,寻找最大的j,使得j的左边范围包括i,并且j的右边范围是否满足条件?
或者,可能可以预先计算每个i的右边能到达的最远位置(确保a[i]是唯一的min),以及每个j的左边能到达的最远位置(确保a[j]是唯一的max)。然后,对于每个i,找到最大的j >=i,使得i在j的左边范围内,并且j在i的右边范围内。此时,区间[i,j]满足两个条件。然后在这些j中找最大的j-i+1。
这似乎可行。例如,我们可以用单调栈来预处理每个元素的右边第一个小于等于它的元素的位置,记为right_min[i]。这样,对于i来说,其作为左端点时,最大的j不能超过right_min[i]-1,因为如果j超过这个位置,那么区间[i, right_min[i]]会有一个元素小于等于a[i],这会导致a[i]不是区间的最小或者唯一。
同理,预处理每个元素的左边第一个大于等于它的元素的位置,记为left_max[j]。这样,对于j来说,作为右端点时,最大的i不能小于left_max[j]+1,因为如果i在left_max[j]的左边,那么区间中存在一个元素大于等于a[j],导致a[j]不是最大的或者唯一的。
那这样的话,对于每个i,可能的j的范围是i <=j <= right_min[i]-1。同时,对于每个j,可能的i的范围是 left_max[j]+1 <=i <=j。
当i的j的范围和j的i的范围有交集时,i和j可以组成一个符合条件的区间。这时候,我们要找最大的j-i+1。
所以,如何高效地找到所有这样的i和j的组合,使得i <=j,i在j的left_max[j]+1到j之间,j在i到right_min[i]-1之间。然后在这些组合中找到最大的j-i+1。
这可能需要遍历每个i,然后在i的right_min[i]-1的范围内,找到最大的j,且i >=left_max[j]+1。这看起来可能不太容易直接处理。
或者,可以考虑遍历每个可能的j,然后在该j的left_max[j]+1到j的范围内,找到最大的j-i+1,其中i的right_min[i]-1 >=j。也就是,i必须满足i <=j <= right_min[i]-1。这可能需要i的right_min[i] >j。但这样对每个j遍历i的left_max[j]+1到j的范围,然后检查i的right_min[i]-1是否 >=j。这可能时间复杂度较高,因为每个j需要遍历O(n)的i,总时间可能O(n^2)。
这显然不行,需要更高效的方法。
那是否有其他方法?
另一个思路是,找到所有可能的候选区间,其中左端点i满足,右边最近的比它小或等于的元素的位置是right_min[i],右端点j满足,左边最近的比它大或等于的元素的位置是left_max[j]。然后,i到j的区间必须满足i的right_min[i] >j,且j的left_max[j] <i。这可能吗?
或者,在这种情况下,当i的right_min[i] >j,且 j的left_max[j] <i,那么区间[i,j]中的最小值是a[i],最大值是a[j],并且是唯一的。这时候,这样的区间就是符合条件的。
那如何找到这样的i和j,使得i <j,且i的right_min[i] >j,且j的left_max[j] <i。这可能吗?比如,假设i的右边第一个比它小的元素的位置是k,那么i的右侧范围是i到k-1,而j必须位于i到k-1之间。同时,j的左边第一个比它大的元素的位置是m,则j的左侧范围是m+1到j。所以i必须位于m+1到j之间。
这样,如何快速找到这样的i和j?
或者,可以预处理每个i的right_min数组和每个j的left_max数组。然后,对于每个j,遍历i的可能范围left_max[j]+1到j,然后检查i的right_min[i]-1是否>=j。如果是的话,那么i到j是一个可能的候选区间。此时,记录长度j-i+1,并取最大值。
但这样的遍历对于每个j可能需要O(1)到O(n)的时间,导致总时间复杂度为O(n^2),这在n较大时不可行。
所以,必须找到一个更高效的方法。
有没有办法在线性时间或者线性对数时间内处理这个问题?
可能需要一些观察,找到符合条件的i和j之间的关系。
例如,对于符合条件的区间[i,j],必须满足:
a[i]是区间的最小且唯一。
a[j]是区间的最大且唯一。
同时,因为a[i]是区间的最小,a[j]是区间的最大,所以a[i] <a[j]。否则,如果a[i] >=a[j],那么a[j]不可能是区间的最大,或者a[i]不可能是唯一的最小(如果等于的话)。
所以,可以只考虑i和j满足a[i] <a[j]的情况。
这可能缩小了候选范围。
然后,如何高效找到满足条件的i和j?
另一个观察点是,对于每个j,当a[j]是区间的最大值时,那么该区间的所有元素必须小于a[j]。因此,左端点i必须是该区间中的最小值,并且该i必须满足a[i] < a[j],且在i到j之间没有比a[i]更小的元素。
所以,对于每个j,可能的最优i是左边第一个比a[j]小的元素中的某个位置,并且该i的右边没有比它小的元素,直到j。
或者,这可能涉及到单调栈结构,比如维护一个单调递增栈。当处理到j时,栈中的元素是递增的,这样,每个元素在栈中的前一个元素可能就是左边第一个比它小的元素。这可能与求最大矩形面积的问题类似。
或者,可以这样考虑:
对于每个j,作为右端点,我们要找左边的i,使得i是某个位置,且在i到j的区间中,a[i]是唯一的最小,并且a[j]是最大的。那么,可能的i的位置应该在左边第一个比a[j]小的元素之后的位置?这可能吗?
或者,假设我们用单调栈维护一个递增的序列。当遍历到j时,栈中的元素是递增的。对于每个j来说,左边第一个比a[j]小的元素的位置可以用栈来找到。例如,当栈被弹出直到栈顶元素小于a[j],此时栈顶元素的位置就是左边第一个比a[j]小的元素的位置。然后,可能的i的位置应该在那个位置之后,并且在该位置之后的元素中,i的右边没有比a[i]更小的元素,直到j。
这似乎有些复杂,但或许可以结合预处理right_min数组,即每个i的右边第一个小于等于a[i]的位置。
例如,假设我们已经预处理了right_min数组,那么对于每个i来说,能够作为左端点的区间的右端点j不能超过right_min[i]-1。
现在,对于每个j,作为右端点,我们需要找到i在left_max[j]+1到j之间,并且i的right_min[i] >j。这相当于,i的右边第一个小于等于a[i]的位置必须大于j,所以在这个区间[i, j]中,a[i]是唯一的最小。
所以,对于每个j,我们需要在left_max[j]+1到j之间寻找i,使得right_min[i] >j。然后在这些i中,最大的j-i+1就是当前j对应的最大长度。
那么,如何快速找到这样的i呢?
例如,对于每个j,left_max[j]是左边第一个大于等于a[j]的位置。所以,在i的可能范围是left_max[j]+1到j。现在,在这个区间内,我们要找最大的i,使得right_min[i] >j。或者,最小的i,这样j-i+1会更大?
或者说,在这个区间内,找到是否存在i使得right_min[i]>j,并且最大的j-i+1?
比如,假设在left_max[j]+1到j之间,最大的可能的i是某个k,使得right_min[k] >j。此时,区间k到j的长度是j-k+1。因为我们要找最大的长度,所以应该尽可能让k尽可能小,即尽可能靠近left_max[j]+1的位置?
或者,反过来,在这个区间中,找到最大的i满足right_min[i] >j。比如,假设在区间中,最大的i是最大的那个满足right_min[i] >j的i。这样,j-i+1就是最大的可能长度。
或者,或者,对于每个j,在可能的i的范围left_max[j]+1到j之间,我们需要找到最大的i,其中right_min[i] >j。然后,这个i对应的区间长度就是j-i+1,记录最大值。
那么,如何高效地找到每个j对应的这个i呢?
假设我们预处理了right_min数组,那么对于每个j,可以遍历left_max[j]+1到j之间的所有i,然后检查right_min[i]是否>j。然后记录最大的i满足这个条件。但这样对于每个j,可能需要O(n)的时间,导致总时间复杂度为O(n^2),这不行。
所以,必须找到更高效的方法。
或许,可以利用滑动窗口或者双指针的方法。
例如,如果我们按顺序处理j,同时维护一个数据结构,保存left_max[j]+1到j之间的所有i,其中right_min[i] >j。然后,对于每个j,我们只需要在这个数据结构中找到最大的i,这样就可以得到对应的最大长度。
但维护这样的数据结构可能需要较高的复杂度。
或者,预处理每个i的right_min数组,这样对于每个i,我们知道它能覆盖的j的范围是i到right_min[i]-1。然后,对于每个j,其对应的i的范围是left_max[j]+1到j。因此,可能的i必须满足i >=left_max[j]+1,且 j <= right_min[i]-1。也就是,i的right_min[i]必须大于等于j+1。
这可以转化为,i的right_min[i] >j。即,j < right_min[i]。
这看起来像是一个二维问题,如何找到满足i >=left_max[j]+1,j >=i,且j <right_min[i]的i和j,使j-i+1最大。
这似乎难以直接处理,但或许可以利用某些性质。
例如,考虑每个i,它能覆盖的j的范围是i到right_min[i]-1。然后,对于这些j来说,要满足i >=left_max[j]+1。即,对于每个i,j的范围是i到min(right_min[i]-1, ...),其中left_max[j] <=i-1。这可能需要找到对于每个i,最大的j在i到right_min[i]-1之间,并且满足left_max[j] <=i-1。
但如何高效计算这个呢?
或者,可以预处理left_max数组。那么,对于每个i,我们希望找到最大的j >=i,且j <=right_min[i]-1,并且left_max[j] <=i-1。
这可能转化为,在j的区间[i, right_min[i]-1]中,找到最大的j,使得left_max[j] <=i-1。
那么,如何高效地找到这样的j?
如果对于每个i,能够快速找到这个j的最大值,那么问题就解决了。这可能需要对每个i进行一次查询,时间复杂度为O(1)或O(logn)的时间,这样总的时间复杂度可能为O(n logn)或O(n),这满足题目要求。
但如何实现这一点?
例如,可以对数组预处理一个结构,比如对于每个可能的i,在区间[i, right_min[i]-1]中找到最大的j使得left_max[j] <=i-1。
这可以通过预处理每个j的left_max[j],并建立某种结构,比如对于每个位置j,记录left_max[j],然后对于每个i,查询在区间[i, right_min[i]-1]中的最大j,其中left_max[j] <=i-1。
这看起来像是一个二维范围查询问题,但通常这样的问题难以高效处理。不过,如果left_max[j]的值具有某种单调性,可能可以利用。
例如,假设对于j递增,left_max[j]的值可能变化,但或许对于某些情况,可以找到规律。
或者,可以预处理一个数组,其中每个位置i,存储所有j的left_max[j]值,然后构建一个线段树或类似结构,允许在区间查询中快速找到满足条件的最大j。这样,对于每个i,在区间[i, right_min[i]-1]中进行查询,找到最大的j,其中left_max[j] <=i-1。这的每次查询的时间复杂度为O(logn),总时间复杂度为O(n logn),符合要求。
这可能是一个可行的方案。具体来说:
预处理right_min数组,得到每个i的右边第一个小于等于a[i]的位置。
预处理left_max数组,得到每个j的左边第一个大于等于a[j]的位置。
预处理一个线段树,其中每个节点存储对应区间的left_max[j]的值,并且可以在给定区间[l, r]中查询最大的j,满足left_max[j] <=x。
对于每个i,计算j的可能范围是i到right_min[i]-1。如果这个范围是有效的(即i <=right_min[i]-1),则在该区间中查询最大的j,使得left_max[j] <=i-1。如果存在这样的j,则计算长度j-i+1,并更新最大值。
这似乎可行,但需要构建一个能够处理范围查询和条件判断的结构。
不过,构建这样的线段树可能比较复杂。例如,线段树的每个节点需要能够快速回答在某个区间中最大的j,使得left_max[j] <=x。这可能需要对每个区间维护一个有序列表,或者利用其他技巧。
或者,可以将数组的每个元素按j的顺序存储,并且对于每个j,存储left_max[j]。然后,对于给定的查询区间[i, R],其中 R=right_min[i]-1,需要找到最大的j <=R,且j >=i,同时left_max[j] <=i-1。这可以转换为,在区间[i, R]中,最大的j满足left_max[j] <=i-1。
这类似于在范围内寻找最大的j,满足某个条件。对于这个问题,可以使用线段树中的二分搜索。
例如,线段树的每个节点保存该区间内的left_max的最小值和最大值。然后,对于给定的区间[i, R],我们可以逐步向下查询,寻找最大的j满足left_max[j] <=i-1。
这可能需要每个节点保存区间内的最大值和最小值,并且在线段树的结构下进行二分搜索。
这样的线段树查询的时间复杂度为O(logn),因此总的时间复杂度是O(n logn),这符合题目要求。
因此,总体的步骤如下:
预处理步骤:
- 预处理每个i的右边第一个小于等于a[i]的位置,得到right_min数组。
这可以通过单调栈实现。例如,维护一个单调递增栈,遍历数组时,遇到较小的元素时,弹出栈中的元素,并记录它们的right_min为当前索引。这需要O(n)的时间。
- 预处理每个j的左边第一个大于等于a[j]的位置,得到left_max数组。
同样,使用单调栈。维护一个单调递减栈,遍历数组时,遇到较大的元素时,弹出栈中的元素,并记录它们的left_max为当前索引。需要O(n)的时间。
线段树预处理:
- 构建线段树,每个节点保存对应区间的left_max数组的信息,允许在区间[l, r]中查询最大的j,使得left_max[j] <=x。
查询步骤:
- 遍历每个i,计算可能的j的范围是i到R=right_min[i]-1。如果i >R,跳过。否则,在该区间[i, R]中查询最大的j,使得left_max[j] <=i-1。如果存在这样的j,计算长度j-i+1,并更新最大值。
这样,每个i的处理时间是O(logn),总时间是O(n logn)。
现在,需要具体实现这些预处理步骤和线段树的查询。
例如,预处理right_min数组:
对于数组a中的每个元素i,初始化right_min[i] =n(数组长度),表示右边没有比它更小的元素。使用单调递增栈。栈中保存的是索引,对应的元素递增。遍历数组时,对于当前元素a[j],如果栈顶元素i对应的a[i] >a[j],则弹出i,并设置right_min[i] =j。然后将j入栈。这样,每个元素入栈一次,出栈一次,时间复杂度O(n)。
同理,预处理left_max数组:
对于每个元素j,left_max[j]初始化为-1。使用单调递减栈。遍历数组时,对于当前元素a[j],栈中保存的是索引,对应的元素递减。遇到a[j]时,弹出栈中所有比a[j]小的元素。栈顶元素即为左边第一个大于等于a[j]的元素的索引。然后设置left_max[j]为栈顶元素(如果栈不为空),否则-1。然后将j入栈。
接下来,线段树的构建和查询:
线段树的每个节点代表一个区间[l, r]。每个节点需要保存该区间内所有j的left_max[j]的值,并允许快速查询最大的j满足条件。这可以通过在每个节点保存该区间的left_max的最大值,或者在查询时采用二分的方式。
或者,可以使用线段树的结构,其中每个节点保存该区间内所有left_max[j]的值的列表,并排序,以便进行二分查找。但这样建树的时间复杂度可能较高。
另一种方法是,线段树的每个节点保存该区间的left_max数组中的最大值和最小值。当需要查询在区间[L, R]中最大的j,使得left_max[j] <=x时,可以递归地查询线段树:
如果当前节点的区间与查询区间[L, R]无交集,返回-1。
如果当前节点的区间完全在查询区间内,检查该区间的最大left_max是否<=x。如果该区间的最大值都> x,则返回-1。否则,如果整个区间的left_max的最大值 <=x,则返回该区间的最大j(即该区间的右端点)。
否则,递归查询右子树,如果右子树有结果,则取之;否则,查询左子树。
这样,可以利用线段树的区间划分,尽可能在最右端找到满足条件的j。因为我们需要最大的j,所以应该优先查询右子树。
这样的线段树查询函数的大致逻辑是:
function query(L, R, x, node):
if node的区间不在[L, R]内,返回-1
if node的区间的left_max的最大值 >x: 返回-1
if node是叶子节点: 返回j
res = query右子树的[L, R, x]
if res !=-1: return res
else: return query左子树的[L, R, x]
这可能不行,因为即使右子树的某些区间的left_max的最大值<=x,但可能该区间的节点并不完全在查询范围内。所以需要更精确的处理。
正确的做法是,线段树的每个节点保存该区间的左右端点,以及该区间内的left_max的最大值。然后,在查询时,先处理右子树,再处理左子树,以找到最大的j。
具体的查询函数可以这样设计:
在给定查询区间[L, R],要找到最大的j in [L, R],满足left_max[j] <=x。查询过程如下:
如果当前节点的区间与[L, R]没有交集,返回-1。
如果当前节点的区间完全包含在[L, R]中,并且当前区间的left_max最大值> x,则返回-1。
如果当前节点是叶子节点,检查left_max[j] <=x。如果满足,返回j,否则返回-1。
否则,先递归查询右子树。如果在右子树中找到有效的j,返回之。
如果右子树没有找到,再递归查询左子树,并返回结果。
这样,每次优先查询右子树,保证找到最大的j。这种查询的时间复杂度为O(logn),因为每次递归只进入一个子树,或者两个,但每个层级最多访问两个节点。
这样,线段树的每个节点需要保存该区间的left_max最大值。建树时,每个节点的最大值是左右子节点的最大值中的较大者。
因此,线段树的结构需要支持:
构建:每个节点存储区间范围,以及该区间的left_max的最大值。
查询:在区间[L, R]中找到最大的j,满足left_max[j] <=x.
这需要线段树节点存储足够的信息。
现在,具体到实现,可能需要一个自定义的线段树结构。
综上,整个算法的大致步骤是:
预处理:
计算right_min数组,每个元素i的右边第一个<=a[i]的位置。
计算left_max数组,每个元素j的左边第一个>=a[j]的位置.
构建线段树,用于在任意区间[L, R]中查询最大的j,使得left_max[j] <=x.
然后,遍历每个i,计算其可能的j的范围是i到right_min[i]-1。对于每个i,使用线段树查询在这个范围内最大的j,使得left_max[j] <=i-1。如果存在这样的j,计算区间长度,并更新最大值。
这样,总时间复杂度为O(n)预处理 + O(n)遍历i * O(logn)查询 = O(n logn).
现在,需要将这些步骤用C++实现。
首先,预处理right_min数组:
使用单调递增栈。例如:
vector
同样,预处理left_max数组:
vector
接下来,构建线段树:
线段树的每个节点保存区间的起始和结束位置,以及该区间内left_max的最大值。
线段树的节点可以用结构体或类来表示,但为了效率,通常使用数组实现。不过,在C++中,可以递归构建线段树,或者使用数组的方式。
假设我们构建一个线段树,每个节点包含l、r、max_left_max。其中,max_left_max是该区间内最大的left_max值。
构建线段树的函数:
struct Node { int l, r; int max_left; Node *left, *right; Node(int l, int r) : l(l), r(r), max_left(-1), left(nullptr), right(nullptr) {} };
Node* build(int l, int r, vector
查询函数:
int query_max_j(Node* node, int L, int R, int x) { if (node->r < L || node->l > R) { return -1; } if (node->max_left > x) { return -1; } if (node->l == node->r) { return node->l; } // 先查询右子树 int right_res = query_max_j(node->right, L, R, x); if (right_res != -1) { return right_res; } // 再查询左子树 int left_res = query_max_j(node->left, L, R, x); return left_res; }
这样,在查询时,如果当前节点的区间完全在[L, R]内,且其max_left <=x,则优先查询右子树,找到最大的j。否则返回-1。
但可能存在问题,例如,当查询区间是某个子区间,而当前节点的子区间部分位于查询区间外。因此,需要调整查询条件。
可能需要更精确的查询条件。例如,当查询区间为[L, R],而当前节点的区间是[l, r],则如果 r < L或者 l > R,返回-1。否则,将查询限制在[L, R]的范围内。
修改后的查询函数:
int query_max_j(Node* node, int L, int R, int x) { if (node->r < L || node->l > R) { return -1; } if (L <= node->l && node->r <= R) { // 当前节点区间完全在查询范围内 if (node->max_left > x) { return -1; } // 在当前节点区间内找到最大的j if (node->l == node->r) { return node->l; } int right_res = query_max_j(node->right, L, R, x); if (right_res != -1) { return right_res; } int left_res = query_max_j(node->left, L, R, x); return left_res; } else { // 当前节点区间部分在查询范围内,递归查询左右子树 int right_res = query_max_j(node->right, L, R, x); if (right_res != -1) { return right_res; } int left_res = query_max_j(node->left, L, R, x); return left_res; } }
这可能还需要进一步优化。
或者,另一种思路是,将查询区间分解到线段树的节点中,仅处理与查询区间有交集的节点,并且在处理每个节点时,如果其max_left<=x,则尝试在该节点中找到最大的j。
为了找到最大的j,应该优先查询右子树,再查询左子树。这样,一旦在右子树中找到符合条件的j,就可以直接返回,而不需要查询左子树。
修改后的查询函数:
int query_max_j(Node* node, int L, int R, int x) { if (node->r < L || node->l > R) { // 当前节点区间完全在查询范围外 return -1; } if (node->max_left > x) { // 当前节点的整个区间都不满足条件 return -1; } if (node->l == node->r) { // 叶子节点,满足条件 return node->l; } // 优先查询右子树 int right_res = query_max_j(node->right, L, R, x); if (right_res != -1) { return right_res; } // 再查询左子树 int left_res = query_max_j(node->left, L, R, x); return left_res; }
这样,当查询一个节点的右子树时,如果找到结果,就直接返回。否则,查询左子树。这样可以确保返回最大的j。
测试这个函数是否正确。例如,假设线段树中某个区间的左子树和右子树都包含符合条件的j,但右子树的j更大,所以会被优先返回。
这样,整个查询函数应该正确。
现在,将所有这些步骤整合到C++代码中:
首先,预处理right_min和left_max数组。然后构建线段树。接着,遍历每个i,查询对应的j的范围,并记录最大长度。
具体代码示例:
#include
struct Node { int l, r; int max_left; Node *left, *right; Node(int l, int r) : l(l), r(r), max_left(-1), left(nullptr), right(nullptr) {} };
Node* build(int l, int r, const vector
int query_max_j(Node* node, int L, int R, int x) { if (node->r < L || node->l > R) { return -1; } if (node->max_left > x) { return -1; } if (node->l == node->r) { return node->l; } int right_res = query_max_j(node->right, L, R, x); if (right_res != -1) { return right_res; } return query_max_j(node->left, L, R, x); }
int max_interval_length(const vector
// 预处理right_min数组
vector<int> right_min(n, n);
stack<int> stk;
for (int j = 0; j < n; ++j) {
while (!stk.empty() && a[j] <= a[stk.top()]) {
right_min[stk.top()] = j;
stk.pop();
}
stk.push(j);
}
// 预处理left_max数组
vector<int> left_max(n, -1);
stack<int> stk2;
for (int j = 0; j < n; ++j) {
while (!stk2.empty() && a[j] >= a[stk2.top()]) {
stk2.pop();
}
if (!stk2.empty()) {
left_max[j] = stk2.top();
}
stk2.push(j);
}
// 构建线段树
Node* root = build(0, n-1, left_max);
int max_len = 0;
for (int i = 0; i < n; ++i) {
int R = right_min[i] - 1;
if (i > R) continue; // 无效区间
// 在区间[i, R]中查询最大的j,使得left_max[j] <=i-1
int j = query_max_j(root, i, R, i-1);
if (j != -1) {
max_len = max(max_len, j - i + 1);
}
}
return max_len;
}
需要注意的是,线段树的构建和查询可能存在一些边界条件的问题。例如,当i=0时,i-1=-1,left_max[j] <=-1可能只有当left_max[j] =-1时才成立,这可能对应j的左侧没有比它大的元素。例如,数组中的最大值元素的left_max[j]可能为-1。
例如,如果数组是递增的,那么每个元素的left_max[j]是前一个元素吗?或者需要重新确认left_max的预处理逻辑是否正确。
例如,预处理left_max数组时,栈是单调递减的。当遇到a[j] >=栈顶元素时,弹出栈顶元素。当栈为空时,left_max[j] =-1,否则为栈顶元素。这样,left_max[j]是左边第一个>=a[j]的元素的位置。
例如,数组[3,1,4,2]:
j=0: 栈空,left_max[0]=-1,栈压入0.
j=1: a[1]=1 < a[0]=3。栈顶元素0对应的a[0] >=a[1]吗?3 >=1,是的。所以弹出,栈空,left_max[1]=-1。压入1.
j=2: a[2]=4. 栈顶元素1对应的a[1]=1 <4,所以弹出。栈空,left_max[2]=-1. 压入2.
j=3: a[3]=2 <a[2]=4. 栈顶元素2的a[2] >=2?4 >=2,是的。所以left_max[3]=2.
所以left_max数组为[-1, -1, -1, 2]. 正确吗?
是的,因为对于元素4(j=2),左边没有比它大的元素,所以left_max[2]=-1。对于j=3,a[j]=2,左边第一个比它大的元素是4在位置2,所以left_max[3]=2.
接着,right_min数组:
预处理时,栈是递增的。
数组[3,1,4,2]:
i=0: a=3. 栈空,压入0.
i=1: a=1 <=栈顶的3。弹出0,right_min[0] =1。压入1.
i=2: a=4 >1,压入2.
i=3: a=2 <=4. 弹出2,right_min[2]=3. 现在,栈顶是1,a=1 <=2?是的。弹出1,right_min[1]=3. 压入3.
最后,栈中剩余元素3的right_min[3]=4(n=4)。
所以right_min数组是 [1,3,3,4].
这样,对于i=0,R= right_min[0]-1=0。区间是[0,0]. 查询j=0的left_max[j] <=-1吗?left_max[0]=-1 <=-1是满足的。所以区间长度为1.
但原数组中的区间[0,0]只有元素3,左右端点都是自己,显然满足条件,所以长度1。
对于i=1,right_min[i]=3,R=2. 查询区间[1,2]中的j,满足left_max[j] <=0。left_max[1]=-1 <=0,left_max[2]=-1 <=0。最大的j是2。此时区间是[1,2],元素是1和4。检查条件:左端点1是区间的最小,右端点4是区间的最大。符合条件。所以长度是2.
这可能是一个候选。
对于i=2,R=3-1=2.区间是[2,2].left_max[2]=-1 <=1。符合条件,所以长度为1.
对于i=3,right_min=4-1=3.区间是[3,3].left_max[j=3]=2 <=2.符合条件,长度1.
所以最大长度是2.
这与实际情况一致。例如,区间[1,4]中的元素是1和4,满足条件,长度是2。
这样,代码逻辑似乎正确。
综上所述,该算法的时间复杂度为O(n logn),符合题目要求。要解决该问题,我们需要找到满足以下条件的区间最大长度:
- 区间左端点的值为区间最小值(唯一)。
- 区间右端点的值为区间最大值(唯一)。
方法思路
预处理边界信息:
- **right_min[i]**:表示右边第一个小于等于
a[i]
的元素位置,确保a[i]
是左端点时,区间右端点不能超过该位置。 - **left_max[j]**:表示左边第一个大于等于
a[j]
的元素位置,确保a[j]
是右端点时,区间左端点不能小于该位置。
- **right_min[i]**:表示右边第一个小于等于
线段树优化查询:
- 构建线段树,用于在给定区间
[L, R]
内快速查询最大的j
,使得left_max[j] <= x
。这帮助我们高效验证每个左端点i
对应的右端点j
是否满足条件。
- 构建线段树,用于在给定区间
解决代码
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
struct Node {
int l, r;
int max_left;
Node *left, *right;
Node(int l, int r) : l(l), r(r), max_left(-1), left(nullptr), right(nullptr) {}
};
Node* build(int l, int r, const vector<int>& left_max) {
Node* node = new Node(l, r);
if (l == r) {
node->max_left = left_max[l];
return node;
}
int mid = (l + r) / 2;
node->left = build(l, mid, left_max);
node->right = build(mid + 1, r, left_max);
node->max_left = max(node->left->max_left, node->right->max_left);
return node;
}
int query_max_j(Node* node, int L, int R, int x) {
if (node->r < L || node->l > R) return -1;
if (node->max_left > x) return -1;
if (node->l == node->r) return node->l;
int right_res = query_max_j(node->right, L, R, x);
if (right_res != -1) return right_res;
return query_max_j(node->left, L, R, x);
}
int maxIntervalLength(const vector<int>& a) {
int n = a.size();
if (n == 0) return 0;
vector<int> right_min(n, n);
stack<int> stk;
for (int j = 0; j < n; ++j) {
while (!stk.empty() && a[j] <= a[stk.top()]) {
right_min[stk.top()] = j;
stk.pop();
}
stk.push(j);
}
vector<int> left_max(n, -1);
stack<int> stk2;
for (int j = 0; j < n; ++j) {
while (!stk2.empty() && a[j] >= a[stk2.top()]) {
stk2.pop();
}
if (!stk2.empty()) left_max[j] = stk2.top();
stk2.push(j);
}
Node* root = build(0, n-1, left_max);
int max_len = 0;
for (int i = 0; i < n; ++i) {
int R = right_min[i] - 1;
if (i > R) continue;
int j = query_max_j(root, i, R, i-1);
if (j != -1) max_len = max(max_len, j - i + 1);
}
return max_len;
}
代码解释
**预处理
right_min
和left_max
**:right_min
使用单调递增栈,找到每个元素右边第一个小于等于它的位置。left_max
使用单调递减栈,找到每个元素左边第一个大于等于它的位置。
线段树构建与查询:
- 线段树节点存储区间内
left_max
的最大值,用于快速查询满足条件的最大j
。
- 线段树节点存储区间内
**遍历每个左端点
i
**:- 确定
i
的右端点范围[i, right_min[i]-1]
。 - 通过线段树查询该范围内最大的
j
,使得left_max[j] <= i-1
,并更新最大区间长度。
- 确定
该方法通过预处理和线段树优化,将时间复杂度降至 $O(n \log n)$,适用于大规模数据。
相关推荐















