阅读视图

发现新文章,点击刷新页面。

每日一题-两个字符串的最小ASCII删除和🟡

给定两个字符串s1 和 s2,返回 使两个字符串相等所需删除字符的 ASCII 值的最小和 

 

示例 1:

输入: s1 = "sea", s2 = "eat"
输出: 231
解释: 在 "sea" 中删除 "s" 并将 "s" 的值(115)加入总和。
在 "eat" 中删除 "t" 并将 116 加入总和。
结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最小和。

示例 2:

输入: s1 = "delete", s2 = "leet"
输出: 403
解释: 在 "delete" 中删除 "dee" 字符串变成 "let",
将 100[d]+101[e]+101[e] 加入总和。在 "leet" 中删除 "e" 将 101[e] 加入总和。
结束时,两个字符串都等于 "let",结果即为 100+101+101+101 = 403 。
如果改为将两个字符串转换为 "lee" 或 "eet",我们会得到 433 或 417 的结果,比答案更大。

 

提示:

  • 0 <= s1.length, s2.length <= 1000
  • s1 和 s2 由小写英文字母组成

正难则反:计算最多保留的 ASCII 之和(Python/Java/C++/Go)

用 $s_1$ 和 $s_2$ 的 ASCII 值之和,减去保留的 ASCII 之和的最大值,就是删除字符的 ASCII 值之和的最小值。

计算最多保留的 ASCII 之和,方法和 1143. 最长公共子序列 一样:

  • 1143 题,$s_1[i] = s_2[j]$ 时,都保留,最长公共子序列长度增加 $1$。
  • 本题,$s_1[i] = s_2[j]$ 时,都保留,保留的 ASCII 之和增加 $\text{ASCII}(s_1[i])\cdot 2$。

所以只需把 1143 题的 $+1$ 改成 $+\text{ASCII}(s_1[i])\cdot 2$。

也可以改成 $+\text{ASCII}(s_1[i])$,最后返回时再乘以 $2$。

###py

class Solution:
    def minimumDeleteSum(self, s1: str, s2: str) -> int:
        n, m = len(s1), len(s2)
        total = sum(map(ord, s1)) + sum(map(ord, s2))

        f = [[0] * (m + 1) for _ in range(n + 1)]
        for i, x in enumerate(s1):
            for j, y in enumerate(s2):
                if x == y:
                    f[i + 1][j + 1] = f[i][j] + ord(x)
                else:
                    f[i + 1][j + 1] = max(f[i][j + 1], f[i + 1][j])
        return total - f[n][m] * 2

###java

class Solution {
    public int minimumDeleteSum(String s1, String s2) {
        int total = s1.chars().sum() + s2.chars().sum();

        char[] s = s1.toCharArray();
        char[] t = s2.toCharArray();
        int n = s.length;
        int m = t.length;

        int[][] f = new int[n + 1][m + 1];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (s[i] == t[j]) {
                    f[i + 1][j + 1] = f[i][j] + s[i];
                } else {
                    f[i + 1][j + 1] = Math.max(f[i][j + 1], f[i + 1][j]);
                }
            }
        }
        return total - f[n][m] * 2;
    }
}

###cpp

class Solution {
public:
    int minimumDeleteSum(string s1, string s2) {
        int n = s1.size(), m = s2.size();
        int total = reduce(s1.begin(), s1.end(), 0) + reduce(s2.begin(), s2.end(), 0);

        vector f(n + 1, vector<int>(m + 1));
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (s1[i] == s2[j]) {
                    f[i + 1][j + 1] = f[i][j] + s1[i];
                } else {
                    f[i + 1][j + 1] = max(f[i][j + 1], f[i + 1][j]);
                }
            }
        }
        return total - f[n][m] * 2;
    }
};

###go

func minimumDeleteSum(s1, s2 string) int {
n, m := len(s1), len(s2)
total := 0
for _, c := range s1 {
total += int(c)
}
for _, c := range s2 {
total += int(c)
}

f := make([][]int, n+1)
for i := range f {
f[i] = make([]int, m+1)
}
for i, x := range s1 {
for j, y := range s2 {
if x == y {
f[i+1][j+1] = f[i][j] + int(x)
} else {
f[i+1][j+1] = max(f[i][j+1], f[i+1][j])
}
}
}
return total - f[n][m]*2
}

空间优化:

###py

# 更快的写法见【Python3 手写 max】
class Solution:
    def minimumDeleteSum(self, s1: str, s2: str) -> int:
        m = len(s2)
        total = sum(map(ord, s1)) + sum(map(ord, s2))

        f = [0] * (m + 1)
        for x in s1:
            ord_x = ord(x)
            pre = 0  # f[0]
            for j, y in enumerate(s2):
                tmp = f[j + 1]
                if x == y:
                    f[j + 1] = pre + ord_x
                else:
                    f[j + 1] = max(f[j + 1], f[j])
                pre = tmp
        return total - f[m] * 2

###py

class Solution:
    def minimumDeleteSum(self, s1: str, s2: str) -> int:
        m = len(s2)
        total = sum(map(ord, s1)) + sum(map(ord, s2))

        f = [0] * (m + 1)
        for x in s1:
            ord_x = ord(x)
            pre = 0  # f[0]
            for j, y in enumerate(s2):
                tmp = f[j + 1]
                if x == y:
                    f[j + 1] = pre + ord_x
                elif f[j] > f[j + 1]:
                    f[j + 1] = f[j]
                pre = tmp
        return total - f[m] * 2

###java

class Solution {
    public int minimumDeleteSum(String s1, String s2) {
        int total = s1.chars().sum() + s2.chars().sum();

        char[] s = s1.toCharArray();
        char[] t = s2.toCharArray();
        int m = t.length;

        int[] f = new int[m + 1];
        for (char x : s) {
            int pre = 0; // f[0]
            for (int j = 0; j < m; j++) {
                int tmp = f[j + 1];
                if (x == t[j]) {
                    f[j + 1] = pre + x;
                } else {
                    f[j + 1] = Math.max(f[j + 1], f[j]);
                }
                pre = tmp;
            }
        }
        return total - f[m] * 2;
    }
}

###cpp

class Solution {
public:
    int minimumDeleteSum(string s1, string s2) {
        int m = s2.size();
        int total = reduce(s1.begin(), s1.end(), 0) + reduce(s2.begin(), s2.end(), 0);

        vector<int> f(m + 1);
        for (char x : s1) {
            int pre = 0; // f[0]
            for (int j = 0; j < m; j++) {
                int tmp = f[j + 1];
                if (x == s2[j]) {
                    f[j + 1] = pre + x;
                } else {
                    f[j + 1] = max(f[j + 1], f[j]);
                }
                pre = tmp;
            }
        }
        return total - f[m] * 2;
    }
};

###go

func minimumDeleteSum(s1, s2 string) int {
m := len(s2)
total := 0
for _, c := range s1 {
total += int(c)
}
for _, c := range s2 {
total += int(c)
}

f := make([]int, m+1)
for _, x := range s1 {
pre := 0 // f[0]
for j, y := range s2 {
tmp := f[j+1]
if x == y {
f[j+1] = pre + int(x)
} else {
f[j+1] = max(f[j+1], f[j])
}
pre = tmp
}
}
return total - f[m]*2
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(nm)$,其中 $n$ 是 $s_1$ 的长度,$m$ 是 $s_2$ 的长度。
  • 空间复杂度:$\mathcal{O}(m)$。

专题训练

见下面动态规划题单的「§4.1 最长公共子序列(LCS)」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

最长公共子序列 (LCS):「模版」&「输出」

如果想要查看作者更多文章,可以点击此处!!!🔥🔥🔥

为了本篇文章更好的观感,可以点击此处!!!

1143. 最长公共子序列

583. 两个字符串的删除操作

712. 两个字符串的最小ASCII删除和


最长递增子序列」针对一个字符串,求出其最长递增子序列 (废话文学!!) 详细介绍可见 动态规划设计:最长递增子序列

最长重复子数组」针对两个数组,求出其最长重复子数组 (子数组必须要连着) 详细介绍可见 最长重复子数组

而我们今天要介绍的是「最长公共子序列」,它是针对两个字符串,求出其最长公共子序列 (子序列可以不用连着)

模版归纳

首先结合题目 最长公共子序列,归纳总结出「最长公共子序列」问题的模版

毫无疑问这种类型的题目需要使用「DP」去解决!!这里给出一个例子 s1 = "abcde", s2 = "ace" ,下面所有分析都围绕该样例展开

先给出 dp[][] 数组的定义:dp[i][j] 表示子串 s1[0..i]s2[0..j] 最长公共子序列的长度

那么「状态转移方程」是什么呢?

  • 如果 s1[i] = s2[j]dp[i][j] = dp[i - 1][j - 1] + 1
  • 如果 s1[i] != s2[j]dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j])

为什么就是这样转移的呢?直接看下图:

5.svg

那么「base case」是什么呢?

6.svg

如上图粉色标记出来的就是 base case。橙色标记出来的是相等的情况,其余是不等的情况

完整模版

###java

