[LeetCode] 2866. 美丽塔 II

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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值