力扣——42.接雨水(困难难度)——条条大路通罗马

   日期:2020-08-24     浏览:106    评论:0    
核心提示:42.接雨水Trapping Rain Water——力扣一道有意思的题一级目录原链接题目描述思路代码实现一级目录原链接力扣原题:接雨水题目描述给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。示例:输入: [0,1,0,2,1,0,1,3,2,1,2,1]输出: 6

力扣——42.接雨水

  • 一、算法目录合集
    • 1.地址
    • 2.说明
  • 二、题目说明
    • 1.题干
    • 2.原地址
  • 三、实现步骤
    • 1.思路分析
      • 1.1.分析问题
      • 1.2.转化问题
      • 1.3.简化问题
      • 1.4.具体步骤
        • ① 特殊情况分析
        • ② 常规分析
        • ③ 分类分析
    • 2.代码实现
      • 2.1 方法代码
      • 2.2 测试部分代码
      • 2.3 耗用资源情况
  • 四、官方题解
    • 1.原地址
    • 2.方法一——暴力
      • 思路分析
      • 代码实现(C++)
      • 复杂度
    • 3.方法二——动态编程
      • 思路分析
      • 代码实现
      • 复杂度
    • 4.方法三——栈的应用
      • 思路分析
      • 代码实现
      • 复杂度
    • 5.方法四——使用双指针
      • 思路分析
      • 代码实现
      • 复杂度

一、算法目录合集

1.地址

   算法目录合集

2.说明

  该地址指向所有由本人自己所经历的算法习题(也有可能仅仅是一个入门的案例或者是经典案例),仅仅为我做过而且比较有意思的,也许还会有一些我自己想出来的,出于兴趣写在这里,具体格式我会在下面列明,总目录也会在这里出现,方便查阅以及自己进行复习回顾。

二、题目说明

1.题干

  给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

  上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。

示例:
输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6

2.原地址

  42.接雨水

三、实现步骤

1.思路分析

  对于这个题,其实解决方法挺多的,双指针,动态分析等;我这里用一个递归的方法来求,原题比较不太容易看出来这个思路,我画一个新的图,下面的图分别是空桶和最大限度装满水的情况:

空桶

满水

  这个图的层数稍微高了一点儿,柱子与柱子之间的距离有些刻意空开,这样比较直观地展现思路。
  有些人会被题目误导,产生一个惯性思维:题目叫“接雨水”,那我就看看是什么样的填充方法可以满足要求?试试从下往上吧,于是在他心里,就有了这么一个过程:

  其实这种思路也不能说是一种错误思路,因为完全是可以实现的,先把最左边的1和最右边的1找到,那么最中间的0肯定都是要变成1的,因为他们都被填上了水……以此类推,一直填到最高处,那么如果一个数组中只有一个数字呢,比如只有一个2,那么怎么操作呢,那就只能找比他高一位的了——3,这样最左侧的2和最右侧的3之间所有的比2小的填充为2,再这么进行下去,一直到最高的柱子,最终得到的就是结果了,把总值减去之前的总值,就是填充部分的雨水面积,只不过这种方法需要每次判定最右侧的柱子到底怎么选择,比较麻烦,不推荐。
  下面提供一种我想的解题思路:

1.1.分析问题

  首先要分析一下,怎么才能方便地去填水呢?不知道大家见过这个模型没:香槟塔!

香槟塔矢量图(图片来自千库网)

香槟塔(图片来自百度)

  没错,就是这么个东西,这是怎么倒酒的?你没见过一个人拿个香槟一个杯子一个杯子挨个倒吧?这个就是从最高处的杯子倒水,它满了自然就自动去倒下一层了,这个思想也可以用在我们这一题来。

1.2.转化问题

  怎么把这个填充问题转化成香槟塔呢?请看下图:

  我们就往这个最高层倒水,结果显而易见(这玩意儿又不是杯子,这是柱子,柱子!!),离中间最近的空格子很快就会满了然后就是继续往两边进行扩散,然后渐渐地两侧地也满了,然后就没有然后了,这水不就满了吗?