public int longestCommonSubsequence(String text1, String text2) {
    int n1 = text1.length(), n2 = text2.length();
    int[][] dp = new int[n1 + 1][n2 + 1];
    for (int i = 1; i <= n1; i++) {
        for (int j = 1; j <= n2; j++) {
            char c1 = text1.charAt(i - 1);
            char c2 = text2.charAt(j - 1);
            // 相等情况
            if (c1 == c2) dp[i][j] = dp[i - 1][j - 1] + 1;
            // 不等情况
            else dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
        }
    }
    return dp[n1][n2];
}

✨ 如何输出最长公共子序列

「最长公共子序列」问题基本都是要求返回一个最值即可,但是有时候面试官喜欢不按常理出牌,让你输出最长公共子序列

我们可以通过构造出来的二维 dp 数组来得到最长公共子序列。如下图所示,从最后一个点开始往左上角的方向遍历

7.svg

如果 s1[i] = s2[j],那么当前字符肯定在最长公共子序列中;否在我们就向左或者向上遍历

至于选择「向左」还是「向上」的方向,这就要和构造 dp 的时候联系起来。我们是挑了一个最大值,所以遍历的方向也是谁大就往谁的方向遍历

###java

public int longestCommonSubsequence(String text1, String text2) {
    
    // 同上面的模版
    
    /* ------- print ------- */
    int i = n1, j = n2;
    StringBuffer sb = new StringBuffer();
    while (i > 0 && j > 0) {
        char c1 = text1.charAt(i - 1);
        char c2 = text2.charAt(j - 1);
        if (c1 == c2) {
            sb.append(c1);
            // 向左上角遍历
            i--; j--;
        } else {
            // 向上
            if (dp[i - 1][j] > dp[i][j - 1]) i--;
            // 向左
            else j--;
        }
    }
    System.out.println(sb.reverse());
    /* ------- end ------- */
    return dp[n1][n2];
}

两个字符串的最小ASCII删除和

题目详情可见 两个字符串的最小ASCII删除和

其实这个题目的底层也是「最长公共子序列」,只是问法稍微变化了一点

「需要被删除的字符 = 原字符串 - 最长公共子序列」

结合这个题目我们把 dp[][] 数组的定义稍微改改:dp[i][j] 表示子串 s1[0..i]s2[0..j] 最小 ASCII 删除和

那么「状态转移方程」是什么呢?(有点逆过程的意思!!!)

  • 如果 s1[i] = s2[j]dp[i][j] = dp[i - 1][j - 1] (不需要被删除)
  • 如果 s1[i] != s2[j]dp[i][j] = Math.min(dp[i - 1][j] + s1[i], dp[i][j - 1] + s2[j])

那么「base case」是什么呢?

8.svg

如上图粉色标记出来的就是 base case,e 表示 e 的 ASCII 值

###java

public int minimumDeleteSum(String s1, String s2) {
    int n1 = s1.length(), n2 = s2.length();
    int[][] dp = new int[n1 + 1][n2 + 1];
    // base case
    for (int i = 1; i <= n1; i++) dp[i][0] = dp[i - 1][0] + s1.charAt(i - 1);
    for (int i = 1; i <= n2; i++) dp[0][i] = dp[0][i - 1] + s2.charAt(i - 1);
    for (int i = 1; i <= n1; i++) {
        for (int j = 1; j <= n2; j++) {
            int c1 = s1.charAt(i - 1);
            int c2 = s2.charAt(j - 1);
            // 相等情况
            if (c1 == c2) dp[i][j] = dp[i - 1][j - 1];
            // 不等情况
            else dp[i][j] = Math.min(dp[i][j - 1] + c2, dp[i - 1][j] + c1);
        }
    }
    return dp[n1][n2];
}

LCS的dp解法转化而来 C++

首先给出LCS的模板解法:

int longestCommonSubsequence(string text1, string text2)
{
    int LCS[text1.size() + 1][text2.size() + 1];
    memset(LCS,0, sizeof(LCS));

    for (int i = 1; i <= text1.size(); ++i)
        for (int j = 1; j <= text2.size(); ++j)
        {
            if(text1[i - 1] == text2[j - 1])
                LCS[i][j] = LCS[i - 1][j - 1] + 1;
            else
                LCS[i][j] = max(LCS[i - 1][j],LCS[i][j - 1]);
        }
    return LCS[text1.size()][text2.size()];
}

那么如何改造这个模板来让他适应我们的问题呢?
因为在求LCS的时候我们是按照构造一个dp[i][j]表示以str1的第i项为结尾,str2的第j项为结尾,那么就会有:(LCS(i,j) <=> dp[i][j])

--------------------------------------
 * if(str1.n == str2.m):
 *      LCS(n,m) = LCS(n - 1,m - 1) + 1
 * else
 *      LCS(n,m) = max{LCS(n - 1,m),LCS(n,m - 1)}
--------------------------------------

所以我们就会有一种想法,对这个LCS求解的dp过程在进行一次约数,肯定可以得到我们的目标LCS

int minimumDeleteSum(string s1, string s2)
{
    int len1 = s1.length();
    int len2 = s2.length();

    int dp[len1 + 1][len2 + 1];
    memset(dp,0, sizeof(dp));

    for (int i = 1; i <= len1; ++i)
        for (int j = 1; j <= len2; ++j)
        {
            if(s1[i - 1] == s2[j - 1])
                dp[i][j] = dp[i - 1][j - 1] + s1[i - 1];
            else
                dp[i][j] = max(dp[i - 1][j],dp[i][j - 1]);
        }

    int sum = 0;
    for (int i = 0; i < len1; ++i)
        sum += s1[i];
    for (int i = 0; i < len2; ++i)
        sum += s2[i];
    return sum - 2 * dp[len1][len2];
}

别忘了最后返回的值是两个string的ASCII和减去两个LCS的ASCII的sum哦

每日一题-具有所有最深节点的最小子树🟡

给定一个根为 root 的二叉树,每个节点的深度是 该节点到根的最短距离

返回包含原始树中所有 最深节点最小子树

如果一个节点在 整个树 的任意节点之间具有最大的深度,则该节点是 最深的

一个节点的 子树 是该节点加上它的所有后代的集合。

 

示例 1:

输入:root = [3,5,1,6,2,0,8,null,null,7,4]
输出:[2,7,4]
解释:
我们返回值为 2 的节点,在图中用黄色标记。
在图中用蓝色标记的是树的最深的节点。
注意,节点 5、3 和 2 包含树中最深的节点,但节点 2 的子树最小,因此我们返回它。

示例 2:

输入:root = [1]
输出:[1]
解释:根节点是树中最深的节点。

示例 3:

输入:root = [0,1,3,null,2]
输出:[2]
解释:树中最深的节点为 2 ,有效子树为节点 2、1 和 0 的子树,但节点 2 的子树最小。

 

提示:

  • 树中节点的数量在 [1, 500] 范围内。
  • 0 <= Node.val <= 500
  • 每个节点的值都是 独一无二 的。

 

注意:本题与力扣 1123 重复:https://leetcode.cn/problems/lowest-common-ancestor-of-deepest-leaves

两种 O(n) 递归写法(Python/Java/C++/Go/JS)

前置题目236. 二叉树的最近公共祖先

视频讲解二叉树的最近公共祖先【基础算法精讲 12】

方法一:递归递归,有递有归

{:width=360px}

看上图(示例 1),这棵树的节点 $3,5,2$ 都是最深叶节点 $7,4$ 的公共祖先,但只有节点 $2$ 是最近的公共祖先。

上面视频中提到,如果我们要找的节点只在左子树中,那么最近公共祖先也必然只在左子树中。对于本题,如果左子树的最大深度比右子树的大,那么最深叶结点就只在左子树中,所以最近公共祖先也只在左子树中。

如果左右子树的最大深度一样呢?当前节点一定是最近公共祖先吗?

不一定。比如节点 $1$ 的左右子树最深叶节点 $0,8$ 的深度都是 $2$,但该深度并不是全局最大深度,所以节点 $1$ 并不能是答案。

根据以上讨论,正确做法如下:

  1. 递归这棵二叉树,同时维护全局最大深度 $\textit{maxDepth}$。
  2. 在「递」的时候往下传 $\textit{depth}$,用来表示当前节点的深度。
  3. 在「归」的时候往上传当前子树最深叶节点的深度。
  4. 设左子树最深叶节点的深度为 $\textit{leftMaxDepth}$,右子树最深叶节点的深度为 $\textit{rightMaxDepth}$。如果 $\textit{leftMaxDepth}=\textit{rightMaxDepth}=\textit{maxDepth}$,那么更新答案为当前节点。⚠注意:这并不代表我们立刻找到了答案,如果后面发现了更深的叶节点,答案还会更新。
