力扣338题解

165 阅读4分钟

两个回文子序列长度的最大乘积

题目描述

给你一个字符串 s ,请你找到 s 中两个 不相交回文子序列 ,使得它们长度的 乘积最大 。两个子序列在原字符串中如果没有任何相同下标的字符,则它们是 不相交 的。

请你返回两个回文子序列长度可以达到的 最大乘积 。

子序列 指的是从原字符串中删除若干个字符(可以一个也不删除)后,剩余字符不改变顺序而得到的结果。如果一个字符串从前往后读和从后往前读一模一样,那么这个字符串是一个 回文字符串 。

示例

示例 1:

输入:s = "leetcodecom"
输出:9
解释:最优方案是选择 "ete" 作为第一个子序列,"cdc" 作为第二个子序列。
它们的乘积为 3 * 3 = 9

示例 2:

输入:s = "bb"
输出:1
解释:最优方案为选择 "b" (第一个字符)作为第一个子序列,"b" (第二个字符)作为第二个子序列。
它们的乘积为 1 * 1 = 1

示例 3:

输入:s = "accbcaxxcxx"
输出:25
解释:最优方案为选择 "accca" 作为第一个子序列,"xxcxx" 作为第二个子序列。
它们的乘积为 5 * 5 = 25

提示

  • 2 <= s.length <= 12
  • s 只含有小写英文字母。

题解思路

这道题可以用动态规划和状态压缩的方法来解决。我们可以用一个整数来表示 s 的一个子集,其中每一位对应 s 的一个字符,如果该位为 1,则表示选择该字符,否则表示不选择该字符。例如,如果 s = "abc",那么整数 5 对应的子集就是 "ac",因为 5 的二进制表示是 101

我们可以预处理出所有可能的子集是否是回文串,以及每个子集的长度。这可以用动态规划来实现,设 f[i][j] 表示 s 的第 i 到第 j 个字符组成的子串是否是回文串,那么有如下状态转移方程:

  • 如果 i == j,则 f[i][j] = true,因为单个字符是回文串;
  • 如果 i + 1 == j,则 f[i][j] = (s[i] == s[j]),因为相邻两个字符相同才是回文串;
  • 如果 i + 1 < j,则 f[i][j] = (s[i] == s[j]) && f[i + 1][j - 1],因为首尾两个字符相同且中间部分也是回文串才是回文串。

预处理完成后,我们可以枚举所有可能的两个子集(用两个整数表示),判断它们是否不相交(即按位与运算结果为零),并且都是回文串(即预处理数组中对应位置为真)。如果满足条件,就更新答案为两个子集长度的乘积。最后返回答案即可。

题解代码

class Solution:
    def maxProduct(self, s: str) -> int:
        # 预处理所有子集是否是回文串,以及每个子集的长度
        n = len(s)
        f = [[False] * n for _ in range(n)]
        g = [0] * (1 << n)
        for i in range(n):
            f[i][i] = True
            g[1 << i] = 1
        for i in range(n - 1):
            f[i][i + 1] = (s[i] == s[i + 1])
            if f[i][i + 1]:
                g[(1 << i) | (1 << (i + 1))] = 2
        for l in range(3, n + 1):
            for i in range(n - l + 1):
                j = i + l - 1
                f[i][j] = (s[i] == s[j]) and f[i + 1][j - 1]
                if f[i][j]:
                    g[(1 << i) | (1 << j) | g[(1 << (i + 1)) | (1 << (j - 1))]] = l

        # 枚举所有可能的两个子集,更新答案
        ans = 0
        for x in range(1, 1 << n):
            y = ((1 << n) - 1) ^ x # y 是 x 的补集
            # 枚举 y 的所有子集,判断是否和 x 不相交且都是回文串
            z = y
            while z > 0:
                if (x & z) == 0 and g[x] > 0 and g[z] > 0:
                    ans = max(ans, g[x] * g[z])
                z = (z - 1) & y # 求下一个子集
        return ans

找出数组中的第 K 大异或值对

题目描述

给你一个整数数组 nums 和一个整数 k ,返回数组中最大的 k 个异或值对。

数组中的每个元素都是在范围 [1, 2^31 - 1] 内的正整数。

示例

示例 1:

输入:nums = [5,2,4,6,6,3], k = 1
输出:7
解释:数组中唯一异或值对是 [5,2] ,异或结果为 5 XOR 2 = 7

示例 2:

输入:nums = [5,2,4,6,6,3], k = 2
输出:8
解释:数组中最大的两个异或值对是 [5,6][5,6] ,异或结果为 5 XOR 6 = 3

示例 3:

输入:nums = [5,2,4,6,6,3], k = 3
输出:8
解释:数组中最大的三个异或值对是 [5,6][5,6][2,3] ,异或结果分别为 3、3 和 1 。

提示

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] < 2^31
  • 1 <= k <= nums.length * (nums.length - 1) / 2

题解思路

