SpiritiualWander

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

LeetCode 每日一题

2415. 反转二叉树的奇数层

问题

给你一棵 完美 二叉树的根节点 root ,请你反转这棵树中每个 奇数 层的节点值。

例如,假设第 3 层的节点值是 [2,1,3,4,7,11,29,18] ,那么反转后它应该变成 [18,29,11,7,4,3,1,2]
反转后,返回树的根节点。

完美 二叉树需满足:二叉树的所有父节点都有两个子节点,且所有叶子节点都在同一层。

节点的 层数 等于该节点到根节点之间的边数。

解答

该题比较简单,使用广度优先搜索即可

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def reverseOddLevels(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        depth = 0
        q = [root]
        while q:
            if depth % 2 == 1:
                c = 1 << depth
                for i in range(c >> 1):
                    q[i].val, q[c - i - 1].val = q[c - i - 1].val, q[i].val
            tmp = q
            q = []
            for node in tmp:
                if node.left == None:
                    return root
                q.append(node.left)
                q.append(node.right)
            depth += 1

当然也可以使用 深度优先搜索

众所周知,所谓 树 和 图 中的 递归 都不过是对 前序遍历,中序遍历,后序遍历的推广

不过本体其实哪种遍历都可,只是要将整棵树分成两半,分别同时进行遍历,所以在递归时,他们永远在同一层,为了对称,左右遍历的时候需要反向,即 dfs(node1.left, node2.right)dfs(node1.right, node2.left)

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def reverseOddLevels(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        def dfs(node1, node2, is_odd):
			if node1 == None:
				return
			if is_odd:
				node1.val, node2.val = node2.val, node1.val
			dfs(node1.left, node2.right, not is_odd)
			dfs(node1.right, node2.left, not is_odd)
		dfs(root.left, root.right, True)
		return root

依据灵神的网站题目顺序

2789. 合并后数组中的最大元素

问题

给你一个下标从 0 开始、由正整数组成的数组 nums

你可以在数组上执行下述操作 任意 次:

选中一个同时满足 0 <= i < nums.length - 1nums[i] <= nums[i + 1] 的整数 i 。将元素 nums[i + 1] 替换为 nums[i] + nums[i + 1] ,并从数组中删除元素 nums[i]

返回你可以从最终数组中获得的 最大 元素的值。

解答

这其实很贴近 状态转移:向一个状态输入一个操作,对数据产生影响,得到下一个状态。所以看成是一个 自动机。

而 自动机 的主要组成便是:状态集、状态转移函数、操作、初始状态集、终止状态集。我们可以列出这些参数,以便从中得到破局之法

显然对于 nums[i] 的操作只有两种:不操作、与 nums[i + 1] 相加

初始状态已被题目给定

终止状态即我们的函数结束后会得到一个什么样的数组呢,显然,它是一个递减的数组,否则可以继续比较。

我们便由其终止状态可以得出我们算法:贪心。朴素的思想是从后往前遍历,遇到比自己小的就加上它,然后把该和当作自己的值,继续遍历;若遇到比自己大的,说明从自己往后的子数组已经是递减的了,而往前的子数组在遍历后的最小的值也必然比自己大,说明子结构已经达到最优,此时让自己的值变成前一个比自己大的值即可

贪心的运用需要证明(其实 动态规划 的使用也要证明,只不过我们往往忽略它):

  1. 最优子结构(一个问题的最优解包含其子问题的最优解)的一般性证明:
    1. 证明问题最优解的第一个组成部分是做出一个选择
    2. 对于一个给定问题,在其可能的第一步选择中,你假定已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择。
    3. 给定可获得最优解的选择后,你确定这次选择会产生哪些子问题,以及如何最好地刻画子问题空间。
    4. 利用“剪切—粘贴”(cut-and-paste) 技术证明:作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解。证明这一点是利用反证法:假定子问题的解不是其自身的最优解,那么我们就可以从原问题的解中“剪切”掉这些非最优解,将最优解“粘贴”进去,从而得到原问题一个更优的解,这与最初的解是原问题最优解的前提假设矛盾。如果原问题的最优解包含多个子问题,通常它们都很相似,我们可以将针对一个子问题的“剪切—粘贴”论证方法稍加修改,用于其他子问题。
  2. 贪心选择(将子问题的最优解和贪心选择组合在一起就可以生成整个问题的最优解)的一般性证明:
    通过时间或者空间的局部最优推出全局最优. 也是剪切粘贴法.
    或者是假设某个全局最优解一定包含某个局部最优解
    设某个全局最优解不包含局部最优解, 去掉一个局部解, 将局部最优解加入, 判断前后变化
    找到 集合中元素的比较指标

动态规划需要满足 最优子问题 性质,否则不可用
动态规划可能会遇到 重叠子问题 问题,据此可以使用记忆化搜索进行优化
对于不同问题领域,最优子结构的不同体现在两个方面:

  1. 原问题的最优解中涉及多少个子问题,以及
  2. 在确定最优解使用哪些子问题时,我们需要考察多少种选择。
    贪心便是选择能带来最优的策略,以减少考察次数
    贪心算法在进行第一次选择之前不求解任何子问题。一个动态规划算法是自底向上进行计算的,而一个贪心算法通常是自顶下的,进行一次又一次选择,将给定问题实例变得更小。

对于这个问题来说:

  1. 最优子结构 是 递减的子数组,即上一个状态。
  2. 因为本题就只有一种操作,即遇大则加操作,所以我们贪心选择就是 遇大则加

本题的证明:

  1. 最优子结构:
    假设某一个最优解得到的数组不是递减的数组,那么其中必然存在 i 满足 nums[i] <= nums[i + 1],那么我们和依旧可以执行操作,使其相加替换,并向前遍历,最后得到的最大值可能大于假设中的最优解,那么这与假设矛盾,故而该问题的最优子结构就是 递减的数组
  2. 贪心选择:
    假设某一个最优解的操作策略和贪心的操作策略不同,由于本题就只有一种操作,所以这不可能,所以他们的操作策略相同,所以我们的贪心能够得到最优解

代码:

class Solution:
    def maxArrayValue(self, nums: List[int]) -> int:
        tmp = nums[-1]
        for i in range(len(nums) - 2, -1, -1):
            if nums[i] <= tmp:
                tmp += nums[i]
            else:
                tmp = nums[i]
        return tmp

2790. 长度递增组的最大数目

问题

给你一个下标从 0 开始、长度为 n 的数组 usageLimits

你的任务是使用从 0 到 n - 1 的数字创建若干组,并确保每个数字 i 在 所有组 中使用的次数总共不超过 usageLimits[i] 次。此外,还必须满足以下条件:

  • 每个组必须由 不同 的数字组成,也就是说,单个组内不能存在重复的数字。
  • 每个组(除了第一个)的长度必须 严格大于 前一个组。

在满足所有条件的情况下,以整数形式返回可以创建的最大组数。

解答

构造出一个 1 + 2 + ··· 的结构出来即可
后面少的向前面借

class Solution:
    def maxIncreasingGroups(self, usageLimits: List[int]) -> int:
        usageLimits.sort()
        ans = 0
        tmp = 0
        for i in usageLimits:
            tmp += i
            if tmp >= (ans + 2) * (ans + 1) // 2:
                ans += 1
        return ans

2791. 树中可以形成回文的路径数

题目

给你一棵 树(即,一个连通、无向且无环的图),根 节点为 0 ,由编号从 0 到 n - 1n 个节点组成。这棵树用一个长度为 n 、下标从 0 开始的数组 parent 表示,其中 parent[i] 为节点 i 的父节点,由于节点 0 为根节点,所以 parent[0] == -1

另给你一个长度为 n 的字符串 s ,其中 s[i] 是分配给 iparent[i] 之间的边的字符。s[0] 可以忽略。

找出满足 u < v ,且从 uv 的路径上分配的字符可以 重新排列 形成 回文 的所有节点对 (u, v) ,并返回节点对的数目。

如果一个字符串正着读和反着读都相同,那么这个字符串就是一个 回文 。

解答

如何思索(主要解释笔者当时的想法,引导思考,未必正确!!!!):
首先看题,题目要求的是满足条件的个数,那么一次搜索就两种状态,要么 True,要么 False,那么我们可以先观察一次搜索中 自动机 的情况。自动机的输入是字符树上的节点,它的状态是 字符串,或者说是 节点A 到 节点B 的路径,终止条件是 这个状态的字符串可以组合成 回文,此时便可以考虑能用的 回文特性,以及 树的路径特点:

  1. 可以通过重新排列组合成 回文串 的 字符串 的特性:
    1. 字符在字符串中出现的字数为0,1以及偶数次,且整个字符串中最多只有一个字符能出现一次,若多个字符只出现一次,那么,不论该字符串如何排列,都不会形成回文串,因为它需要轴对称
    2. 由此,我们可以记录 节点A 到 节点B 的路径中,字符出现的字数,来判断是否形成回文串
  2. 节点A 到 节点B 的路径的特点:
    1. 节点A 到 节点B 的路径必然经过他们的 最近公共祖先(LCA)节点,再结合 树上差分/前缀和 的知识,可以知晓 \(PATH_{AB} = PATH_{RA} - PATH_{R LCA_{AB}} + PATH_{RB} - PATH_{R LCA_{AB}}\)。据此,可以先求 根节点 \(R\) 到其他节点的路径中经过的各字符的个数,最后再使用上述公式进行操作,但是这样的内存空间太大,需要进行压缩
    2. 注意到 字符 出现的次数要么是奇数次,要么是偶数次,而且最多一个奇数次。所以我们可以用 0 表示一个字符出现偶数次,用 1 表示其出现奇数次。那么 26 个字符只需 26 bit 即可标识,即 int 类型。只要字符串对应的 26 bit 中有 多个 1 那么就可以放弃。而且据此,可以将上述公式化简为 \(PATH_{AB} = (PATH_{RA} \oplus PATH_{R LCA_{AB}}) \oplus (PATH_{RB} \oplus PATH_{R LCA_{AB}}) = PATH_{RA} \oplus PATH_{RB}\),所以本体化为求 \(PATH_{AB} = 0 , 1 << i, i \in [0, 25]\),异或既可以看作 位加法,又可以看作 位减法
    3. 更多的位运算应用可以查看这篇文章:从集合论到位运算,常见位运算技巧分类总结!
  3. 由上,最后再进行 DFS 即可,得到 n - 1 的包含根节点到节点的路径信息的数组
  4. 于是,可以将该问题转换成 两数之和 问题,即一个数组中,异或结果为 0 的元素有哪些,即使用 哈希表来解决
class Solution:
    def countPalindromePaths(self, parent: List[int], s: str) -> int:
        n = len(s)
        g = [[] for _ in range(n)]
        for i in range(1, n):
            g[parent[i]].append(i)

        # 用来记录 路径信息
        # cnt[x] 表示 遍历过 的路径中和该条路径相同的路径的个数,他们肯定能组成回文串
        cnt = Counter([0]) 
        # 因为是前序遍历,所以 天然满足 u < v
        def dfs(v: int, xor: int) -> int:
            res = 0
            for w in g[v]:
                bit = 1 << (ord(s[w]) - ord('a'))
                # 当前 路径信息
                x = xor ^ bit
                res += cnt[x] + sum(cnt[x ^ (1 << i)] for i in range(26))
                cnt[x] += 1
                res += dfs(w, x)
            return res
        return dfs(0, 0)

扩展

  1. 1457. 二叉树中的伪回文路径——2023.12.18更新
posted on 2023-12-15 22:16  逝玄  阅读(10)  评论(0编辑  收藏  举报