1. 题目内容
给你一个长度为 n 下标从 0 开始的整数数组 maxHeights 。
你的任务是在坐标轴上建 n 座塔。第 i 座塔的下标为 i ,高度为 heights[i] 。
如果以下条件满足,我们称这些塔是 美丽 的:
1 <= heights[i] <= maxHeights[i]
heights 是一个 山脉 数组。
如果存在下标 i 满足以下条件,那么我们称数组 heights 是一个 山脉 数组:
对于所有 0 < j <= i ,都有 heights[j - 1] <= heights[j]
对于所有 i <= k < n - 1 ,都有 heights[k + 1] <= heights[k]
请你返回满足 美丽塔 要求的方案中,高度和的最大值 。
示例 1:
输入:maxHeights = [5,3,4,1,1]
输出:13
解释:和最大的美丽塔方案为 heights = [5,3,3,1,1] ,这是一个美丽塔方案,因为:
- 1 <= heights[i] <= maxHeights[i]
- heights 是个山脉数组,峰值在 i = 0 处。
13 是所有美丽塔方案中的最大高度和。
示例 2:
输入:maxHeights = [6,5,3,9,2,7]
输出:22
解释: 和最大的美丽塔方案为 heights = [3,3,3,9,2,2] ,这是一个美丽塔方案,因为:
- 1 <= heights[i] <= maxHeights[i]
- heights 是个山脉数组,峰值在 i = 3 处。
22 是所有美丽塔方案中的最大高度和。
提示:
1 <= n == maxHeights <= 1e5
1 <= maxHeights[i] <= 1e9
2. 思考过程
先理解题目的目的,maxHeights
表示的是,每个灯塔可以建造的最高位置。比如maxHeights[i] = 3
,那么你可以选择在高度1,2,3
上进行建造灯塔。同时,建造的灯塔需要满足题目中要求的“山脉数组”条件。什么是山脉数组?1,2,3,2,1
是一个山脉数组,1,1,1,1,1
也可以是一个山脉数组,题目要让我们给出满足山脉数组条件的灯塔高度和的最大值。
先尝试将取i == 0
即数组第一个位置为灯塔高度峰值,这时为了取得满足题意的最大值,一定是要让其余位置灯塔在满足“山脉数组”条件下,尽可能高,最终能得到的最大高度如下图所示。
同理,取i = 1,2,3,4
,分别能得到如下示意图:
分别计算将每个下标作为灯塔的高度峰值,满足“山脉数组”的灯塔数组高度和,就能得到题目答案,但每次重复计算满足条件的其余灯塔高度,使时间复杂度来到了O(n^2)。查看题目给出的输入范围,就可以知道要开始优化算法了,应该要让优化后算法时间复杂度尽可能接近O(n)。
观察上面得到5张图的过程可知,将某一个下标作为灯塔峰值时,也会进行两步操作:以下标为原点,向左不断更新灯塔高度,使左侧灯塔永远不大于右侧灯塔高度;同理以下标为原点,不断更新右侧灯塔高度,使之永不大于左侧灯塔高度。两步操作结束后,只要将所有更新后灯塔高度相加即可得到答案。
先忽略掉右侧灯塔的处理,只看对左侧灯塔的处理,还是用5, 3, 4, 1, 1
例子。
i == 0: 5
i == 1: 3 3
i == 2: 3 3 4
i == 3: 1 1 1 1
i == 4: 1 1 1 1 1
看着这个数据,是否能想到用什么数据结构来处理?没错,这是一个很典型的单调栈能解决的问题。时刻让栈顶元素为栈中最大元素,当出现比栈顶元素更小的元素时,将栈顶元素循环出栈,直到栈为空或者栈顶元素小于待入栈元素,将该元素入栈。
接着,看看右侧元素,这里从后往前处理,栈顶元素在最左侧。
i == 4: 1
i == 3: 1 1
i == 2: 4 1 1
i == 1: 3 3 1 1
i == 0: 5 3 3 1 1
到这里稍缓一下,问自己一个问题,假设现在取i == 2
,我怎么求得最大高度和?很简单,直接将i ==2
时左侧灯塔的处理结果与右侧结果相加,再减去重复计算的灯塔峰值,看下图:
i == 0: 5 5 3 3 1 1 -> 5 + 13 - 5 = 13
i == 1: 3 3 3 3 1 1 -> 6 + 8 - 3 = 11
i == 2: 3 3 4 4 1 1 -> 10 + 6 - 4 = 12
i == 3: 1 1 1 1 1 1 -> 4 + 2 - 1 = 5
i == 4: 1 1 1 1 1 1 -> 5 + 1 - 1 = 5
这样看起来非常直观,不过在写代码时需要把每个位置的左侧高度和右侧高度和记录下来,这样才能做到在O(1)时间复杂度内得到左右两个灯塔高度之和。不断维护左侧、右侧高度总和,这可以用前缀和和后缀和来做。这里创建vector<int> prefix(n)
作为储存左侧高度数据的前缀和,vector<int> suffix(n)
为储存右侧高度数据的后缀和,其中n = maxHeights.size()
。
i == 0: 5 prefix[0] = 5 | 5 3 3 1 1 suffix[0] = 13
i == 1: 3 3 prefix[1] = 6 | 3 3 1 1 suffix[1] = 8
i == 2: 3 3 4 prefix[2] = 10 | 4 1 1 suffix[2] = 6
i == 3: 1 1 1 1 prefix[3] = 4 | 1 1 suffix[3] = 2
i == 4: 1 1 1 1 1 prefix[4] = 5 | 1 suffix[4] = 1
如何计算某个i
下的前缀和呢,看i == 2
时,由于新增的元素4
大于栈顶元素,它可以直接入栈,所以prefix[2] = prefix[1] + 4
。这是理想情况,而当i = 3
时,新增元素小于栈内全部元素,所以需要让prefix[3]
依次减去栈中所有元素,再依次加入4个1。在这种情况下,时间复杂度再次来到了O(n^2)。有办法可以优化时间复杂度吗?答案是有的。设想我们在栈中加入的不是元素本身,而是元素的下标。
i == 0: 5 -> 0
i == 1: 3 3 -> 1
i == 2: 3 3 4 -> 1 2
i == 3: 1 1 1 1 -> 3
i == 4: 1 1 1 1 1 -> 4
i == 4: 1 -> 4
i == 3: 1 1 -> 3
i == 2: 4 1 1 -> 3 2
i == 1: 3 3 1 1 -> 3 1
i == 0: 5 3 3 1 1 -> 3 1 0
那么每个元素的坐标哪怕在最差情况下,也只会入栈一次,时间复杂度O(n)。这道题思考的过程很长,不过到了这里,已经可以实现代码。
long long maximumSumOfHeights(vector<int>& maxHeights) {
long long n = maxHeights.size(), prefixSum = 0, suffixSum = 0;
vector<long long> prefix(n), suffix(n);
stack<int> prefixSt, suffixSt;
for (long long i = 0; i < n; i++)
{
/*
prefixSt维护一个单调栈,使栈顶元素时刻作为栈中最大元素
存入单调栈的不是元素实际值,而是他们在maxHeights数组中的下标
这么做的好处是让平均时间复杂度从O(n^2)降低到O(n),即每个元素最多入栈出栈1次
*/
while (!prefixSt.empty() && maxHeights[prefixSt.top()] >= maxHeights[i])
{
long long temp = prefixSt.top();
prefixSt.pop();
// 分类讨论当前栈为空或非空情况
if (prefixSt.empty())
prefixSum -= (temp + 1) * maxHeights[temp];
else
prefixSum -= (temp - prefixSt.top()) * maxHeights[temp];
}
if (prefixSt.empty())
prefixSum += (i + 1) * maxHeights[i];
else
prefixSum += (i - prefixSt.top()) * maxHeights[i];
prefix[i] = prefixSum;
prefixSt.push(i);
}
// 计算后缀和的时候,唯一的区别是从数组尾部向前
for (long long i = n - 1; i >= 0; i--)
{
while (!suffixSt.empty() && maxHeights[suffixSt.top()] >= maxHeights[i])
{
long long temp = suffixSt.top();
suffixSt.pop();
if (suffixSt.empty())
suffixSum -= (n - temp) * maxHeights[temp];
else
suffixSum -= (suffixSt.top() - temp) * maxHeights[temp];
}
if (suffixSt.empty())
suffixSum += (n - i) * maxHeights[i];
else
suffixSum += (suffixSt.top() - i) * maxHeights[i];
suffix[i] = suffixSum;
suffixSt.push(i);
}
// 对于每个位置i,分别计算作为塔顶时,满足方案的高度和
long long res = 0;
for (int i = 0; i < n; i++)
res = max(res, prefix[i] + suffix[i] - maxHeights[i]);
return res;
}
这里还有一些优化可以考虑实现,在每个栈中提前加入一个哨兵元素,这样可以省略对栈是否为空的判断;在计算高度总和时,也不需要等到计算完成前缀和和后缀和,直接在计算后缀和的过程中,就能得到最终高度总和,有兴趣的朋友可查阅“最终代码”部分。
这道题考验的是单调栈运用+前缀和/后缀和,第一次碰到这种题目的话真的挺棘手,这道题挂着Medium
标签但绝对是Hard
难度,难度分2072
( 2023年12月)。这道题建议是在本子上多写几个例子,加深对单调栈运用的印象,后续碰到类似问题时就能快速在脑海想起单调栈+前后缀的搭配使用。
3. 最终代码
long long maximumSumOfHeights(vector<int>& maxHeights) {
long long n = maxHeights.size(), prefixSum = 0, suffixSum = 0;
vector<long long> prefix(n);
stack<int> prefixSt, suffixSt;
prefixSt.push(-1); // 哨兵
for (long long i = 0; i < n; i++)
{
while (prefixSt.size() > 1 && maxHeights[prefixSt.top()] >= maxHeights[i])
{
long long temp = prefixSt.top();
prefixSt.pop();
prefixSum -= (temp - prefixSt.top()) * maxHeights[temp];
}
prefixSum += (i - prefixSt.top()) * maxHeights[i];
prefix[i] = prefixSum;
prefixSt.push(i);
}
suffixSt.push(n); // 哨兵
long long res = 0;
for (long long i = n - 1; i >= 0; i--)
{
while (suffixSt.size() > 1 && maxHeights[suffixSt.top()] >= maxHeights[i])
{
long long temp = suffixSt.top();
suffixSt.pop();
suffixSum -= (suffixSt.top() - temp) * maxHeights[temp];
}
suffixSum += (suffixSt.top() - i) * maxHeights[i];
res = max(res, prefix[i] + suffixSum - maxHeights[i]);
suffixSt.push(i);
}
return res;
}