这道题可以用二分查找和前缀异或的方法来解决。我们可以先预处理出数组 nums 的所有前缀异或值,即 pre[i] = nums[0] ^ ... ^ nums[i - 1],其中 pre[0] = 0。那么任意一个子数组的异或值就可以用两个前缀异或值相异或得到,即 nums[i] ^ ... ^ nums[j] = pre[i] ^ pre[j + 1]

我们可以枚举所有可能的子数组,得到所有可能的异或值,并排序。然后用二分查找找到第 k 大的异或值,记为 ans。这样我们就知道了答案的上界。

接下来我们需要验证 ans 是否是正确的答案。我们可以用一个贪心的方法来统计数组中有多少个子数组的异或值大于等于 ans。具体地,我们可以用一个变量 cur 表示当前子数组的异或值,从左到右遍历数组,每次更新 cur 为 cur 和当前元素的异或值。如果 cur 大于等于 ans,那么就说明找到了一个合法的子数组,我们将其计入答案,并将 cur 置零,重新开始寻找下一个子数组。如果遍历完整个数组后,答案大于等于 k,那么就说明 ans 是正确的答案;否则就说明 ans 过大,需要减小。

我们可以用二分查找来优化验证过程,即在确定了上界 ans 后,再在 [0, ans] 的范围内二分查找最小的满足条件的答案。最后返回答案即可。

题解代码

class Solution:
    def kthLargestValue(self, nums: List[int], k: int) -> int:
        # 预处理前缀异或值
        n = len(nums)
        pre = [0] * (n + 1)
        for i in range(n):
            pre[i + 1] = pre[i] ^ nums[i]

        # 枚举所有子数组的异或值,并排序
        xor = []
        for i in range(n):
            for j in range(i, n):
                xor.append(pre[i] ^ pre[j + 1])
        xor.sort()

        # 二分查找第 k 大的异或值
        ans = xor[-k]

        # 验证答案是否正确,并优化
        left = 0
        right = ans
        while left < right:
            mid = (left + right) // 2
            # 统计数组中有多少个子数组的异或值大于等于 mid
            count = 0
            cur = 0
            for i in range(n):
                cur ^= nums[i]
                if cur >= mid:
                    count += 1
                    cur = 0
            if count >= k:
                right = mid
            else:
                left = mid + 1

        return left

无向树上的拓扑排序

题目描述

给你一个无向树,这棵树由 n 个节点组成,节点编号从 0 到 n - 1 。树上有 n - 1 条边,每条边都有一个权值。无向树满足以下定义:对于树上的任意两个节点 u 和 v ,存在唯一的一条从 u 到 v 的路径。

给你一个整数数组 nums 和一个二维数组 edges ,其中 edges[i] = [ui, vi, weighti] 表示在节点 ui 和 vi 之间有一条边,权值为 weighti 。同时给你一个整数 k 。

请你找到一条满足下面要求的路径,并返回它的权值:

  • 路径是从树上的某个节点到另一个节点的子路径(可能只包含一个节点)。
  • 路径中边的数量等于 k 且不存在重复边。
  • 路径中边的权值之和最小。

如果树上不存在满足这些要求的路径,请你返回 -1 。

示例

示例 1:

输入:nums = [0,1,2,3], edges = [[0,1,2],[1,2,3],[2,3,4]], k = 2
输出:5
解释:存在一条路径 [0,1,2] ,边的数量等于 k ,权值之和为 2 + 3 = 5

示例 2:

输入:nums = [0,1,2], edges = [[0,1,5],[0,2,3]], k = 2
输出:-1
解释:不存在满足要求的路径

示例 3:

输入:nums = [0,10], edges = [[0,1,123]], k = 1
输出:123
解释:存在一条路径 [0,1] ,边的数量等于 k ,权值之和为 123

提示

  • n == nums.length
  • n - 1 == edges.length
  • 1 <= n <= 10^5
  • nums[i] 是一个二进制字符串,长度为 8
  • ui != vi
  • weighti 是一个整数,范围是 [0, 255]
  • k 是一个整数,范围是 [0, n - 1]

题解思路

这道题可以用动态规划和状态压缩的方法来解决。我们可以用一个整数来表示树上的一个子集,其中每一位对应树上的一个节点,如果该位为 1,则表示选择该节点,否则表示不选择该节点。例如,如果 n = 4,那么整数 9 对应的子集就是 {0, 3},因为 9 的二进制表示是 1001

我们可以预处理出所有可能的子集是否是一条路径,以及每个子集的权值之和。这可以用动态规划来实现,设 f[i][j] 表示以 i 为根节点,子集 j 是否是一条路径,以及该路径的权值之和。那么有如下状态转移方程:

  • 如果 j == (1 << i),则 f[i][j] = (true, 0),因为只有单个节点 i 是一条路径;
  • 如果 j != (1 << i),则枚举 i 的所有子节点 k,并判断 j 是否包含 k。如果包含,则递归计算 f[k][j ^ (1 << i)],即去掉 i 后的子集是否是以 k 为根节点的路径,以及该路径的权值之和。如果是,则 f[i][j] = (true, f[k][j ^ (1 << i)].second + weight(i, k)),其中 weight(i, k) 表示 i 和 k 之间的边的权值;否则,f[i][j] = (false, inf),表示不是一条路径。

