LeetCode 84.柱状图中最大的矩形
双指针
注意,双指针解法可行,但是在力扣上提交会超时。
以heights[i]为中心,用两个指针向两边扩散,直到heights[left]和heights[right]小于heights[i]为止,这样就构成了以left和right为边界,heights[i]为高的矩形,遍历heights[],取所有矩形中面积的最大值。
代码实现如下:
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int result = 0;
for(int i = 0; i < heights.size(); i++){
int left = i, right = i;
for(; left >= 0; left--)
if(heights[left] < heights[i]) break;
for(; right < heights.size(); right++)
if(heights[right] < heights[i]) break;
int tmp = heights[i] * (right - left - 1);
result = max(result, tmp);
}
return result;
}
};
动态规划
从上面的双指针解法中可以发现,每次计算之前都需要往左右两边查找,在动态规划中,先记录每个位置左右两边高度更低的柱子,然后在进行计算。
-
确定dp数组下标及其含义
dp[i][0]:比heights[i]高度更低的左边的柱子下标为dp[i][0]。
dp[i][1]:比heights[i]高度更低的右边的柱子下标为dp[i][1]。 -
确定递推公式
因为这里要找的是左边和右边第一个更低的柱子,因此不能直接从相邻的柱子得到答案,必须循环遍历,但是相邻的柱子能够提供信息以减少遍历次数。以寻找右边第一个更低的柱子为例,如果右边第一个柱子比当前柱子低,那么直接找到答案;如果右边第一个柱子比当前柱子高,那么下一次比较位置卡以从dp[i+1][1]开始,因为dp[i+1][1]指向的就是第i+1个柱子右边第一个更低的柱子,当前的第i个柱子比第i+1个柱子低,那么从i+2开始到dp[i+1][1]的柱子一定比第i个柱子高,因此可以跳过这些柱子的比较,以提高效率。当前,前提是要先记录右边柱子的dp数组中的值,因此对于dp[i][1],要从右往左遍历。同理,dp[i][0]要从左往右遍历。
求dp数组的代码实现如下:
dp[0][0] = -1; //这里的初始化和矩形宽度计算相关,w = right - left - 1
dp.back()[1] = heights.size(); //这里的初始化和矩形宽度计算相关,w = right - left - 1
//求dp[i][0]
for(int i = 1; i < dp.size(); i++){
int j = i - 1;
while(j >= 0 && heights[j] >= heights[i]) j = dp[j][0]; //这样更新j比j--效率更高
dp[i][0] = j;
}
//求dp[i][1]
for(int i = dp.size() - 2; i >= 0; i--){
int j = i + 1;
while(j < dp.size() && heights[j] >= heights[i]) j = dp[j][1];
dp[i][1] = j;
}
-
初始化dp数组
初始化已经在上面的代码中给出,dp[0][0]初始化为-1,一方面在下面的while循环中不会出现死循环,另一方面是要符合宽度计算,宽度w = right - left - 1,其中的left和right指向左右第一个比当前柱子低的柱子。
dp.back()[1] = heights.size()也是同样的原因。 -
确定遍历顺序
在确定递推公式的分析中就已经得出,dp[i][0]要从前往后遍历,dp[i][1]要从后往前遍历。 -
举例推导dp数组
完整的代码实现如下:
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int result = 0;
vector<vector<int>> dp(heights.size(), vector<int>(2));
dp[0][0] = -1; //这里的初始化和矩形宽度计算相关,w = right - left - 1
dp.back()[1] = heights.size(); //这里的初始化和矩形宽度计算相关,w = right - left - 1
//求dp[i][0]
for(int i = 1; i < dp.size(); i++){
int j = i - 1;
while(j >= 0 && heights[j] >= heights[i]) j = dp[j][0]; //这样更新j比j--效率更高
dp[i][0] = j;
}
//求dp[i][1]
for(int i = dp.size() - 2; i >= 0; i--){
int j = i + 1;
while(j < dp.size() && heights[j] >= heights[i]) j = dp[j][1];
dp[i][1] = j;
}
//求面积
for(int i = 0; i < dp.size(); i++){
int tmp = heights[i] * (dp[i][1] - dp[i][0] - 1);
result = max(result, tmp);
}
return result;
}
};
单调栈解法
注意几个点:
(1)栈里元素从栈顶到栈底递减,即栈顶元素最大。注意,代码实现里栈中存放的是元素下标。
(2)以栈顶元素作为矩形的高
(3)宽等于当前元素下标减去从栈顶开始的第二个元素
(4)根据矩形宽的计算方式,每次计算时栈中至少有两个元素。为了处理边界(即第一个和最后一个元素),需要在原来数组的开头和最后插入一个值为0的元素,这样就不需要单独处理第一个和最后一个元素了。
当前元素和栈顶元素的大小关系有以下的三种可能:
- 当前元素小于栈顶元素,说明可以以栈顶元素为高,用当前元素和从栈顶开始的第二个元素的下标计算宽度,这样就可以计算面积了。
如图中所示,高为5,宽为2对应的下标减去1对应的下标,这样就可以计算面积了。计算完之后,弹出栈顶元素,再次比较,即当前元素和新的栈顶元素进行比较,如果还是当前元素大,则继续计算,否则将当前元素入栈。 - 当前元素等于栈顶元素,栈顶元素可以弹栈也可以不弹栈,但是当前元素必须入栈,如下图所示,图中灰色的柱子只能以左边距离最近的更低的柱子为左边界,即时两个柱子高度相同,但只能取最近的一个。栈中有两个高度相同的柱子只会重复计算一次,但不会导致错误。
- 当前元素大于栈顶元素,则直接将当前元素入栈。
完整的代码实现如下:
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> stk;
int result = 0;
stk.push(0);
heights.insert(heights.begin(), 0);
heights.push_back(0);
for(int i = 1; i < heights.size(); i++){
while(heights[i] < heights[stk.top()]){
int h = heights[stk.top()];
stk.pop();
int w = i - stk.top() - 1;
result = max(result, w * h);
}
stk.push(i);
}
return result;
}
};
上面的代码对于当前元素和栈顶元素相同的情况没有弹出栈顶元素。