力扣刷题----42. 接雨水

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

复制代码
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 
复制代码
输入:height = [4,2,0,3,2,5]
输出:9

自己写的

public class Solution

{

public int Trap(int\[\] height)

{

int\[\] Theleft=new intheight.Length;

int\[\] Therightside=new intheight.Length;

int\[\] res=new intheight.Length;

int result = 0;

int left=Theleft0;

int right = TherightsideTherightside.Length - 1;

for (int i = 0; i < height.Length; i++)

{

if (left <= heighti)

{

Thelefti = 0;

left = heighti;

}

else

{

Thelefti = left - heighti ;

}

}

for (int i = height.Length-1; i >= 0; i--)

{

if (right <= heighti)

{

Therightsidei = 0;

right = heighti;

}

else

{

Therightsidei = right - heighti;

}

}

for (int i = 0; i < height.Length; i++)

{

resi= Math.Min(Thelefti, Therightsidei);

}

for (int i = 0; i < res.Length; i++)

{

result=result + resi;

}

return result;

}

}

空间复杂度比较高 和官方的第一个题解一样

方法一:动态规划

对于下标 i,下雨后水能到达的最大高度等于下标 i 两边的最大高度的最小值,下标 i 处能接的雨水量等于下标 i 处的水能到达的最大高度减去 heighti

朴素的做法是对于数组 height 中的每个元素,分别向左和向右扫描并记录左边和右边的最大高度,然后计算每个下标位置能接的雨水量。假设数组 height 的长度为 n,该做法需要对每个下标位置使用 O(n) 的时间向两边扫描并得到最大高度,因此总时间复杂度是 O(n

2

)。

上述做法的时间复杂度较高是因为需要对每个下标位置都向两边扫描。如果已经知道每个位置两边的最大高度,则可以在 O(n) 的时间内得到能接的雨水总量。使用动态规划的方法,可以在 O(n) 的时间内预处理得到每个位置两边的最大高度。

创建两个长度为 n 的数组 leftMax 和 rightMax。对于 0≤i<n,leftMaxi 表示下标 i 及其左边的位置中,height 的最大高度,rightMaxi 表示下标 i 及其右边的位置中,height 的最大高度。

显然,leftMax0=height0,rightMaxn−1=heightn−1。两个数组的其余元素的计算如下:

当 1≤i≤n−1 时,leftMaxi=max(leftMaxi−1,heighti);

当 0≤i≤n−2 时,rightMaxi=max(rightMaxi+1,heighti)。

因此可以正向遍历数组 height 得到数组 leftMax 的每个元素值,反向遍历数组 height 得到数组 rightMax 的每个元素值。

在得到数组 leftMax 和 rightMax 的每个元素值之后,对于 0≤i<n,下标 i 处能接的雨水量等于 min(leftMaxi,rightMaxi)−heighti。遍历每个下标位置即可得到能接的雨水总量。

但是官方的双指针更优化

方法三:双指针

动态规划的做法中,需要维护两个数组 leftMax 和 rightMax,因此空间复杂度是 O(n)。是否可以将空间复杂度降到 O(1)?

注意到下标 i 处能接的雨水量由 leftMaxi 和 rightMaxi 中的最小值决定。由于数组 leftMax 是从左往右计算,数组 rightMax 是从右往左计算,因此可以使用双指针和两个变量代替两个数组。

维护两个指针 left 和 right,以及两个变量 leftMax 和 rightMax,初始时 left=0,right=n−1,leftMax=0,rightMax=0。指针 left 只会向右移动,指针 right 只会向左移动,在移动指针的过程中维护两个变量 leftMax 和 rightMax 的值。

当两个指针没有相遇时,进行如下操作:

使用 heightleft 和 heightright 的值更新 leftMax 和 rightMax 的值;

如果 heightleft<heightright,则必有 leftMax<rightMax,下标 left 处能接的雨水量等于 leftMax−heightleft,将下标 left 处能接的雨水量加到能接的雨水总量,然后将 left 加 1(即向右移动一位);

如果 heightleft≥heightright,则必有 leftMax≥rightMax,下标 right 处能接的雨水量等于 rightMax−heightright,将下标 right 处能接的雨水量加到能接的雨水总量,然后将 right 减 1(即向左移动一位)。

当两个指针相遇时,即可得到能接的雨水总量。

下面用一个例子 height=0,1,0,2,1,0,1,3,2,1,2,1 来帮助读者理解双指针的做法。