class Solution:
    def subtreeWithAllDeepest(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        max_depth = -1  # 全局最大深度
        ans = None

        def dfs(node: Optional[TreeNode], depth: int) -> int:
            nonlocal ans, max_depth
            if node is None:
                max_depth = max(max_depth, depth)  # 维护全局最大深度
                return depth

            left_max_depth = dfs(node.left, depth + 1)  # 获取左子树最深叶节点的深度
            right_max_depth = dfs(node.right, depth + 1)  # 获取右子树最深叶节点的深度

            if left_max_depth == right_max_depth == max_depth:
                ans = node  # node 可能是答案

            return max(left_max_depth, right_max_depth)  # 当前子树最深叶节点的深度

        dfs(root, 0)
        return ans
class Solution {
    private int maxDepth = -1; // 全局最大深度
    private TreeNode ans;

    public TreeNode subtreeWithAllDeepest(TreeNode root) {
        dfs(root, 0);
        return ans;
    }

    private int dfs(TreeNode node, int depth) {
        if (node == null) {
            maxDepth = Math.max(maxDepth, depth); // 维护全局最大深度
            return depth;
        }

        int leftMaxDepth = dfs(node.left, depth + 1); // 获取左子树最深叶节点的深度
        int rightMaxDepth = dfs(node.right, depth + 1); // 获取右子树最深叶节点的深度

        if (leftMaxDepth == rightMaxDepth && leftMaxDepth == maxDepth) {
            ans = node; // node 可能是答案
        }

        return Math.max(leftMaxDepth, rightMaxDepth); // 当前子树最深叶节点的深度
    }
}
class Solution {
public:
    TreeNode* subtreeWithAllDeepest(TreeNode* root) {
        int max_depth = -1; // 全局最大深度
        TreeNode* ans;

        auto dfs = [&](this auto&& dfs, TreeNode* node, int depth) {
            if (node == nullptr) {
                max_depth = max(max_depth, depth); // 维护全局最大深度
                return depth;
            }

            int left_max_depth = dfs(node->left, depth + 1); // 获取左子树最深叶节点的深度
            int right_max_depth = dfs(node->right, depth + 1); // 获取右子树最深叶节点的深度

            if (left_max_depth == right_max_depth && left_max_depth == max_depth) {
                ans = node; // node 可能是答案
            }

            return max(left_max_depth, right_max_depth); // 当前子树最深叶节点的深度
        };

        dfs(root, 0);
        return ans;
    }
};
func subtreeWithAllDeepest(root *TreeNode) (ans *TreeNode) {
    maxDepth := -1 // 全局最大深度

    var dfs func(*TreeNode, int) int
    dfs = func(node *TreeNode, depth int) int {
        if node == nil {
            maxDepth = max(maxDepth, depth) // 维护全局最大深度
            return depth
        }

        leftMaxDepth := dfs(node.Left, depth+1) // 获取左子树最深叶节点的深度
        rightMaxDepth := dfs(node.Right, depth+1) // 获取右子树最深叶节点的深度

        if leftMaxDepth == rightMaxDepth && leftMaxDepth == maxDepth {
            ans = node // node 可能是答案
        }

        return max(leftMaxDepth, rightMaxDepth) // 当前子树最深叶节点的深度
    }

    dfs(root, 0)
    return
}
var subtreeWithAllDeepest = function(root) {
    let maxDepth = -1; // 全局最大深度
    let ans = null;

    function dfs(node, depth) {
        if (node === null) {
            maxDepth = Math.max(maxDepth, depth); // 维护全局最大深度
            return depth;
        }

        const leftMaxDepth = dfs(node.left, depth + 1); // 获取左子树最深叶节点的深度
        const rightMaxDepth = dfs(node.right, depth + 1); // 获取右子树最深叶节点的深度

        if (leftMaxDepth === rightMaxDepth && leftMaxDepth === maxDepth) {
            ans = node; // node 可能是答案
        }

        return Math.max(leftMaxDepth, rightMaxDepth); // 当前子树最深叶节点的深度
    }

    dfs(root, 0);
    return ans;
};

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$。每个节点都会恰好访问一次。
  • 空间复杂度:$\mathcal{O}(n)$。最坏情况下,二叉树是一条链,递归需要 $\mathcal{O}(n)$ 的栈空间。

方法二:自底向上

也可以不用全局变量,而是把每棵子树都看成是一个「子问题」,即对于每棵子树,我们需要知道:

  • 这棵子树最深叶结点的深度。这里是指叶子在这棵子树内的深度,而不是在整棵二叉树的视角下的深度。相当于这棵子树的高度
  • 这棵子树的最深叶结点的最近公共祖先 $\textit{lca}$。

分类讨论:

  • 设子树的根节点为 $\textit{node}$,$\textit{node}$ 的左子树的高度为 $\textit{leftHeight}$,$\textit{node}$ 的右子树的高度为 $\textit{rightHeight}$。
  • 如果 $\textit{leftHeight} > \textit{rightHeight}$,那么子树的高度为 $\textit{leftHeight} + 1$,$\textit{lca}$ 是左子树的 $\textit{lca}$。
  • 如果 $\textit{leftHeight} < \textit{rightHeight}$,那么子树的高度为 $\textit{rightHeight} + 1$,$\textit{lca}$ 是右子树的 $\textit{lca}$。
  • 如果 $\textit{leftHeight} = \textit{rightHeight}$,那么子树的高度为 $\textit{leftHeight} + 1$,$\textit{lca}$ 就是 $\textit{node}$。反证法:如果 $\textit{lca}$ 在左子树中,那么 $\textit{lca}$ 不是右子树的最深叶结点的祖先,这不对;如果 $\textit{lca}$ 在右子树中,那么 $\textit{lca}$ 不是左子树的最深叶结点的祖先,这也不对;如果 $\textit{lca}$ 在 $\textit{node}$ 的上面,那就不符合「最近」的要求。所以 $\textit{lca}$ 只能是 $\textit{node}$。
class Solution:
    def subtreeWithAllDeepest(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        def dfs(node: Optional[TreeNode]) -> Tuple[int, Optional[TreeNode]]:
            if node is None:
                return 0, None

            left_height, left_lca = dfs(node.left)
            right_height, right_lca = dfs(node.right)

            if left_height > right_height:  # 左子树更高
                return left_height + 1, left_lca
            if left_height < right_height:  # 右子树更高
                return right_height + 1, right_lca
            return left_height + 1, node  # 一样高

        return dfs(root)[1]
class Solution {
    private record Pair(int height, TreeNode lca) {}

    public TreeNode subtreeWithAllDeepest(TreeNode root) {
        return dfs(root).lca;
    }

    private Pair dfs(TreeNode node) {
        if (node == null) {
            return new Pair(0, null);
        }

        Pair left = dfs(node.left);
        Pair right = dfs(node.right);

        if (left.height > right.height) { // 左子树更高
            return new Pair(left.height + 1, left.lca);
        }
        if (left.height < right.height) { // 右子树更高
            return new Pair(right.height + 1, right.lca);
        }
        return new Pair(left.height + 1, node); // 一样高
    }
}
class Solution {
    pair<int, TreeNode*> dfs(TreeNode* node) {
        if (node == nullptr) {
            return {0, nullptr};
        }

        auto [left_height, left_lca] = dfs(node->left);
        auto [right_height, right_lca] = dfs(node->right);

        if (left_height > right_height) { // 左子树更高
            return {left_height + 1, left_lca};
        }
        if (left_height < right_height) { // 右子树更高
            return {right_height + 1, right_lca};
        }
        return {left_height + 1, node}; // 一样高
    }

public:
    TreeNode* subtreeWithAllDeepest(TreeNode* root) {
        return dfs(root).second;
    }
};
func dfs(node *TreeNode) (int, *TreeNode) {
    if node == nil {
        return 0, nil
    }

    leftHeight, leftLCA := dfs(node.Left)
    rightHeight, rightLCA := dfs(node.Right)

    if leftHeight > rightHeight { // 左子树更高
        return leftHeight + 1, leftLCA
    }
    if leftHeight < rightHeight { // 右子树更高
        return rightHeight + 1, rightLCA
    }
    return leftHeight + 1, node // 一样高
}

func subtreeWithAllDeepest(root *TreeNode) *TreeNode {
    _, lca := dfs(root)
    return lca
}
function dfs(node) {
    if (node === null) {
        return [0, null];
    }

    const [leftHeight, leftLca] = dfs(node.left);
    const [rightHeight, rightLca] = dfs(node.right);

    if (leftHeight > rightHeight) { // 左子树更高
        return [leftHeight + 1, leftLca];
    }
    if (leftHeight < rightHeight) { // 右子树更高
        return [rightHeight + 1, rightLca];
    }
    return [leftHeight + 1, node]; // 一样高
}

var subtreeWithAllDeepest = function(root) {
    return dfs(root)[1];
};

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$。每个节点都会恰好访问一次。
  • 空间复杂度:$\mathcal{O}(n)$。最坏情况下,二叉树是一条链,递归需要 $\mathcal{O}(n)$ 的栈空间。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

Java递归, O(n)一次遍历

image.png

看了下下面的题解,除了官方题解用多个返回值只遍历了一次,其他题解大都反复计算深度,其实复杂度是O(nlogn)
本题解进行一次遍历得到结果

主要思想:

在一次遍历深度的过程中,找到左右子树深度都为最大值的节点记录下来

class Solution {
  private int maxDeep = Integer.MIN_VALUE;
  private TreeNode result;
  public TreeNode subtreeWithAllDeepest(TreeNode root) {
    maxDeep(root, 0);
    return result;
  }
  private int maxDeep(TreeNode node, int deep) {
    if (node == null) {
      return deep;
    }
    int left = maxDeep(node.left, deep+1);
    int right = maxDeep(node.right, deep+1);
    int currentMax = Math.max(left, right);
    maxDeep = Math.max(maxDeep, currentMax);
    if (left == maxDeep && right == maxDeep) {
      result = node;
    }
    return currentMax;
  }
}

粗俗易懂:直接看代码和注解,简单

执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户

class Solution {
    // 思路:从每个树开始,获得当前节点的左右子树的最大深度
    // 深度相同,说明最深的节点在这个节点两边,那这个节点就是结果
    // 如果深度不相同,则去深度大的子树继续判断,最终就能得到结果
    public TreeNode subtreeWithAllDeepest(TreeNode root) {
        if (root == null) return root;

        // 获取当前节点的左右子树的最大深度
        int leftMaxDepth = getMaxDepth(root.left);
        int rightMaxDepth = getMaxDepth(root.right);

        // 如果两边最大深度相同,则这个节点就是结果
        if (leftMaxDepth == rightMaxDepth) return root;

        // 不相等,那就去深度大的子树那边继续找
        if (leftMaxDepth > rightMaxDepth){
            return subtreeWithAllDeepest(root.left);
        }

        return subtreeWithAllDeepest(root.right);
    }

    public int getMaxDepth(TreeNode root){
        if (root == null) return 0;

        return Math.max(getMaxDepth(root.left), getMaxDepth(root.right)) + 1;
    }
}

分享一下思考过程 C++

  • 知识点:动态规划
  • 时间复杂度:O(n*m);n,m 分别为两个序列的长度。

动态规划题目一般要先想好两个问题:

  • 状态如何定义。
  • 状态之间如何转移。

对于该题,最终目标是在分别两个序列中选取等长且非空的子序列,使得两个子序列的点积最大。
即,在 nums1 的前 n 个数中,在 nums2 的前 m 个数字中分别选取等长且非空的子序列, 使其点积最大。
推而广之,我们可以将问题表示为,在nums1的前 x (x <= n)个数字中,在nums2的前y(y <= m)个数字中,分别选取等长且非空的子序列, 使其点积最大。
为了方便,我们用 f(x, y) 表示子问题的最优方案的点积。
当(x, y) = (n,m) 时,f(x,y) 就是最终答案。

状态转移主要是分析状态之间的关联或差异,利用小问题的解高效的构造大问题的解。
来思考下该题状态如何转移,该题的特点是小问题总是为大问题的前缀,总是可以向小问题中追加数字得到一个大问题。
设 nx = nums1[x],ny = nums2[y]。
f(x,y) 可能由以下几个状态转移得到:

  • 向 f(x-1, y-1) 追加 nx,ny 获得 f(x, y)。
  • 向 f(x, y-1) 追加 ny 获得 f(x, y)。
  • 向 f(x-1, y) 追加 nx 获得 f(x,y)。

当然,也可以同时追加多个数字,由更小的问题获得 f(x, y),但这本质上还是通过上述三种子问题间接转移过来的。
那么,为何f(x-1,y-1) 不能用 f(x-1, y) 或者 f(x, y-1) 间接转移过来呢?因为在求解过程中要考虑nx 和 ny 在对应位置的情况。

总结一下,该题的状态方程如下:
$$
f(x,y) = max \left{ \begin{array}{c}
nxny&, 有且只有 nx,ny\
nx
ny + f(x-1, y-1)&, 包含 nx,ny \
f(x, y-1)&, 不包含 nx \
f(x-1, y)&, 不包含 ny \
f(x-1, y-1)&, 不包含 nx,ny\
\end{array}\right.
$$

###cpp

class Solution {
public:
    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int dp[501][501];
        for(int i = 0; i <= nums1.size(); i++) {
            for(int j = 0; j <= nums2.size(); j++) {
                dp[i][j] = -1000*1000*500;
            }
        }
        for(int i = 1; i <= nums1.size(); i++) {
            for(int j = 1; j <= nums2.size(); j++) {
                int a = nums1[i-1];
                int b = nums2[j-1];
                dp[i][j] = max(dp[i][j], a*b);
                dp[i][j] = max(dp[i][j], dp[i-1][j-1] + a*b);
                dp[i][j] = max(dp[i][j], dp[i-1][j-1]);
                dp[i][j] = max(dp[i][j], dp[i-1][j]);
                dp[i][j] = max(dp[i][j], dp[i][j-1]);
            }
        }
        return dp[nums1.size()][nums2.size()];
    }
};

如果感觉有点意思,可以关注👏HelloNebula👏

  • 分享周赛题解
  • 分享计算机专业课知识
  • 分享C++相关岗位面试题
  • 分享专业书籍PDF

每日一题-两个子序列的最大点积🔴

给你两个数组 nums1 和 nums2 。

请你返回 nums1nums2 中两个长度相同的 非空 子序列的最大点积。

数组的非空子序列是通过删除原数组中某些元素(可能一个也不删除)后剩余数字组成的序列,但不能改变数字间相对顺序。比方说,[2,3,5] 是 [1,2,3,4,5] 的一个子序列而 [1,5,3] 不是。

 

示例 1:

输入:nums1 = [2,1,-2,5], nums2 = [3,0,-6]
输出:18
解释:从 nums1 中得到子序列 [2,-2] ,从 nums2 中得到子序列 [3,-6] 。
它们的点积为 (2*3 + (-2)*(-6)) = 18 。

示例 2:

输入:nums1 = [3,-2], nums2 = [2,-6,7]
输出:21
解释:从 nums1 中得到子序列 [3] ,从 nums2 中得到子序列 [7] 。
它们的点积为 (3*7) = 21 。

示例 3:

输入:nums1 = [-1,-1], nums2 = [1,1]
输出:-1
解释:从 nums1 中得到子序列 [-1] ,从 nums2 中得到子序列 [1] 。
它们的点积为 -1 。

 

提示:

  • 1 <= nums1.length, nums2.length <= 500
  • -1000 <= nums1[i], nums2[i] <= 1000

 

点积:

定义 a = [a1a2,…, an] b = [b1b2,…, bn] 的点积为:

\mathbf{a}\cdot \mathbf{b} = \sum_{i=1}^n a_ib_i = a_1b_1 + a_2b_2 + \cdots + a_nb_n 

这里的 Σ 指示总和符号。

教你一步步思考 DP:从记忆化搜索到递推到空间优化(Python/Java/C++/Go)

一、寻找子问题

为方便描述,下文把 $\textit{nums}_1$ 和 $\textit{nums}_2$ 简称为 $a$ 和 $b$。

在示例 1 中,我们要解决的问题(原问题)是:

  • 从 $a=[2,1,-2,5]$ 和 $b=[3,0,-6]$ 中选两个长度相等的非空子序列 $c$ 和 $d$,计算 $c$ 和 $d$ 的点积的最大值。

注意:选出的子序列必须是非空的。

考虑从右往左选数字,用「选或不选」分类讨论:

  • 选 $a[3]$ 和 $b[2]$,需要解决的子问题为:从 $a=[2,1,-2]$ 和 $b=[3,0]$ 中选两个长度相等的子序列,计算两个子序列点积的最大值。由于我们选了元素,所以子序列可以为空。但这样思考的话,子问题就和原问题不相似了。为了保证子问题和原问题相似,我们可以再细分为两种情况:
    • 选 $a[3]$ 和 $b[2]$,且前面不再选数字。这意味着点积就是 $a[3]\cdot b[2]$。
    • 选 $a[3]$ 和 $b[2]$,且前面还要选数字。需要解决的子问题为:从 $a=[2,1,-2]$ 和 $b=[3,0]$ 中选两个长度相等的非空子序列,计算两个子序列点积的最大值。
  • 不选 $a[3]$,需要解决的子问题为:从 $a=[2,1,-2]$ 和 $b=[3,0,-6]$ 中选两个长度相等的非空子序列,计算两个子序列点积的最大值。
  • 不选 $b[2]$,需要解决的子问题为:从 $a=[2,1,-2,5]$ 和 $b=[3,0]$ 中选两个长度相等的非空子序列,计算两个子序列点积的最大值。

由于选或不选都会把原问题变成一个和原问题相似的、规模更小的子问题,所以可以用递归解决。

注 1:从右往左思考,主要是为了方便把递归翻译成递推。从左往右思考也是可以的。

注 2:动态规划有「选或不选」和「枚举选哪个」两种基本思考方式。子序列相邻无关一般是「选或不选」,子序列相邻相关(例如 LIS 问题)一般是「枚举选哪个」。本题用到的是「选或不选」。

二、状态定义与状态转移方程

根据上面的讨论,定义状态为 $\textit{dfs}(i,j)$,表示从 $a$ 的前缀 $[0,i]$ 和 $b$ 的前缀 $[0,j]$ 中选两个长度相等的非空子序列,计算两个子序列点积的最大值。

接下来,思考如何从一个状态转移到另一个状态。

用「选或不选」分类讨论:

  • 选 $a[i]$ 和 $b[j]$,且前面不再选数字。这意味着点积就是 $a[i]\cdot b[j]$。
  • 选 $a[i]$ 和 $b[j]$,且前面还要选数字。需要解决的子问题为:从 $a$ 的前缀 $[0,i-1]$ 和 $b$ 的前缀 $[0,j-1]$ 中选两个长度相等的非空子序列,计算两个子序列点积的最大值,即 $\textit{dfs}(i-1,j-1)$。再加上 $a[i]\cdot b[j]$,就得到了 $\textit{dfs}(i,j)$。
  • 前两种情况可以合并为:$\max(\textit{dfs}(i-1,j-1), 0) + a[i]\cdot b[j]$。
  • 不选 $a[i]$,需要解决的子问题为:从 $a$ 的前缀 $[0,i-1]$ 和 $b$ 的前缀 $[0,j]$ 中选两个长度相等的非空子序列,计算两个子序列点积的最大值,即 $\textit{dfs}(i-1,j)$。
  • 不选 $b[j]$,需要解决的子问题为:从 $a$ 的前缀 $[0,i]$ 和 $b$ 的前缀 $[0,j-1]$ 中选两个长度相等的非空子序列,计算两个子序列点积的最大值,即 $\textit{dfs}(i,j-1)$。

这四种情况取最大值,就得到了 $\textit{dfs}(i,j)$,即

$$
\textit{dfs}(i,j) = \max{\max(\textit{dfs}(i-1,j-1), 0) + a[i]\cdot b[j], \textit{dfs}(i-1,j),\textit{dfs}(i,j-1)}
$$

递归边界:$\textit{dfs}(-1,j)=\textit{dfs}(i,-1)=-\infty$。此时其中一个数组没有元素,无法选出非空子序列,不合法。用 $-\infty$ 表示不合法的状态,从而保证 $\max$ 不会取到不合法的状态。

递归入口:$\textit{dfs}(n-1,m-1)$,这是原问题,也是答案。其中 $n$ 是 $a$ 的长度,$m$ 是 $b$ 的长度。

三、递归搜索 + 保存递归返回值 = 记忆化搜索

考虑到整个递归过程中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:

  • 如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 $\textit{memo}$ 数组中。
  • 如果一个状态不是第一次遇到($\textit{memo}$ 中保存的结果不等于 $\textit{memo}$ 的初始值),那么可以直接返回 $\textit{memo}$ 中保存的结果。

注意:$\textit{memo}$ 数组的初始值一定不能等于要记忆化的值!例如初始值设置为 $0$,并且要记忆化的 $\textit{dfs}(i,j)$ 也等于 $0$,那就没法判断 $0$ 到底表示第一次遇到这个状态,还是表示之前遇到过了,从而导致记忆化失效。一般把初始值设置为 $-1$,但本题子序列点积可能是负数,可以初始化为 $\infty$。

Python 用户可以无视上面这段,直接用 @cache 装饰器。

具体请看视频讲解 动态规划入门:从记忆化搜索到递推【基础算法精讲 17】,其中包含把记忆化搜索 1:1 翻译成递推的技巧。

###py

class Solution:
    def maxDotProduct(self, nums1: List[int], nums2: List[int]) -> int:
        # 返回从 nums1[:i+1] 和 nums2[:j+1] 中选两个长度相同的【非空】子序列的最大点积
        @cache  # 缓存装饰器,避免重复计算 dfs(一行代码实现记忆化)
        def dfs(i: int, j: int) -> int:
            if i < 0 or j < 0:
                # 其中一个数组没有元素,无法选出非空子序列
                return -inf  # 下面计算 max 不会取到无解情况

            # 选 nums1[i] 和 nums2[j]
            # 和前面的子序列拼起来,或者不拼(作为子序列的第一个数)
            res1 = max(dfs(i - 1, j - 1), 0) + nums1[i] * nums2[j]

            # 不选 nums1[i]
            res2 = dfs(i - 1, j)

            # 不选 nums2[j]
            res3 = dfs(i, j - 1)

            return max(res1, res2, res3)

        return dfs(len(nums1) - 1, len(nums2) - 1)

###java

class Solution {
    public int maxDotProduct(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int m = nums2.length;

        int[][] memo = new int[n][m];
        for (int[] row : memo) {
            Arrays.fill(row, Integer.MAX_VALUE);
        }

        return dfs(n - 1, m - 1, nums1, nums2, memo);
    }

    // 从 nums1[0,i] 和 nums2[0,j] 中选两个长度相同的【非空】子序列的最大点积
    private int dfs(int i, int j, int[] nums1, int[] nums2, int[][] memo) {
        if (i < 0 || j < 0) {
            // 其中一个数组没有元素,无法选出非空子序列
            return Integer.MIN_VALUE; // 下面计算 max 不会取到无解情况
        }

        if (memo[i][j] != Integer.MAX_VALUE) { // 之前计算过
            return memo[i][j];
        }

        // 选 nums1[i] 和 nums2[j]
        // 和前面的子序列拼起来,或者不拼(作为子序列的第一个数)
        int res1 = Math.max(dfs(i - 1, j - 1, nums1, nums2, memo), 0) + nums1[i] * nums2[j];

        // 不选 nums1[i]
        int res2 = dfs(i - 1, j, nums1, nums2, memo);

        // 不选 nums2[j]
        int res3 = dfs(i, j - 1, nums1, nums2, memo);

        memo[i][j] = Math.max(res1, Math.max(res2, res3)); // 记忆化
        return memo[i][j];
    }
}

###cpp

class Solution {
public:
    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size();
        int m = nums2.size();
        vector memo(n, vector<int>(m, INT_MAX));

        // 从 nums1[0,i] 和 nums2[0,j] 中选两个长度相同的【非空】子序列的最大点积
        auto dfs = [&](this auto&& dfs, int i, int j) -> int {
            if (i < 0 || j < 0) {
                // 其中一个数组没有元素,无法选出非空子序列
                return INT_MIN; // 下面计算 max 不会取到无解情况
            }

            int& res = memo[i][j]; // 注意这里是引用
            if (res != INT_MAX) { // 之前计算过
                return res;
            }

            // 选 nums1[i] 和 nums2[j]
            // 和前面的子序列拼起来,或者不拼(作为子序列的第一个数)
            res = max(dfs(i - 1, j - 1), 0) + nums1[i] * nums2[j];

            // 不选 nums1[i]
            res = max(res, dfs(i - 1, j));

            // 不选 nums2[j]
            res = max(res, dfs(i, j - 1));

            return res;
        };

        return dfs(n - 1, m - 1);
    }
};

###go

func maxDotProduct(nums1, nums2 []int) int {
n := len(nums1)
m := len(nums2)
memo := make([][]int, n)
for i := range memo {
memo[i] = make([]int, m)
for j := range memo[i] {
memo[i][j] = math.MaxInt
}
}

// 从 nums1[:i+1] 和 nums2[:j+1] 中选两个长度相同的【非空】子序列的最大点积
var dfs func(int, int) int
dfs = func(i, j int) int {
if i < 0 || j < 0 {
// 其中一个数组没有元素,无法选出非空子序列
return math.MinInt // 下面计算 max 不会取到无解情况
}

p := &memo[i][j]
if *p != math.MaxInt { // 之前计算过
return *p
}

// 选 nums1[i] 和 nums2[j]
// 和前面的子序列拼起来,或者不拼(作为子序列的第一个数)
res1 := max(dfs(i-1, j-1), 0) + nums1[i]*nums2[j]

// 不选 nums1[i]
res2 := dfs(i-1, j)

// 不选 nums2[j]
res3 := dfs(i, j-1)

*p = max(res1, res2, res3) // 记忆化
return *p
}

return dfs(n-1, m-1)
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(nm)$,其中 $n$ 是 $\textit{nums}_1$ 的长度,$m$ 是 $\textit{nums}_2$ 的长度。由于每个状态只会计算一次,动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。本题状态个数等于 $\mathcal{O}(nm)$,单个状态的计算时间为 $\mathcal{O}(1)$,所以总的时间复杂度为 $\mathcal{O}(nm)$。
  • 空间复杂度:$\mathcal{O}(nm)$。保存多少状态,就需要多少空间。

四、1:1 翻译成递推

我们可以去掉递归中的「递」,只保留「归」的部分,即自底向上计算。

具体来说,$f[i+1][j+1]$ 的定义和 $\textit{dfs}(i,j)$ 的定义是一样的,都表示从 $a$ 的前缀 $[0,i]$ 和 $b$ 的前缀 $[0,j]$ 中选两个长度相等的非空子序列,计算两个子序列点积的最大值。这里 $+1$ 是为了把 $\textit{dfs}(-1,j)$ 和 $\textit{dfs}(i,-1)$ 也翻译过来,这样我们可以把 $f[0][j]$ 和 $f[i][0]$ 作为初始值。

相应的递推式(状态转移方程)也和 $\textit{dfs}$ 一样:

$$
f[i+1][j+1] = \max{\max(f[i][j], 0) + a[i]\cdot b[j], f[i][j+1],f[i+1][j]}
$$

初始值:$f$ 第一行和第一列初始化成 $-\infty$,翻译自递归边界 $\textit{dfs}(-1,j)=\textit{dfs}(i,-1)=-\infty$。

答案为 $f[n][m]$,翻译自递归入口 $\textit{dfs}(n-1,m-1)$。

###py

class Solution:
    def maxDotProduct(self, nums1: List[int], nums2: List[int]) -> int:
        n, m = len(nums1), len(nums2)
        f = [[-inf] * (m + 1) for _ in range(n + 1)]
        for i, x in enumerate(nums1):
            for j, y in enumerate(nums2):
                f[i + 1][j + 1] = max(max(f[i][j], 0) + x * y, f[i][j + 1], f[i + 1][j])
        return f[n][m]

###java

class Solution {
    public int maxDotProduct(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int m = nums2.length;

        int[][] f = new int[n + 1][m + 1];
        for (int[] row : f) {
            Arrays.fill(row, Integer.MIN_VALUE);
        }

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                f[i + 1][j + 1] = Math.max(
                    Math.max(f[i][j], 0) + nums1[i] * nums2[j],
                    Math.max(f[i][j + 1], f[i + 1][j])
                );
            }
        }
        return f[n][m];
    }
}

