不相交的线

标签: 数组 动态规划

难度: Medium

在两条独立的水平线上按给定的顺序写下 nums1nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:

  •  nums1[i] == nums2[j]
  • 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

示例 1:

输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。 
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。

示例 2:

输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3

示例 3:

输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2

提示:

  • 1 <= nums1.length, nums2.length <= 500
  • 1 <= nums1[i], nums2[j] <= 2000

Submission

运行时间: 35 ms

内存: 16.2 MB

# class Solution:
#     def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
#         nums2 = nums2[::-1]
#         @cache
#         def find(n, m):
#             if not n or m == len(nums2): return 0
#             try: return max(find(n - 1, nums2[m:].index(nums1[n - 1]) + m + 1) + 1, find(n - 1, m))
#             except: return find(n - 1, m)
#         return find(len(nums1), 0)

class Solution:
    def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
        nums1, nums2 = (nums2, nums1) if len(nums2) > len(nums1) else (nums1, nums2)

        dic = collections.defaultdict(list)
        for i in range(len(nums1) - 1, -1, -1):#同值坐标降序放置,排除对最长递增公共序列的干扰
            dic[nums1[i]].append(i)
        #print(dic)
        tmp = []
        for n in nums2:#取公共值
            tmp += dic[n]
        #print(tmp)
        dp = []
        for x in tmp:#求最长递增公共序列,因为题意要求的连线序号肯定是单调递增的
            i = bisect_left(dp, x)
            if i == len(dp): dp.append(x)
            else: dp[i] = x
        
        return len(dp)

Explain

本题解的思路是将问题转化为寻找两个数组之间的最长递增子序列(Longest Increasing Subsequence, LIS)的问题。首先,为了方便处理,确保nums1是两个数组中较短的一个。然后,使用一个字典dic来存储nums1中每个数字的所有出现位置,并按照逆序存储,以便后续操作。接下来,遍历nums2数组,将nums2中的元素按照在nums1中出现的位置顺序记录到一个列表tmp中。这一步是为了找到所有在nums1和nums2中都出现的元素,并保留其在nums1中的位置。最后,通过计算tmp列表的最长递增子序列的长度来确定最大的不相交的线的数量,这是因为这些位置的递增序列对应于不相交的连接线。

时间复杂度: O(m log m)

空间复杂度: O(n + m)

#class Solution:
#    def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
#        nums2 = nums2[::-1]
#        @cache
#        def find(n, m):
#            if not n or m == len(nums2): return 0
#            try: return max(find(n - 1, nums2[m:].index(nums1[n - 1]) + m + 1) + 1, find(n - 1, m))
#            except: return find(n - 1, m)
#        return find(len(nums1), 0)

class Solution:
    def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
        # 选择较短的数组作为nums1以减少空间使用
        nums1, nums2 = (nums2, nums1) if len(nums2) > len(nums1) else (nums1, nums2)

        # 创建字典存储nums1中元素的索引,逆序存储以便后续操作
        dic = collections.defaultdict(list)
        for i in range(len(nums1) - 1, -1, -1):
            dic[nums1[i]].append(i)

        # 遍历nums2,记录nums1中元素的出现位置
        tmp = []
        for n in nums2:
            tmp += dic[n]

        # 使用二分法计算最长递增子序列
        dp = []
        for x in tmp:
            i = bisect_left(dp, x)
            if i == len(dp): dp.append(x)
            else: dp[i] = x
        
        # 返回最长递增子序列的长度,即最大连线数
        return len(dp)

Explore

在该算法实现中,选择较短的数组作为nums1主要是为了优化空间复杂度和提高算法效率。具体来说,由于需要建立一个字典来存储nums1中每个元素的出现位置,如果nums1较短,则字典的大小也会较小,从而减少内存使用。此外,遍历nums2并查找其元素在nums1的位置时,较短的nums1意味着在字典中查找操作的平均复杂度较低,从而可以加快整个算法的执行速度。

逆序存储nums1中元素的出现位置的主要优势在于便于处理重复元素并保持他们的相对顺序。当nums2中的元素在nums1中多次出现时,逆序存储可以确保我们总是能够尽可能地使用最靠后的元素,从而在构造tmp数组时保持最长递增子序列的潜在长度。这样做有助于确保找到的递增子序列是有效的,不会因为重复利用前面的元素而导致错误。

算法中利用二分查找来构建最长递增子序列是通过维护一个动态的列表dp,该列表存储当前找到的最长递增子序列的元素。对于tmp中的每个元素x,使用二分查找确定它在dp中的位置i。如果x可以添加到dp的末尾(即i等于dp的长度),则直接添加x;如果i小于dp的长度,则替换dp中的第i个元素为x。这种方法有效地保持了dp的长度为当前的最长递增子序列的长度,并确保dp始终尽可能小,从而提高查找和更新的效率。

在给定的算法实现中没有显式处理nums1或nums2为空的情况。如果任一数组为空,则最长递增子序列的长度自然应为0,因为没有可用的元素来形成连线。为了处理这种情况,应在算法开始时添加一个检查,如果发现任一数组为空,则直接返回0。这样可以有效避免后续的无意义计算,并保证算法的鲁棒性。