复制代码
class Solution {
    public int trap(int[] height) {
        int ans = 0;
        int left = 0, right = height.length - 1;
        int leftMax = 0, rightMax = 0;
        while (left < right) {
            leftMax = Math.max(leftMax, height[left]);
            rightMax = Math.max(rightMax, height[right]);
            if (height[left] < height[right]) {
                ans += leftMax - height[left];
                ++left;
            } else {
                ans += rightMax - height[right];
                --right;
            }
        }
        return ans;
    }
}

关于动态规划的介绍

动态规划(Dynamic Programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学等领域中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划经常用于求解具有重叠子问题和最优子结构性质的问题。这里的"重叠子问题"是指在递归算法中反复出现的问题,而"最优子结构"是指问题的最优解包含其子问题的最优解。

动态规划的关键步骤:

  1. 识别子问题:将问题分解为小的子问题,这些子问题往往是原问题的规模较小的版本。

  2. 确定状态 :定义问题的解状态,通常用变量表示,例如 dp[i]

  3. 确定状态转移方程:找出状态之间的关系,即当前状态是如何由之前的一个或多个状态推导出来的。

  4. 确定初始状态和边界条件:确定状态转移的起点,即基本情况或边界条件。

  5. 确定求解策略:是自底向上(从最小的子问题开始解决)还是自顶向下(递归地解决)。

  6. 构造最优解:根据子问题的解构造原问题的解。

动态规划的常见应用:

  • 斐波那契数列:计算第n个斐波那契数。
  • 背包问题:在不超过背包容量的前提下,选择物品以最大化价值。
  • 最长公共子序列:找出两个序列的最长公共子序列。
  • 最短路径问题:如贝尔曼-福特算法解决带负权边的最短路径问题。
  • 矩阵链乘问题:计算矩阵乘法的最少操作次数。
  • 编辑距离问题:计算两个字符串之间的最小编辑距离。

示例:斐波那契数列的动态规划解法

斐波那契数列是一个典型的动态规划问题,其递归解法存在大量重复计算。动态规划解法如下:

  1. 状态定义dp[i] 表示斐波那契数列的第 i 个数。

  2. 状态转移方程dp[i] = dp[i-1] + dp[i-2]

  3. 初始状态dp[0] = 0dp[1] = 1

  4. 循环计算 :从 2n,依次计算 dp[i]

    using System;

    class Program
    {
    // 动态规划计算斐波那契数列的第n项
    static long Fibonacci(int n)
    {
    if (n <= 1)
    return n;

    复制代码
         long[] dp = new long[n + 1];
         dp[0] = 0;
         dp[1] = 1;
    
         for (int i = 2; i <= n; i++)
         {
             dp[i] = dp[i - 1] + dp[i - 2];
         }
    
         return dp[n];
     }
    
     static void Main()
     {
         int n = 10; // 计算斐波那契数列的第10项
         long result = Fibonacci(n);
         Console.WriteLine($"Fibonacci of {n} is {result}");
     }

    }

动态规划是一种非常强大的方法,可以显著提高算法的效率,特别是在解决具有重复子问题和最优子结构的问题时。

相关推荐
KaMeidebaby6 小时前
卡梅德生物技术快报|PD1 单克隆抗体定制配套 N 糖全谱质控开发
前端·人工智能·算法·数据挖掘·数据分析
8Qi87 小时前
LeetCode 235. 二叉搜索树的最近公共祖先(LCA)
算法·leetcode·二叉树·递归·二叉搜索树·lca·迭代
bIo7lyA8v7 小时前
算法稳定性分析中的随机扰动建模的技术8
算法
科研online7 小时前
基于多源数据和XGBoost-SHAP分析中国大陆绿地碳汇空间变异影响因素的非线性相关性与尺度差异
算法·学习方法
Cthy_hy8 小时前
拓扑排序超详解:原理 + Kahn 贪心算法
python·算法·贪心算法
三品吉他手会点灯8 小时前
C语言学习笔记 - 43.运算符与表达式 - 运算符1 - 运算符的分类和简单介绍
c语言·笔记·学习·算法
VkN2X2X4b8 小时前
算法复杂度的实验验证与误差分析的技术8
算法
其利天下技术8 小时前
风扇灯无刷电机自适应算法实战指南
算法·cocos2d·无刷电机自适应算法·bldc驱动自适应算法·其利无刷电机驱动算法
8Qi89 小时前
LeetCode 494:目标和(Target Sum)—— 题解 ✅
算法·leetcode·职场和发展·动态规划·01背包
hujinyuan201609 小时前
2026年3月 中国电子学会青少年软件编程(Python)三级考试试卷 真题及答案
java·python·算法