HOT100(九)多维动态规划

一、多维动态规划

1、不同路径

①常规思路

维护一个二维数组f,f[i][j]表示从起点到(i,j)的路径数。

已知初始化f[0][0]=1,即机器人在起点的路径是1;机器人到达第一列f[:][0]只能从上方这一条路走下来,因此第一列的每个元素都为1;机器人到达第一行f[0][:]只能从左方这一条路走下来,因此第一行的每个元素都为1。

状态转移公式:f[i][j]=f[i-1][j]+f[i][j-1],这是因为机器人到达一个点(i,j),只能从上方和左方走过来,因此到达(i,j)的路径之和=到达(i-1,j)的路径总数+到达(i,j-1)的路径总数。

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        f=[[1]*n for _ in range(m)]
        for i in range(1,m):
            for j in range(1,n):
                f[i][j]=f[i-1][j]+f[i][j-1]
        return f[m-1][n-1]

②优化空间复杂度

对于上面的二维状态数组而言,当我们遍历到第 i 行时,f[i][j] 的计算只依赖于当前 j 列(也就是 cur[j],对应的是 f[i-1][j] 的值)和 j-1 列(也就是cur[j-1],对应的是 f[i][j-1] 的值)

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        cur=[1]*n
        for i in range(1,m):
            for j in range(1,n):
                cur[j]+=cur[j-1]
        return cur[n-1]

2、 最小路径和

对于第一行来说,只能从左往右走;对于第一列来说,只能从上往下走。所以初始化grid[i][0]=grid[i-1][0]+grid[i][0],grid[0][j]=grid[0][j-1]+grid[0][j]。

因为只能向下或向右到达某个位置,因此到达一个位置的最小路径和等于到达其上方位置的最小路径和加当前位置值到达其左方位置的最小路径和加当前位置值中的最小值。

class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        if not grid or not grid[0]:
            return 0
        m,n=len(grid),len(grid[0])
        for i in range(1,m):
            grid[i][0]=grid[i-1][0]+grid[i][0]
        for j in range(1,n):
            grid[0][j]=grid[0][j-1]+grid[0][j]
        for i in range(1,m):
            for j in range(1,n):
                grid[i][j]=min(grid[i-1][j]+grid[i][j],grid[i][j-1]+grid[i][j])
        return grid[m-1][n-1]

3、最长回文子串

①动态规划

(1)状态数组:维护一个二维数组dp,dp[i][j]表示子串s[i:j+1]是否为回文串,若dp[i][j]为True说明子串s[i:j+1]是为回文串,否则不是。

(2)状态转移方程:①当子串长度为2时(即j=i+1),若有s[i]=s[j],则此时子串s[i:j+1]是为回文串,dp[i][j]=True;②当子串长度大于2时,若有s[i]=s[j],并且该子串内部子串s[i+1:j-1]也是回文串,则当前子串s[i:j+1]也为回文串(举个例子,abba,a=a,bb又是回文串,所以abba也是回文串)。

(3)步骤:

  • 判断串s是否为空串,若为空则返回空串。
  • 判断串s是否长度为1,若长度唯一,则一定是回文串,返回s即可。
  • 初始化dp数组除了dp[i][i]以外全部为False(这是因为dp[i][i]表明的是字符串中的单个字符,单个字符一定是回文串,因此dp[i][i]=True)。维护两个变量start和max_len,分别记录最长回文子串的起始点和最大长度,初始化start为0,max_len为1(单个字符长度为1)。
  • 遍历每个dp[i][j],判断并更新start和max_len。

时间复杂度和空间复杂度都是O(n²)。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        if not s:
            return ""
        n=len(s)
        if n==1:
            return s
        dp=[[False]*n for _ in range(n)]
        for i in range(n):
            dp[i][i]=True
        start=0;max_len=1
        for j in range(1,n):
            for i in range(j):
                if s[i]==s[j]:
                    if j==i+1:
                        dp[i][j]=True
                    else:
                        dp[i][j]=dp[i+1][j-1]
                if dp[i][j] and j-i+1>max_len:
                    max_len=j-i+1
                    start=i
        return s[start:start+max_len]

②中心扩展