###cpp

class Solution {
public:
    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size();
        int m = nums2.size();
        vector f(n + 1, vector<int>(m + 1, INT_MIN));
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                int res1 = max(f[i][j], 0) + nums1[i] * nums2[j];
                // 注:max({...}) 比 max(..., max(...)) 慢
                f[i + 1][j + 1] = max(res1, max(f[i][j + 1], f[i + 1][j]));
            }
        }
        return f[n][m];
    }
};

###go

func maxDotProduct(nums1, nums2 []int) int {
n := len(nums1)
m := len(nums2)
f := make([][]int, n+1)
for i := range f {
f[i] = make([]int, m+1)
for j := range f[i] {
f[i][j] = math.MinInt
}
}

for i, x := range nums1 {
for j, y := range nums2 {
f[i+1][j+1] = max(max(f[i][j], 0)+x*y, f[i][j+1], f[i+1][j])
}
}
return f[n][m]
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(nm)$,其中 $n$ 是 $\textit{nums}_1$ 的长度,$m$ 是 $\textit{nums}_2$ 的长度。
  • 空间复杂度:$\mathcal{O}(nm)$。

五、空间优化

类似 1143. 最长公共子序列 的空间优化方法,只用一个长为 $m+1$ 的一维数组,原理讲解请看 最长公共子序列 编辑距离【基础算法精讲 19】

###py

# 更快的写法见【Python3 手写 max】
class Solution:
    def maxDotProduct(self, nums1: List[int], nums2: List[int]) -> int:
        m = len(nums2)
        f = [-inf] * (m + 1)
        for x in nums1:
            pre = f[0]
            for j, y in enumerate(nums2):
                tmp = f[j + 1]
                f[j + 1] = max(max(pre, 0) + x * y, f[j + 1], f[j])
                pre = tmp
        return f[m]

###py

class Solution:
    def maxDotProduct(self, nums1: List[int], nums2: List[int]) -> int:
        m = len(nums2)
        f = [-inf] * (m + 1)
        for x in nums1:
            pre = f[0]
            for j, y in enumerate(nums2):
                tmp = f[j + 1]
                res = x * y
                if pre > 0: res += pre
                if f[j] > res: res = f[j]
                if f[j + 1] > res: res = f[j + 1]
                f[j + 1] = res
                pre = tmp
        return f[m]

###java

class Solution {
    public int maxDotProduct(int[] nums1, int[] nums2) {
        int m = nums2.length;
        int[] f = new int[m + 1];
        Arrays.fill(f, Integer.MIN_VALUE);
        for (int x : nums1) {
            int pre = f[0];
            for (int j = 0; j < m; j++) {
                int tmp = f[j + 1];
                f[j + 1] = Math.max(Math.max(pre, 0) + x * nums2[j], Math.max(f[j + 1], f[j]));
                pre = tmp;
            }
        }
        return f[m];
    }
}

###cpp

class Solution {
public:
    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int m = nums2.size();
        vector<int> f(m + 1, INT_MIN);
        for (int x : nums1) {
            int pre = f[0];
            for (int j = 0; j < m; j++) {
                int tmp = f[j + 1];
                f[j + 1] = max(max(pre, 0) + x * nums2[j], max(f[j + 1], f[j]));
                pre = tmp;
            }
        }
        return f[m];
    }
};

