365天挑战LeetCode1000题——Day 003 每日一题 + 动态规划 03

本文分享了LeetCode中关于序列化二叉搜索树、单词拆分及接雨水问题的解题思路。针对二叉搜索树,通过前序遍历实现序列化和反序列化;单词拆分采用动态规划解决;接雨水问题,提出了两种解法,一种简单但效率不高,另一种优化后的高效解决方案。通过这些题目,作者积累了编程经验并探讨了问题解决的思维过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


1. 序列化和反序列化二叉搜索树

序列化是将数据结构或对象转换为一系列位的过程,以便它可以存储在文件或内存缓冲区中,或通过网络连接链路传输,以便稍后在同一个或另一个计算机环境中重建。

设计一个算法来序列化和反序列化 二叉搜索树 。 对序列化/反序列化算法的工作方式没有限制。 您只需确保二叉搜索树可以序列化为字符串,并且可以将该字符串反序列化为最初的二叉搜索树。

编码的字符串应尽可能紧凑。

1.1 前序遍历

对于BST来说,只需要给出它的前序遍历,就能得到它的中序遍历,为什么呢?因为BST的中序遍历是有序的,所以对前序遍历结果排序就是中序遍历。
所以,我们需要求出先序或者后序中的一种,然后在解码的时候,进行递归求解左子树和右子树和右子树即可。
怎么递归求解呢?以先序为例:
如果有左儿子,那么序列中下一个节点的值小于当前结点的值,对下一个节点找左儿子,直到找不到为止,然后找右儿子,如果下一个节点的值大于当前结点的值,那么是右儿子,再对右儿子找左右儿子……

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if (root == null) return "";
        List<Integer> ls = new ArrayList<Integer>();
        preOrder(root, ls);
        String str = ls.toString();
        return str.substring(1, str.length() - 1);
    }

    private void preOrder(TreeNode root, List<Integer> list) {
        if (root == null) return;
        list.add(root.val);
        preOrder(root.left, list);
        preOrder(root.right, list);
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if (data.isEmpty()) return null;
        String[] array = data.split(", ");
        Queue<Integer> qu = new LinkedList<Integer>();
        for (String tmp : array) {
            qu.add(Integer.parseInt(tmp));
        }
        return construct(-1, 100001, qu);
    }

    private TreeNode construct(int low, int high, Queue<Integer> qu) {
        if (qu.isEmpty() || qu.peek() < low || qu.peek() > high) {
            return null;
        }
        int val = qu.peek();
        qu.remove();
        TreeNode root = new TreeNode(val);
        
        root.left = construct(low, val, qu);
        root.right = construct(val, high, qu);
        return root;
    }
}

// Your Codec object will be instantiated and called as such:
// Codec ser = new Codec();
// Codec deser = new Codec();
// String tree = ser.serialize(root);
// TreeNode ans = deser.deserialize(tree);
// return ans;

2. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

2.1 动态规划

本次dp的状态是f[i],判断第0到第i个字符组成的子字符串可以被拆分成字典中所有的单词。
如何判断f[i]是true还是false呢?
从0到i-1,如果有f[j] == true && j ~ i 的子字符串能够在字典中找到对应的单词,说明f[i]为真,如果找不到这么一个f[j],那么就为假

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> dict;
        for (auto tmp : wordDict) {
            dict.emplace(tmp);
        }

        int n = s.size();
        vector<bool> dp(n + 1);
        dp[0] = true;
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j < i; j++) {
                if (dp[j] && (dict.find(s.substr(j, i - j))) != dict.end()) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[n];
    }
};

3. 接雨水

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

3.1 本人的垃圾方法

根据图推知,如果i点比它左边的点都高,或者比它右边的点都高,那么它不会接到雨水;反之,他坑定会接到雨水……
所以,怎么求i点左边最高点,和i点右边最高点呢?
动态规划,做左边为例,dp[i]记为到第i点的最高点,那么递推式:

  • d p [ i ] = m a x ( d p [ i − 1 ] , h e i g h t [ i ] ) dp[i] = max(dp[i - 1], height[i]) dp[i]=max(dp[i1],height[i])

右边同样的道理,这样扫描两边,我们就有了判断某一点是否能接到雨水了……
那么如果会接到雨水,接多少?
接离他最近的左非接水点和右非接水点中的最小值……
怎么求左非接水点和右非接水点呢?
向左向右扫描……

#include <vector>
#include <algorithm>
using namespace std;

class Solution {
public:
    int trap(vector<int>& height) {
        int n = height.size();
        if (n <= 2) return 0;
        vector<bool> canGetRain(n, false);
        vector<int> leftMost(n);
        vector<int> rightMost(n);
        leftMost[0] = height[0];
        rightMost[n - 1] = height[n - 1];

        for (int i = 1; i < n - 1; i++) {
            leftMost[i] = max(leftMost[i - 1], height[i]);
        }

        for (int i = n - 2; i >= 1; i--) {
            rightMost[i] = max(rightMost[i + 1], height[i]);
        }

        for (int i = 1; i < n - 1; i++) {
            canGetRain[i] = height[i] < min(leftMost[i], rightMost[i]) ? true : false;
        }

        int ans = 0;
        for (int i = 1; i < n - 1; i++) {
            if (canGetRain[i]) {
                ans += getCount(i, canGetRain, height) - height[i];
            }
        }
        return ans;
    }

    int getCount(int i, vector<bool> canGetRain, vector<int> height) {
        int left, right;
        for (int j = i - 1; j >= 0; j--) {
            if (!canGetRain[j]) {
                left = j;
                break;
            }
        }
        for (int j = i + 1; j < height.size(); j++) {
            if (!canGetRain[j]) {
                right = j;
                break;
            }
        }
        return min(height[left], height[right]);
    }
};

3.2 优秀解法

再想一想,最近左非接水点,和最近右非接水点,其实就是左边的最大值,和右边的最大值……
所以接水量可以简单地写成一句话:

  • W a t e r [ i ] = m i n ( l e f t M o s t [ i ] , r i g h t M o s t [ i ] ) − h e i g h t [ i ] Water[i] = min(leftMost[i], rightMost[i]) - height[i] Water[i]=min(leftMost[i],rightMost[i])height[i]
class Solution {
public:
    int trap(vector<int>& height) {
        int n = height.size();
        if (n == 0) {
            return 0;
        }
        vector<int> leftMax(n);
        leftMax[0] = height[0];
        for (int i = 1; i < n; ++i) {
            leftMax[i] = max(leftMax[i - 1], height[i]);
        }

        vector<int> rightMax(n);
        rightMax[n - 1] = height[n - 1];
        for (int i = n - 2; i >= 0; --i) {
            rightMax[i] = max(rightMax[i + 1], height[i]);
        }

        int ans = 0;
        for (int i = 0; i < n; ++i) {
            ans += min(leftMax[i], rightMax[i]) - height[i];
        }
        return ans;
    }
};

总结

应该是第一次做出困难题,虽然性能只干掉了5%的人,但好歹是做出来了的……
今天这两道题的思路都不算特别好想,如果是第一次写,很难马上列出递推式吧,只能慢慢想,慢慢观察,算是积累经验吧……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值