预处理完成后,我们可以枚举所有可能的子集,判断它们是否是一条路径,以及它们包含的节点个数是否等于 k + 1。如果满足条件,就更新答案为该子集的权值之和的最小值。最后返回答案即可。

题解代码

class Solution:
    def minCostPath(self, nums: List[str], edges: List[List[int]], k: int) -> int:
        # 预处理所有子集是否是一条路径,以及每个子集的权值之和
        n = len(nums)
        f = [[(False, float('inf'))] * (1 << n) for _ in range(n)]
        g = [[0] * n for _ in range(n)]
        for u, v, w in edges:
            g[u][v] = w
            g[v][u] = w
        def dp(i, j):
            # 如果已经计算过,直接返回
            if f[i][j][0] != False:
                return f[i][j]
            # 如果只有单个节点,返回真和零
            if j == (1 << i):
                return (True, 0)
            # 枚举所有子节点,并递归计算
            for k in range(n):
                if j & (1 << k) and g[i][k] > 0:
                    res = dp(k, j ^ (1 << i))
                    if res[0]:
                        f[i][j] = (True, res[1] + g[i][k])
                        break
            return f[i][j]

        # 枚举所有可能的子集,并更新答案
        ans = float('inf')
        for j in range(1, 1 << n):
            # 统计子集中节点个数是否等于 k + 1
            cnt = 0
            for i in range(n):
                if j & (1 << i):
                    cnt += 1
            if cnt == k + 1:
                # 枚举所有可能的根节点,并判断是否是一条路径
                for i in range(n):
                    if j & (1 << i):
                        res = dp(i, j)
                        if res[0]:
                            ans = min(ans, res[1])
        return ans if ans < float('inf') else -1

最小化目标值与所选元素的差

题目描述

给你一个大小为 m x n 的整数矩阵 mat 和一个整数 target 。

从矩阵的 每一行 中选择一个整数,你的目标是 最小化 所有选中元素之 和 与目标值 target 的 绝对差 。

返回 最小的绝对差 。

a 和 b 两数字的 绝对差 是 a - b 的绝对值。

示例

示例 1:

输入:mat = [[1,2,3],[4,5,6],[7,8,9]], target = 13
输出:0
解释:一种可能的最优选择方案是:
- 第一行选出 1
- 第二行选出 5
- 第三行选出 7
所选元素的和是 13 ,等于目标值,所以绝对差是 0

示例 2:

输入:mat = [[1],[2],[3]], target = 100
输出:94
解释:唯一一种选择方案是:
- 第一行选出 1
- 第二行选出 2
- 第三行选出 3
所选元素的和是 6 ,与目标值的差是 94 ,所以绝对差是 94

示例 3:

输入:mat = [[1,2,9,8,7]], target = 6
输出:1
解释:最优的选择方案是选出第一行的 7 ,所选元素和为 7 ,与目标值的差为 1 ,所以绝对差是 1

提示

  • m == mat.length
  • n == mat[i].length
  • 1 <= m, n <= 70
  • 1 <= mat[i][j] <= 70
  • 1 <= target <= 800

题解思路

这道题可以用动态规划和二分查找的方法来解决。我们可以用一个数组 dp 来表示从矩阵的前 i 行中选择一个整数,所有可能的和。初始时,dp 只有一个元素,即 0。然后我们遍历矩阵的每一行,对于每个元素 x,我们将 dp 中的每个元素加上 x,并将结果存入一个新的数组 tmp。然后我们将 tmp 中的元素去重并排序,得到新的 dp 数组。这样 dp 数组就表示了从矩阵的前 i + 1 行中选择一个整数,所有可能的和。

遍历完矩阵后,我们可以用二分查找在 dp 数组中找到最接近 target 的元素,并返回它们之间的绝对差作为答案。

题解代码

class Solution:
    def minimizeTheDifference(self, mat: List[List[int]], target: int) -> int:
        # 初始化 dp 数组
        dp = [0]
        # 遍历矩阵的每一行
        for row in mat:
            # 创建一个新的数组 tmp 存储所有可能的和
            tmp = []
            for x in row:
                for y in dp:
                    tmp.append(x + y)
            # 去重并排序
            tmp = sorted(list(set(tmp)))
            # 更新 dp 数组
            dp = tmp

        # 在 dp 数组中二分查找最接近 target 的元素,并返回绝对差
        ans = float('inf')
        left = 0
        right = len(dp) - 1
        while left <= right:
            mid = (left + right) // 2
            ans = min(ans, abs(dp[mid] - target))
            if dp[mid] == target:
                break
            elif dp[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        return ans