###go

func maxDotProduct(nums1, nums2 []int) int {
m := len(nums2)
f := make([]int, m+1)
for i := range f {
f[i] = math.MinInt
}
for _, x := range nums1 {
pre := f[0]
for j, y := range nums2 {
tmp := f[j+1]
f[j+1] = max(max(pre, 0)+x*y, f[j+1], f[j])
pre = tmp
}
}
return f[m]
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(nm)$,其中 $n$ 是 $\textit{nums}_1$ 的长度,$m$ 是 $\textit{nums}_2$ 的长度。
  • 空间复杂度:$\mathcal{O}(m)$。

专题训练

见下面动态规划题单的「§4.1 最长公共子序列(LCS)」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

记忆化搜索->递推->空间优化

灵神dp题单打卡

思路

定义$dfs(i,j) $,代表了以前$i$和$j$个数中$nums1$和$nums2$最大点积
(即$nums1[0...i]$和$nums2[0...j]$的最大点积)。

  • 如果选当前位置,那么算出当前位置点积为$nums1[i] * nums2[j]$,同时看前面位置的最大点积$dfs(i - 1,j - 1)$是否大于0,如果小于0的话,越加越小,不如不要,跟0取max就可以实现。状态方程如下:
    $$dfs(i,j) = max(dfs(i - 1,j - 1),0) + nums1[i] * nums2[j]$$
  • 如果不选当前位置,也就是跳过一格,状态方程如下:
    $$dfs(i,j) = max(dfs(i - 1,j),dfs(i,j - 1))$$

递推是记忆化搜索1: 1翻译而来,而空间优化则是在二维的基础上,观察值如何转移的优化的。具体可见b站灵神算法精讲中的内容,有总结。

Code

###C++

class Solution {
public:
    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size(),m = nums2.size();
        vector<vector<int>> memo(n,vector<int>(m,INT_MIN));
        function<int(int,int)> dfs = [&](int i,int j) -> int{
            if(i < 0 || j < 0) return INT_MIN;
            if(memo[i][j] != INT_MIN) return memo[i][j];
            //选
            memo[i][j] = max(dfs(i - 1,j - 1),0) + nums1[i] * nums2[j];
            memo[i][j] = max({memo[i][j],dfs(i - 1,j),dfs(i,j - 1)});
            return memo[i][j];
        };
        return dfs(n - 1,m - 1);
    }
};