基本思想是,对于每个字符(或字符之间的空隙)作为中心,向两边扩展,找到以该中心为轴对称的回文子串,并记录最长的回文子串。

  • 中心的选择:回文的中心可以是一个字符(回文长度为奇数)或者两个字符之间的空隙(回文长度为偶数)。所以,对于一个长度为 n 的字符串,有 2n-1 个可能的中心(包括字符本身和字符之间的空隙)。

  • 扩展过程:对于每个中心,从中心向两边扩展,检查字符是否相同,直到扩展不再满足回文条件为止。

  • 记录结果:在每次扩展的过程中,记录下当前回文的长度,如果发现更长的回文子串,就更新结果。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        if not s:
            return ""
        n=len(s)
        def expand(s,left,right):
            while left>=0 and right<len(s) and s[left]==s[right]:
                left-=1
                right+=1
            return right-left-1
        start=0;max_len=1
        for i in range(n):
            len1=expand(s,i,i)
            len2=expand(s,i,i+1)
            curr_max=max(len1,len2)
            if curr_max>max_len:
                max_len=curr_max
                start=i-(curr_max-1)//2
        return s[start:start+max_len]

4、最长公共子序列

维护一个状态数组f,f[i][j]表示text1的前i个元素text2的前j个元素最长公共子序列

若text1和text2当前元素相等,那么扩展最长公共子序列,表示为f[i][j]=f[i-1][j-1]+1

若text1和text2当前元素不相等,那么当前最长公共子序列长度可表示为:f[i][j]=max(f[i][j-1],f[i-1][j])。

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        n,m=len(text1),len(text2)
        f=[[0]*(m+1) for _ in range(n+1)]
        for i in range(1,n+1):
            for j in range(1,m+1):
                if text1[i-1]==text2[j-1]:
                    f[i][j]=f[i-1][j-1]+1
                else:
                    f[i][j]=max(f[i][j-1],f[i-1][j])
        return f[n][m]

5、编辑距离

维护一个二维数组ff[i][j] 为将字符串 word1 的前 i 个字符转换为 word2 的前 j 个字符的最小编辑距离。

  • 边界初始化:

    • word1 的长度为 0 时(i=0),要将它转换为 word2 的前 j 个字符,最小操作数就是插入 j 个字符。因此,f[0][j] = j
    • 同理,当 word2 的长度为 0 时(j=0),要将 word1 的前 i 个字符转换为空串,最小操作数就是删除 i 个字符。因此,f[i][0] = i
  • 状态转移方程: 对于任意 ij,可以有以下三种操作来计算 f[i][j]

    • 删除操作:从 word1[0:i] 删除一个字符,即从 f[i-1][j] + 1,表示删除了 word1[i-1]
    • 插入操作:在 word1[0:i] 后插入一个字符,使其匹配 word2[j-1],即从 f[i][j-1] + 1,表示插入了 word2[j-1]
    • 替换操作:如果 word1[i-1] == word2[j-1],则不需要操作,直接继承 f[i-1][j-1] 的值;如果 word1[i-1] != word2[j-1],则需要一次替换操作,即 f[i-1][j-1] + 1
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        n,m=len(word1),len(word2)
        f=[[0]*(m+1) for _ in range(n+1)]
        for i in range(m+1):
            f[0][i]=i
        for j in range(n+1):
            f[j][0]=j
        for i in range(1,n+1):
            for j in range(1,m+1):
                f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1)
                if word1[i-1]==word2[j-1]:
                    f[i][j]=min(f[i][j],f[i-1][j-1])
                else:
                    f[i][j]=min(f[i][j],f[i-1][j-1]+1)
        return f[n][m]

二、技巧

1、只出现一次的数字

利用异或运算的性质,相同数字异或结果为0,0与任何数字异或结果仍为该数字。遍历数组,将所有数字进行异或运算。因为除了一个数字外,其他数字都成对出现,所以它们的异或结果会相互抵消,最后剩下的就是那个只出现一次的数字。

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        single=0
        for num in nums:
            single^=num
        return single

2、多数元素

①利用counter

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        counter=Counter(nums)
        half_len=len(nums)//2
        for num,count in counter.items():
            if count>half_len:
                return num