1.3.简化问题

  在上一个步骤里按照逻辑是没有任何问题的,不过可以再进行优化一下:当距离最近的格子不是最高格子(当然除了已知的最高的那个了,最高的记为A)的时候,可以忽略不记,一直到最高的(记为B)那个,然后把他到实际最高的格子A之间所有的格子填满水(即高度提升至B的高度),如图:

  然后继续往下找,记为C,填充至C

  如果还有,就继续DEFG,把26个字母给他用完,不够?用汉字计数!!这样就把问题给简化了。

1.4.具体步骤

① 特殊情况分析

  如果整个数组的元素只有一个或者没有,那说明无法倒水,返回0(说明一个格子的水也没填上)。

if (height.length == 0 || height.length == 1) {
	return 0;
}

② 常规分析

  常规步骤就是上述的分析,先找出来最高的,找出他的值以及他的索引位置,方便以后的操作:

        ArrayList<Integer> heightList = new ArrayList<>();
        int[] heightNew = new int[height.length];

        for (int i : height) {
            heightList.add(i);
        }
        if (height.length == 0 || height.length == 1) {
            return 0;
        }
        int max = heightList.get(0);
        int maxIndex = 0;
        for (int i = 0; i < heightList.size(); i++) {
            if (heightList.get(i) > max) {
                max = heightList.get(i);
                maxIndex = i;
            }
        }
        heightNew[maxIndex] = max;

  maxIndex就是最大值的索引位置,max就是目前的最大值。

③ 分类分析

  找到最高的之后就很简单了,先看左边:
  这里就要用到递归的思想了,假设左边剩下的部分已经填满了,我们只需要填最后一步,就像图中,先考虑黄色,再考虑蓝色:

private void getLeft(ArrayList<Integer> heightList, int[] heightNew, int startIndex, int tailIndex) {
	int max = heightList.get(startIndex);
	int maxIndex = startIndex;

	for (int i = startIndex; i < tailIndex; i++) {
		if (heightList.get(i) > max) {
			max = heightList.get(i);
			maxIndex = i;
		}
	}

	for (int i = maxIndex; i < tailIndex; i++) {
		heightNew[i] = max;
	}
	if (maxIndex != startIndex) {
		getLeft(heightList, heightNew, 0, maxIndex);
	}
}

  这里是把左边的格子填满,实现步骤是先把最高的找出来,赋值给max,并记录索引位置maxIndex,然后把maxIndex右边的,一直到A柱子中的所有数字,变成max的值,接着对剩下的左边部分,再次调用本身方法。

  同理,右边也是这样:

private void getRight(ArrayList<Integer> heightList, int[] heightNew, int startIndex, int tailIndex) {
	int max = heightList.get(startIndex);
	int maxIndex = startIndex;

	for (int i = startIndex; i <= tailIndex; i++) {
		if (heightList.get(i) >= max) {
			max = heightList.get(i);
			maxIndex = i;
		}
	}
	for (int i = startIndex; i <= maxIndex; i++) {
		heightNew[i] = max;
	}
	if (maxIndex != tailIndex) {
		getRight(heightList, heightNew, maxIndex + 1, heightList.size() - 1);
	}
}

  左边和右边的方法已经完成了然后我们需要调用一下,调用方法之前有一个注意事项:确定A柱子右边是有东西的。如果A柱子已经在最右边了,那我们调用getRight方法的时候会报出“IndexOutOfBoundsException”异常,所以我们只有在A柱子不在最右边才会调用getRight方法。

getLeft(heightList, heightNew, 0, maxIndex);
if (maxIndex != height.length - 1) {
	getRight(heightList, heightNew, maxIndex + 1, heightList.size() - 1);
}

  最后,我们求出两数组中各个数值的和的差就是答案了。

for (int i = 0; i < heightNew.length; i++) {
	result += (heightNew[i] - height[i]);
}

2.代码实现

2.1 方法代码

class Solution42 {
    public int trap(int[] height) {
        ArrayList<Integer> heightList = new ArrayList<>();
        int[] heightNew = new int[height.length];

        for (int i : height) {
            heightList.add(i);
        }
        if (height.length == 0 || height.length == 1) {
            return 0;
        }
        int max = heightList.get(0);
        int maxIndex = 0;
        for (int i = 0; i < heightList.size(); i++) {
            if (heightList.get(i) > max) {
                max = heightList.get(i);
                maxIndex = i;
            }
        }
        
        heightNew[maxIndex] = max;

        getLeft(heightList, heightNew, 0, maxIndex);
        if(maxIndex!=height.length-1) {
            getRight(heightList, heightNew, maxIndex + 1, heightList.size() - 1);
        }
        int result = 0;
        for (int i = 0; i < heightNew.length; i++) {
            result += (heightNew[i]-height[i]);
        }
        
        return result;
    }