###cpp

class Solution {
public:
    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size(),m = nums2.size();
        vector<vector<int>> memo(n,vector<int>(m,INT_MIN));
        //新一点的递归写法
        auto dfs = [&](this auto&& self,int i,int j) -> int{
            if(i < 0 || j < 0) return INT_MIN;
            if(memo[i][j] != INT_MIN) return memo[i][j];
            //选
            memo[i][j] = max(self(i - 1,j - 1),0) + nums1[i] * nums2[j];
            memo[i][j] = max({memo[i][j],self(i - 1,j),self(i,j - 1)});
            return memo[i][j];
        };
        return dfs(n - 1,m - 1);
    }
};

###c++

    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size(),m = nums2.size();
        vector<vector<int>> f(n + 1,vector<int>(m + 1,INT_MIN));
        for(int i = 0;i < n;i++){
            for(int j = 0;j < m;j++){
                f[i + 1][j + 1] = max({max(f[i][j],0) + nums1[i] * nums2[j],
                                    f[i + 1][j],f[i][j + 1]});
            }
        }
        return f[n][m];
    }

###c++

    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size(),m = nums2.size();
        vector<int> f(m + 1,INT_MIN);
        for(int i = 0;i < n;i++){
            f[0] = INT_MIN;//这里相当于初始化第一列。
            int pre = INT_MIN;
            for(int j = 0;j < m;j++){
                int t = f[j + 1];
                f[j + 1] = max({max(pre,0) + nums1[i] * nums2[j],
                                    f[j],f[j + 1]});
                pre = t;
            }
        }
        return f[m];
    }

c++ 动态规划 易懂

题意

本题就是求两个子序列点积的最大值。题意很明确,直接说解法。
很显然这题用dp做,但是状态转移方程怎么写,dp[i][j]代表什么意思,依然是一个值得写一下的问题。

首先我们考虑dp[i][j]代表什么意思。

dp[i][j]的含义

第一种想法:
dp[i][j]的含义是以nums1[i]和nums2[j]结尾的子序列的最大点积。
第二种想法:
dp[i][j]的含义是到nums1[i]和nums2[j]为止的子序列的最大点积。