②利用排序

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        nums.sort()
        return nums[len(nums) // 2]

③Boyer-Moore 投票算法

Boyer-Moore 是通过一种投票机制来找到多数元素。

维护两个变量,一个变量count作为计数器,一个变量candidate作为多数元素候选记录。

因为数组中至多只存在一个多数元素,Boyer Moore主要思想就是利用这一点,遍历数组的过程中遇到与candidate不同的元素,计数器count会减一(不同相消),遇到与candidate相同的元素,计数器count会加一,最后剩下来的一定是多数元素。

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        count=0
        candidate=None
        for num in nums:
            if count==0:
                candidate=num
            count+=(1 if num==candidate else -1)
        return candidate

3、颜色分类

用快排去解决。

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        def quicksort(nums,left,right):
            if left>=right:
                return
            mid=(left+right)//2
            pivot=nums[mid]
            i=left-1;j=right+1
            while i<j:
                while True:
                    i+=1
                    if nums[i]>=pivot:
                        break
                while True:
                    j-=1
                    if nums[j]<=pivot:
                        break
                if i<j:
                    nums[i],nums[j]=nums[j],nums[i]
            quicksort(nums,left,j)
            quicksort(nums,j+1,right)
        quicksort(nums,0,len(nums)-1)
        return nums

4、下一个排列

题意:

下一个排列是按照字典序逐渐变大的,下面是一个例子:

【1,2,3,4,5】的下一个排列是【1,2,3,5,4】,再下一个排列是【1,2,4,3,5】,再下一个是【1,2,4,5,3】,以此类推。

要寻找下一个排列,可以总结为如下步骤:

  • 找转折点:从右向左找到第一个nums[i]<nums[i+1]的元素,这个nums[i]就是转折点。
  • 找交换点:从右向左找到第一个大于nums[i]的元素nums[j],这个元素就是交换点。
  • 交换:交换nums[i]和nums[j]。
  • 将转折点后的序列反转过来,按照升序排列:反转nums[i+1:]变成升序序列。

注:为什么要找到下降点:

因为下降点以后的序列都是降序排列的(比如说【1,2,3,6,5,4】中3是下降点,654是降序排列的,维持当前下降点左边不变12,【1,2,3,6,5,4】已经达到了当前下降点的最大排列。如果要找下一个排列,就需要更换下降点上的元素为比其稍大的值。

class Solution:
    def nextPermutation(self, nums: List[int]) -> None:
        if len(nums)<=1:
            return nums
        i=len(nums)-2
        while i>=0 and nums[i]>=nums[i+1]:
            i-=1
        if i>=0:
            j=len(nums)-1
            while j>=0 and nums[j]<=nums[i]:
                j-=1
            nums[i],nums[j]=nums[j],nums[i]
        left,right=i+1,len(nums)-1
        while left<right:
            nums[left],nums[right]=nums[right],nums[left]
            left+=1
            right-=1

5、寻找重复数

解题思路是采用快慢指针算法,将具有重复数的数组看成一个带环的链表

(1)用快慢指针找到链表中的环起点

首先,先确定环存在,再找环起点。

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head or not head.next:
            return None
        slow=fast=head
        flag=False
        while fast and fast.next:
            slow=slow.next
            fast=fast.next.next
            if slow==fast:
                flag=True
                break
        if not flag:
            return None
        slow=head
        while slow!=fast:
            slow=slow.next
            fast=fast.next
        return slow

注:为什么可以看成一个带环的链表

①链表带环

两个结点的next指针都指向了同一个结点,会导致链表中存在环。

1-2-3-4-5-6,数值6所在的结点是链表中的尾部节点,对于单链表来说,应该是6.next=None。但是此时6.next指向了3,于是在第一遍遍历完链表中所有结点以后,又从3开始遍历链表,进入环中无限循环下去。

②具有重复数的数组

两个指针指向同一个结点的问题对应到数组中则是:两个索引指向同一个值。

数组[1,2,3,4,5,3,6]中,索引2和索引5同时指向了数值3。

(2)用快慢指针寻找重复数

class Solution:
    def findDuplicate(self, nums: List[int]) -> int:
        slow=fast=nums[0]
        while True:
            slow=nums[slow]
            fast=nums[nums[fast]]
            if slow==fast:
                break
        slow=nums[0]
        while slow!=fast:
            slow=nums[slow]
            fast=nums[fast]
        return slow

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值