    private void getLeft(ArrayList<Integer> heightList, int[] heightNew, int startIndex, int tailIndex) {
        int max = heightList.get(startIndex);
        int maxIndex = startIndex;

        for (int i = startIndex; i < tailIndex; i++) {
            if (heightList.get(i) > max) {
                max = heightList.get(i);
                maxIndex = i;
            }
        }

        for (int i = maxIndex; i < tailIndex; i++) {
            heightNew[i] = max;
        }
        if (maxIndex != startIndex) {
            getLeft(heightList, heightNew, 0, maxIndex);
        }
    }

    private void getRight(ArrayList<Integer> heightList, int[] heightNew, int startIndex, int tailIndex) {
        int max = heightList.get(startIndex);
        int maxIndex = startIndex;

        for (int i = startIndex; i <= tailIndex; i++) {
            if (heightList.get(i) >= max) {
                max = heightList.get(i);
                maxIndex = i;
            }
        }
        for (int i = startIndex; i <= maxIndex; i++) {
            heightNew[i] = max;
        }
        if (maxIndex != tailIndex) {
            getRight(heightList, heightNew, maxIndex + 1, heightList.size() - 1);
        }
    }
}

2.2 测试部分代码

这里随便定义一个随便看看就好了

public class Test42Hard {
    public static void main(String[] args) {
        Solution42 s = new Solution42();
        System.out.println(s.trap(new int[]{4,2,0,3,2,5}));
    }
}

2.3 耗用资源情况

四、官方题解

1.原地址

力扣官方答疑戳这里

2.方法一——暴力

思路分析

直观想法
  直接按问题描述进行。对于数组中的每个元素,我们找出下雨后水能达到的最高位置,等于两边最大高度的较小值减去当前高度的值。
算法

  • 初始化 ans=0,
  • 从左向右扫描数组:
    • 初始化 max_left = 0 和 max_right = 0
    • 从当前元素向左扫描并更新:
      • max_left = max (max_left ,height[j])
    • 从当前元素向右扫描并更新:
      • max_right = max (max_right ,height[j])
    • 将 min (max_left ,max_right) - height[j] 累加到 ans\

代码实现(C++)

int trap(vector<int>& height)
{
    int ans = 0;
    int size = height.size();
    for (int i = 1; i < size - 1; i++) {
        int max_left = 0, max_right = 0;
        for (int j = i; j >= 0; j--) { //Search the left part for max bar size
            max_left = max(max_left, height[j]);
        }
        for (int j = i; j < size; j++) { //Search the right part for max bar size
            max_right = max(max_right, height[j]);
        }
        ans += min(max_left, max_right) - height[i];
    }
    return ans;
}

复杂度

  • 时间复杂度:
       O(n2)。数组中的每个元素都需要向左向右扫描。
  • 空间复杂度:
      O(1) 的额外空间。

3.方法二——动态编程

思路分析

直观想法
   在暴力方法中,我们仅仅为了找到最大值每次都要向左和向右扫描一次。但是我们可以提前存储这个值。因此,可以通过动态编程解决。
   这个概念可以见下图解释:

算法

  • 找到数组中从下标 i 到最左端最高的条形块高度 left_max 。
  • 找到数组中从下标 i 到最右端最高的条形块高度 right_max 。
  • 扫描数组 height 并更新答案:
    • 累加 min (max_left[i],max_right[i]) − height[i] 到 ans 上

代码实现

int trap(vector<int>& height)
{
	if(height == null)
		return 0;
    int ans = 0;
    int size = height.size();
    vector<int> left_max(size), right_max(size);
    left_max[0] = height[0];
    for (int i = 1; i < size; i++) {
        left_max[i] = max(height[i], left_max[i - 1]);
    }
    right_max[size - 1] = height[size - 1];
    for (int i = size - 2; i >= 0; i--) {
        right_max[i] = max(height[i], right_max[i + 1]);
    }
    for (int i = 1; i < size - 1; i++) {
        ans += min(left_max[i], right_max[i]) - height[i];
    }
    return ans;
}