这两种是不一样的:
第一种想法一定要包含nums1[i]和nums2[j],因为以它们结尾。
但是第二种想法就没有这个限制,以谁结尾无所谓,最主要是大。

我们应该使用第二种,具体原因是因为状态转移方程。

状态转移方程

第一种想法的状态转移方程怎么写呢?

dp[i][j]=max(nums1[i]*nums2[j] , nums1[i]*nums2[j]+ maxVal);  

首先我们知道nums1[i]*nums2[j]这个值在第一种想法中是一定要有的。
接下来我们可以选择只有这两项或者包含前面的子序列点积最大值:
假如只有这两项,那么就什么都不加;假如也包含前面的就加上前面子序列点积的最大值maxVal。

来算一下时间复杂度:
首先算n^2个dp值
在每次dp计算中都要找到前面子序列点积的最大值,又要花费n^2的时间
所以时间复杂度为n^4,(500)^4是超时的

第二种想法的状态转移方程怎么写呢?
第二种可以选择nums1[i]和nums2[j],所以我们可以通过这个来写状态转移方程:
(其实对于子序列的很多dp题来讲,都可以使用选不选来写状态转移方程)

1.选择nums1[i]和nums2[j]

1.1不选择前面的 dp[i][j]=nums1[i]*nums2[j]
1.2也选择前面的 dp[i][j]=max(dp[i][j],nums1[i]*nums2[j]+dp[i-1][j-1])
因为dp[i][j]是截止到nums1[i]和nums2[j]中的最大点积,所以只需要dp[i-1][j-1]就可以了  
事实上从这里可以看出想法一就是想法二的情况之一

2.选择nums1[i],不选择nums2[j]

等价于dp[i][j-1]
dp[i][j]=max(dp[i][j],dp[i][j-1])

3.不选择nums1[i],选择nums2[j]

等价于dp[i-1][j]
dp[i][j]=max(dp[i][j],dp[i-1][j])

4.???

聪明的你肯定知道了
状态方程你来写吧:dp[i][j]=max(dp[i][j],???)

代码

###cpp


class Solution {
public:
    int maxDotProduct(vector<int>& nums1, vector<int>& nums2) {
        int sz1=nums1.size(),sz2=nums2.size();
        vector<vector<int>> dp(sz1+1,vector<int>(sz2+1,-1e8));

        for(int i=1;i<=sz1;i++){
            for(int j=1;j<=sz2;j++){
                //1.1
                dp[i][j]=nums1[i-1]*nums2[j-1];
                //1.2
                dp[i][j]=max(dp[i][j],nums1[i-1]*nums2[j-1]+dp[i-1][j-1]);
                //2
                dp[i][j]=max(dp[i][j],dp[i][j-1]);
                //3
                dp[i][j]=max(dp[i][j],dp[i-1][j]);
                //4
                dp[i][j]=max(dp[i][j],dp[i-1][j-1]);
            }
        }
        return dp[sz1][sz2];
    }
};

哦,对,求个赞

有疑问评论区可以交流,看到一定回

每日一题-分裂二叉树的最大乘积🟡

给你一棵二叉树,它的根为 root 。请你删除 1 条边,使二叉树分裂成两棵子树,且它们子树和的乘积尽可能大。

由于答案可能会很大,请你将结果对 10^9 + 7 取模后再返回。

 

示例 1:

输入:root = [1,2,3,4,5,6]
输出:110
解释:删除红色的边,得到 2 棵子树,和分别为 11 和 10 。它们的乘积是 110 (11*10)

示例 2:

输入:root = [1,null,2,3,4,null,null,5,6]
输出:90
解释:移除红色的边,得到 2 棵子树,和分别是 15 和 6 。它们的乘积为 90 (15*6)

示例 3:

输入:root = [2,3,9,10,7,8,6,5,4,11,1]
输出:1025

示例 4:

输入:root = [1,1]
输出:1

 

提示:

  • 每棵树最多有 50000 个节点,且至少有 2 个节点。
  • 每个节点的值在 [1, 10000] 之间。

两次 DFS,或者提前保存子树和(Python/Java/C++/C/Go/JS/Rust)

第一次 DFS:计算整棵树的点权和 $\textit{total}$。

第二次 DFS:计算子树点权和 $s$,那么删除当前节点到其父节点的边后,另一部分的和就是 $\textit{total} - s$,二者乘积为

$$
s\cdot(\textit{total} - s)
$$

用其更新答案的最大值。

由于本题保证点权是非负,我们无需判断当前节点是根节点的特殊情况(无父节点),此时上式为 $0$,不影响答案。

如果有负数点权,就需要跳过当前节点是根节点的情况。

也可以在第一次 DFS 时,把子树和保存到一个列表中,第二次只需遍历这个列表,见 Python3 的第一份代码。

###py

class Solution:
    def maxProduct(self, root: Optional[TreeNode]) -> int:
        def dfs(node: Optional[TreeNode]) -> int:
            if node is None:
                return 0
            s = node.val + dfs(node.left) + dfs(node.right)
            sub_sum.append(s)
            return s

        sub_sum = []
        total = dfs(root)

        ans = max(s * (total - s) for s in sub_sum)
        return ans % 1_000_000_007

###py

class Solution:
    def maxProduct(self, root: Optional[TreeNode]) -> int:
        def dfs1(node: Optional[TreeNode]) -> int:
            if node is None:
                return 0
            return node.val + dfs1(node.left) + dfs1(node.right)

        def dfs2(node: Optional[TreeNode]) -> int:
            if node is None:
                return 0
            s = node.val + dfs2(node.left) + dfs2(node.right)
            nonlocal ans
            ans = max(ans, s * (total - s))
            return s

        total = dfs1(root)

        ans = 0
        dfs2(root)

        return ans % 1_000_000_007

###java

class Solution {
    private static final int MOD = 1_000_000_007;
    private long ans = 0;

    public int maxProduct(TreeNode root) {
        int total = dfs1(root);
        dfs2(root, total);
        return (int) (ans % MOD);
    }

    private int dfs1(TreeNode node) {
        if (node == null) {
            return 0;
        }
        return node.val + dfs1(node.left) + dfs1(node.right);
    }

    private int dfs2(TreeNode node, int total) {
        if (node == null) {
            return 0;
        }
        int s = node.val + dfs2(node.left, total) + dfs2(node.right, total);
        ans = Math.max(ans, (long) s * (total - s));
        return s;
    }
}

###cpp

class Solution {
public:
    int maxProduct(TreeNode* root) {
        auto dfs1 = [&](this auto&& dfs1, TreeNode* node) -> int {
            if (node == nullptr) {
                return 0;
            }
            return node->val + dfs1(node->left) + dfs1(node->right);
        };
        long long total = dfs1(root);

        long long ans = 0;
        auto dfs2 = [&](this auto&& dfs2, TreeNode* node) -> int {
            if (node == nullptr) {
                return 0;
            }
            int s = node->val + dfs2(node->left) + dfs2(node->right);
            ans = max(ans, s * (total - s));
            return s;
        };
        dfs2(root);

        return ans % 1'000'000'007;
    }
};

###c

#define MAX(a, b) ((b) > (a) ? (b) : (a))

int maxProduct(struct TreeNode* root) {
    int dfs1(struct TreeNode* node) {
        if (node == NULL) {
            return 0;
        }
        return node->val + dfs1(node->left) + dfs1(node->right);
    }

    long long total = dfs1(root);
    long long ans = 0;

    int dfs2(struct TreeNode* node) {
        if (node == NULL) {
            return 0;
        }
        int s = node->val + dfs2(node->left) + dfs2(node->right);
        ans = MAX(ans, s * (total - s));
        return s;
    }

    dfs2(root);
    return ans % 1000000007;
}

###go

func maxProduct(root *TreeNode) (ans int) {
var dfs1 func(*TreeNode) int
dfs1 = func(node *TreeNode) int {
if node == nil {
return 0
}
return node.Val + dfs1(node.Left) + dfs1(node.Right)
}
total := dfs1(root)

var dfs2 func(*TreeNode) int
dfs2 = func(node *TreeNode) int {
if node == nil {
return 0
}
s := node.Val + dfs2(node.Left) + dfs2(node.Right)
ans = max(ans, s*(total-s))
return s
}
dfs2(root)

return ans % 1_000_000_007
}

###js

var maxProduct = function(root) {
    function dfs1(node) {
        if (node === null) {
            return 0;
        }
        return node.val + dfs1(node.left) + dfs1(node.right);
    }

    function dfs2(node) {
        if (node === null) {
            return 0;
        }
        const s = node.val + dfs2(node.left) + dfs2(node.right);
        ans = Math.max(ans, s * (total - s));
        return s;
    }

    const total = dfs1(root);

    let ans = 0;
    dfs2(root);

    return ans % 1_000_000_007;
};

###rust

use std::rc::Rc;
use std::cell::RefCell;