复杂度

  • 时间复杂度:
       O(n)。存储最大高度数组,需要两次遍历,每次 O(n) 。最终使用存储的数据更新ans ,O(n)。
  • 空间复杂度:
       O(n) 额外空间。和方法 1 相比使用了额外的O(n) 空间用来放置left_max 和right_max 数组。

4.方法三——栈的应用

思路分析

直观想法
   我们可以不用像方法 2 那样存储最大高度,而是用栈来跟踪可能储水的最长的条形块。使用栈就可以在一次遍历内完成计算。
   我们在遍历数组时维护一个栈。如果当前的条形块小于或等于栈顶的条形块,我们将条形块的索引入栈,意思是当前的条形块被栈中的前一个条形块界定。如果我们发现一个条形块长于栈顶,我们可以确定栈顶的条形块被当前条形块和栈的前一个条形块界定,因此我们可以弹出栈顶元素并且累加答案到ans 。
算法

  • 使用栈来存储条形块的索引下标。
  • 遍历数组:
    • 当栈非空且height[current]>height[st.top()]
      • 意味着栈中元素可以被弹出。弹出栈顶元素top。
      • 计算当前元素和栈顶元素的距离,准备进行填充操作
        distance=current−st.top()−1
      • 找出界定高度
        bounded_height=min(height[current],height[st.top()])−height[top]
      • 往答案中累加积水量ans+=distance×bounded_height
    • 将当前索引下标入栈
    • 将 current 移动到下个位置

代码实现

int trap(vector<int>& height)
{
    int ans = 0, current = 0;
    stack<int> st;
    while (current < height.size()) {
        while (!st.empty() && height[current] > height[st.top()]) {
            int top = st.top();
            st.pop();
            if (st.empty())
                break;
            int distance = current - st.top() - 1;
            int bounded_height = min(height[current], height[st.top()]) - height[top];
            ans += distance * bounded_height;
        }
        st.push(current++);
    }
    return ans;
}

复杂度

  • 时间复杂度:
       O(n)。单次遍历O(n) ,每个条形块最多访问两次(由于栈的弹入和弹出),并且弹入和弹出栈都是 O(1) 的。
  • 空间复杂度:
       O(n)。 栈最多在阶梯型或平坦型条形块结构中占用 O(n)的空间。

5.方法四——使用双指针

思路分析

直观想法
   和方法 2 相比,我们不从左和从右分开计算,我们想办法一次完成遍历。
   从动态编程方法的示意图中我们注意到,只要 right_max[i] > left_max[i] (元素 0 到元素 6),积水高度将由 left_max 决定,类似地 left_max[i] > right_max[i](元素 8 到元素 11)。
   所以我们可以认为如果一端有更高的条形块(例如右端),积水的高度依赖于当前方向的高度(从左到右)。当我们发现另一侧(右侧)的条形块高度不是最高的,我们则开始从相反的方向遍历(从右到左)。
   我们必须在遍历时维护 left_max 和 right_max ,但是我们现在可以使用两个指针交替进行,实现 1 次遍历即可完成。
算法

  • 初始化 left 指针为 0 并且 right 指针为 size-1
  • While left<right, do:
    • If height[left] < height[right]
      • If height[left] ≥ left_max, 更新 left_max
      • Else 累加 left_max − height[left] 到 ans
      • left = left + 1.
    • Else
      • If height[right] ≥ right_max, 更新 right_max
      • Else 累加 right_max−height[right] 到 ans
      • right = right - 1.


友情提示:此gif循环播放,不算开始图一共11张,每张1秒,如果想暂停看截图或者去官网

代码实现

int trap(vector<int>& height)
{
    int left = 0, right = height.size() - 1;
    int ans = 0;
    int left_max = 0, right_max = 0;
    while (left < right) {
        if (height[left] < height[right]) {
            height[left] >= left_max ? (left_max = height[left]) : ans += (left_max - height[left]);
            ++left;
        }
        else {
            height[right] >= right_max ? (right_max = height[right]) : ans += (right_max - height[right]);
            --right;
        }
    }
    return ans;
}

复杂度

  • 时间复杂度:
       O(n)。单次遍历的时间O(n)。
  • 空间复杂度:
       O(1) 的额外空间。left,right,left_max 和right_max 只需要常数的空间。
 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服