impl Solution {
    pub fn max_product(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
        fn dfs1(node: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
            if let Some(n) = node {
                let n = n.borrow();
                n.val + dfs1(&n.left) + dfs1(&n.right)
            } else {
                0
            }
        }

        fn dfs2(node: &Option<Rc<RefCell<TreeNode>>>, total: i32, ans: &mut i64) -> i32 {
            if let Some(n) = node {
                let n = n.borrow();
                let s = n.val + dfs2(&n.left, total, ans) + dfs2(&n.right, total, ans);
                *ans = (*ans).max(s as i64 * (total - s) as i64);
                s
            } else {
                0
            }
        }

        let total = dfs1(&root);

        let mut ans = 0;
        dfs2(&root, total, &mut ans);

        (ans % 1_000_000_007) as _
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是二叉树的节点个数。
  • 空间复杂度:$\mathcal{O}(h)$,其中 $h$ 是二叉树的高度。递归需要消耗 $\mathcal{O}(h)$ 的栈空间。

专题训练

见下面树题单的「§2.3 自底向上 DFS」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

JAVA 简单易懂的代码

分为两个步骤:
1.求整个二叉树的和
2.遍历所有子树和,求最大值

    double ans = Double.MIN_VALUE;
    double allSum;
    double nodeSum;
    public int maxProduct(TreeNode root) {
        allSum = sum(root);
        dfs(root);
        return (int)(ans % (int)(1e9 + 7));
    }

    public double sum(TreeNode node){
        if(node == null) return 0;
        return node.val + sum(node.left) + sum(node.right);
    }

    public double dfs(TreeNode node){
        if(node == null)    return 0 ;
        nodeSum = node.val + dfs(node.left) + dfs(node.right);
        ans = Math.max(ans, (allSum - nodeSum) * nodeSum);
        return nodeSum;
    }

C++ 后序遍历

解题思路

  1. 后序遍历得到分别以各个节点为根的子树总和
  2. 去掉一条边的乘积 = 子树总和 * (总和 - 子树总和),取最大值
  3. 不能提前取模。比如 1e9 + 7(取模后为0) 和 1e9 + 6(取模后不变),提前取模会导致错误

代码

###cpp

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<long> sums;
    static const int mod = 1e9 + 7;
    int maxProduct(TreeNode* root) {
        postOrder(root);
        long res = -1;
        for (int i = 0; i < sums.size() - 1; ++i) {
            // 取最大值时不能取模,应该用long型存结果
            res = max(res, sums[i] * (sums.back() - sums[i]));
        }
        return (int)(res % mod);
    }

    long postOrder(TreeNode* root) {
        if (root == nullptr) return 0;
        long res = root->val + postOrder(root->left) + postOrder(root->right);
        sums.push_back(res);
        return res;
    }
};

每日一题-最大层内元素和🟡

给你一个二叉树的根节点 root。设根节点位于二叉树的第 1 层,而根节点的子节点位于第 2 层,依此类推。

返回总和 最大 的那一层的层号 x。如果有多层的总和一样大,返回其中 最小 的层号 x

 

示例 1:

输入:root = [1,7,0,7,-8,null,null]
输出:2
解释:
第 1 层各元素之和为 1,
第 2 层各元素之和为 7 + 0 = 7,
第 3 层各元素之和为 7 + -8 = -1,
所以我们返回第 2 层的层号,它的层内元素之和最大。

示例 2:

输入:root = [989,null,10250,98693,-89388,null,null,null,-32127]
输出:2

 

提示:

  • 树中的节点数在 [1, 104]范围内
  • -105 <= Node.val <= 105

两种方法:BFS / DFS(Python/Java/C++/Go)

方法一:BFS

本题需要计算每一层的节点值之和,适合用 BFS原理讲解【基础算法精讲 13】

维护两个信息:最大层和 $\textit{maxSum}$,以及对应的层号 $\textit{ans}$。如果当前层的和大于 $\textit{maxSum}$,那么更新 $\textit{maxSum}$ 和 $\textit{ans}$。

class Solution:
    def maxLevelSum(self, root: Optional[TreeNode]) -> int:
        max_sum = -inf
        ans = 0

        q = [root]
        level = 1
        while q:
            tmp = q
            q = []
            s = 0
            for node in tmp:
                s += node.val
                if node.left:
                    q.append(node.left)
                if node.right:
                    q.append(node.right)

            if s > max_sum:
                max_sum = s
                ans = level

            level += 1

        return ans
class Solution {
    public int maxLevelSum(TreeNode root) {
        int max_sum = Integer.MIN_VALUE;
        int ans = 0;

        List<TreeNode> q = new ArrayList<>();
        q.add(root);

        for (int level = 1; !q.isEmpty(); level++) {
            List<TreeNode> tmp = q;
            q = new ArrayList<>();
            int s = 0;

            for (TreeNode node : tmp) {
                s += node.val;
                if (node.left != null) {
                    q.add(node.left);
                }
                if (node.right != null) {
                    q.add(node.right);
                }
            }

            if (s > max_sum) {
                max_sum = s;
                ans = level;
            }
        }

        return ans;
    }
}
class Solution {
public:
    int maxLevelSum(TreeNode* root) {
        int max_sum = INT_MIN;
        int ans = 0;

        vector<TreeNode*> q = {root};
        for (int level = 1; !q.empty(); level++) {
            auto tmp = q;
            q.clear();
            int s = 0;

            for (auto node : tmp) {
                s += node->val;
                if (node->left) {
                    q.push_back(node->left);
                }
                if (node->right) {
                    q.push_back(node->right);
                }
            }

            if (s > max_sum) {
                max_sum = s;
                ans = level;
            }
        }

        return ans;
    }
};
func maxLevelSum(root *TreeNode) (ans int) {
maxSum := math.MinInt
q := []*TreeNode{root}

for level := 1; q != nil; level++ {
tmp := q
q = nil
s := 0

for _, node := range tmp {
s += node.Val
if node.Left != nil {
q = append(q, node.Left)
}
if node.Right != nil {
q = append(q, node.Right)
}
}

if s > maxSum {
maxSum = s
ans = level
}
}

return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是二叉树的节点个数。
  • 空间复杂度:$\mathcal{O}(n)$。

方法二:DFS

创建一个列表 $\textit{rowSum}$,维护每一层的元素和。

在 DFS 的同时,把节点值加到这一层的元素和中。

最后求出最大层和对应的层号。

class Solution:
    def maxLevelSum(self, root: Optional[TreeNode]) -> int:
        def dfs(node: Optional[TreeNode], level: int) -> None:
            if node is None:
                return

            if len(row_sum) == level:  # 首次访问 level 层
                row_sum.append(node.val)  # 节点值作为层和的初始值
            else:
                row_sum[level] += node.val

            dfs(node.left, level + 1)
            dfs(node.right, level + 1)

        row_sum = []
        dfs(root, 0)
        return row_sum.index(max(row_sum)) + 1  # 层号从 1 开始
class Solution {

    private void dfs(TreeNode node, int level, List<Integer> rowSum) {
        if (node == null) {
            return;
        }

        if (rowSum.size() == level) { // 首次访问 level 层
            rowSum.add(node.val); // 节点值作为层和的初始值
        } else {
            rowSum.set(level, rowSum.get(level) + node.val);
        }

        dfs(node.left, level + 1, rowSum);
        dfs(node.right, level + 1, rowSum);
    }

    public int maxLevelSum(TreeNode root) {
        List<Integer> rowSum = new ArrayList<>();
        dfs(root, 0, rowSum);

        int ans = 0;
        for (int i = 1; i < rowSum.size(); i++) {
            if (rowSum.get(i) > rowSum.get(ans)) {
                ans = i;
            }
        }
        return ans + 1; // 层号从 1 开始
    }
}
class Solution {
public:
    int maxLevelSum(TreeNode* root) {
        vector<int> row_sum;

        auto dfs = [&](this auto&& dfs, TreeNode* node, int level) -> void {
            if (node == nullptr) {
                return;
            }

            if (row_sum.size() == level) { // 首次访问 level 层
                row_sum.push_back(node->val); // 节点值作为层和的初始值
            } else {
                row_sum[level] += node->val;
            }

            dfs(node->left, level + 1);
            dfs(node->right, level + 1);
        };

        dfs(root, 0);
        return ranges::max_element(row_sum) - row_sum.begin() + 1; // 层号从 1 开始
    }
};
func maxLevelSum(root *TreeNode) (ans int) {
rowSum := []int{}

var dfs func(*TreeNode, int)
dfs = func(node *TreeNode, level int) {
if node == nil {
return
}

if len(rowSum) == level { // 首次访问 level 层
rowSum = append(rowSum, node.Val) // 节点值作为层和的初始值
} else {
rowSum[level] += node.Val
}

dfs(node.Left, level+1)
dfs(node.Right, level+1)
}

dfs(root, 0)

for i, s := range rowSum {
if s > rowSum[ans] {
ans = i
}
}
return ans + 1 // 层号从 1 开始
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是二叉树的节点个数。
  • 空间复杂度:$\mathcal{O}(n)$。

专题训练

见下面树题单的「§2.13 二叉树 BFS」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

❌