Task02:动态规划

1 理解动态规划
首先看定义:

动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
从定义中至少可以看出来三点要点:

1.动态规划其实是分治算法的一种
2.动态规划中动态的意思是要解决的问题,其规模不确定,而问题的解却依赖于问题规模
3.能以较快速度(动态规划是一种泛用性算法,而泛用性算法与特定算法相比往往存在性能差距)将结果正确计算出来
动态规划效率高的原因,是以空间换取时间。将前一步的结果储存起来,下一步可以直接调用,加速了计算速度。

引用其他文章中的理解:动态规划这一思想的实质其实是以下两点:
1.分析问题,构造状态转移方程
2.以空间换时间

说下自己的理解:

动态规划是求数列的题目,数列有三个重要元素,初始值,递推式(通项公式),求和式

高中数学题目中,有数列的内容,数列的推导式有两种,一种类似于An=3*n-1,这个是公式法,即通项公式,当知道明确规模n时,可以直接求出An的值

第二种是递推式,即An=An-1+3,这个是递推式,描述前后两项关系,及一种规律,通过这个递推式,同样能解决关于数列的所有信息。

但数学的数列问题,都是欠缺实际含义的,会给出一些已存在的项,及项之间的关系,基本就是幼儿园水平,拿来直接找规律就行。

动态规划的数列问题,则是数列问题的实际运用,至少大学水平(哈哈,调侃一下)。需要1.自己确定数列的含义(确定动态规划状态),即你的数列的值,表示什么意思,存的是啥东西。2.写出状态转移方程,就是递推公式(这个递推公式一般带条件判断)。3.考虑初始化条件(求A0,任何数列问题都需要确定初始值)

这部分基本就是数列问题和动态规划的相似之处了,如何忘记数列知识,可以回去翻高中数学教材。

所以动态规划,可以说是数列思想的计算算法实现,后面还涉及算法优化,难度是很大的。

有了数列,那么下一步就是从数列取要的答案了,这里有几种情况,需要根据题意选择,应该是比较简单的,要么是特定规模的值,要么是数列中的某个极值。

严谨一点表达,就是

返回dp数组中最后一个值作为输出,一般对应二维dp问题。

返回dp数组中最大的那个数字,一般对应记录最大值问题。

返回保存的最大值,一般是Maxval=max(Maxval,dp[i])这样的形式。

最后一步,则是计算机算法都会有的一点东西,算法优化,涉及存储优化。

2 动态规划解决问题的步骤
有了以上理解,那么看一道具体问题的求解过程

第一步:确定动态规划状态
是否存在状态转移?

什么样的状态比较好转移,找到对求解问题最方便的状态转移?

想清楚到底是直接用需要求的,比如长度作为dp保存的变量还是用某个判断问题的状态比如是否是回文子串来作为方便求解的状态

该题目可以直接用一个一维数组dp来存储转移状态,dp[i]可以定义为以nums[i]这个数结尾的最长递增子序列的长度。举个实际例子,比如在nums[10,9,2,5,3,7,101,18]中,dp[0]表示数字10的最长递增子序列长度,那就是本身,所以为1,对于dp[5]对应的数字7来说的最长递增子序列是[2,5,7](或者[2,3,7])所以dp[5]=3。

第二步:写出一个好的状态转移方程
使用数学归纳法思维,写出准确的状态方程

比如还是用刚刚那个nums数组,我们思考一下是如何得到dp[5]=3的:既然是递增的子序列,我们只要找到nums[5] (也就是7)前面那些结尾比7小的子序列,然后把7接到最后,就可以形成一个新的递增的子序列,也就是这个新的子序列也就是在找到的前面那些数后面加上7,相当长度加1。当然可能会找到很多不同的子序列,比如刚刚在上面列举的,但是只需要找到长度最长的作为dp[5]的值就行。总结来说就是比较当前dp[i]的长度和dp[i]对应产生新的子序列长度,我们用j来表示所有比i小的组数中的索引,可以用如下代码公式表示

for i in range(len(nums)):
    for j in range(i):
    	if nums[i]>nums[j]:
    		dp[i]=max(dp[i],dp[j]+1)

Tips: 在实际问题中,如果不能很快得出这个递推公式,可以先尝试一步一步把前面几步写出来,如果还是不行很可能就是 dp 数组的定义不够恰当,需要回到第一步重新定义 dp 数组的含义;或者可能是 dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组。

第三步:考虑初始条件
​ 这是决定整个程序能否跑通的重要步骤,当我们确定好状态转移方程,我们就需要考虑一下边界值,边界值考虑主要又分为三个地方:

dp数组整体的初始值

dp数组(二维)i=0和j=0的地方

dp存放状态的长度,是整个数组的长度还是数组长度加一,这点需要特别注意。

对于本问题,子序列最少也是自己,所以长度为1,这样我们就可以方便的把所有的dp初始化为1,再考虑长度问题,由于dp[i]代表的是nums[i]​的最长子序列长度,所以并不需要加一。 所以用代码表示就是​dp=[1]*len(nums)​
第四步:考虑输出状态
主要有以下三种形式,对于具体问题,我们一定要想清楚到底dp数组里存储的是哪些值,最后我们需要的是数组中的哪些值:

返回dp数组中最后一个值作为输出,一般对应二维dp问题。

返回dp数组中最大的那个数字,一般对应记录最大值问题。

返回保存的最大值,一般是Maxval=max(Maxval,dp[i])这样的形式。

**Tips:**这个公式必须是在满足递增的条件下,也就是nums[i]>nums[j]​的时候才能成立,并不是nums[i]​前面所有数字都满足这个条件的,理解好这个条件就很容易懂接下来在输出时候应该是​max(dp)​而不是​dp[-1]​,原因就是dp数组由于计算递增的子序列长度,所以dp数组里中间可能有值会是比最后遍历的数值大的情况,每次遍历nums[j]所对应的位置都是比nums[i]小的那个数。举个例子,比如nums=[1,3,6,7,9,4,10,5,6],而最后dp=[1,2,3,4,5,3,6,4,5]。 总结一下,最后的结果应该返回dp数组中值最大的数。

最后加上考虑数组是否为空的判断条件,下面是该问题完整的代码:

def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:return 0  #判断边界条件
        dp=[1]*len(nums)      #初始化dp数组状态
        for i in range(len(nums)):
            for j in range(i):
                if nums[i]>nums[j]:   #根据题目所求得到状态转移方程
                    dp[i]=max(dp[i],dp[j]+1)
        return max(dp)  #确定输出状态

实战
5. 最长回文子串

class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        dp = [[False] * n for _ in range(n)]
        ans = ""
        # 枚举子串的长度 l+1
        for l in range(n):
            # 枚举子串的起始位置 i,这样可以通过 j=i+l 得到子串的结束位置
            for i in range(n):
                j = i + l
                if j >= len(s):
                    break
                if l == 0:
                    dp[i][j] = True
                elif l == 1:
                    dp[i][j] = (s[i] == s[j])
                else:
                    dp[i][j] = (dp[i + 1][j - 1] and s[i] == s[j])
                if dp[i][j] and l + 1 > len(ans):
                    ans = s[i:j+1]
        return ans

72.编辑距离
在这里插入图片描述

class Solution:
    def minDistance(self, word1, word2):
        """
        :type word1: str
        :type word2: str
        :rtype: int
        """
        n = len(word1)
        m = len(word2)
        
        # 有一个字符串为空串
        if n * m == 0:
            return n + m
        
        # DP 数组
        D = [ [0] * (m + 1) for _ in range(n + 1)]
        
        # 边界状态初始化
        for i in range(n + 1):
            D[i][0] = i
        for j in range(m + 1):
            D[0][j] = j
        
        # 计算所有 DP 值
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                left = D[i - 1][j] + 1
                down = D[i][j - 1] + 1
                left_down = D[i - 1][j - 1] 
                if word1[i - 1] != word2[j - 1]:
                    left_down += 1
                D[i][j] = min(left, down, left_down)
        
        return D[n][m]

  1. 打家劫舍
    在这里插入图片描述
class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:
            return 0

        size = len(nums)
        if size == 1:
            return nums[0]
        
        dp = [0] * size
        dp[0] = nums[0]
        dp[1] = max(nums[0], nums[1])
        for i in range(2, size):
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
        
        return dp[size - 1]

  1. 打家劫舍II
    在这里插入图片描述
class Solution:
    def rob(self, nums: [int]) -> int:
        def my_rob(nums):
            cur, pre = 0, 0
            for num in nums:
                cur, pre = max(pre + num, cur), cur
            return cur
        return max(my_rob(nums[:-1]),my_rob(nums[1:])) if len(nums) != 1 else nums[0]

  1. 最长回文子序列
    在这里插入图片描述
class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n = len(s)
        maxL = -1
        dp = [[0]*n for _ in range(n)]

        for i in range(n):
            dp[i][i] = 1
        
        for i in range(n-1, -1, -1):
            for j in range(i+1, n):
                if s[i] == s[j]:
                    dp[i][j] = dp[i+1][j-1] + 2
                else:
                    dp[i][j] = max(dp[i][j-1], dp[i+1][j])
        
        return dp[0][n-1]

  1. 最长连续递增序列
    在这里插入图片描述
class Solution(object):
    def findLengthOfLCIS(self, nums):
        ans = anchor = 0
        for i in range(len(nums)):
            if i and nums[i-1] >= nums[i]: anchor = i
            ans = max(ans, i - anchor + 1)
        return ans

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`std::task::Context` 是 Rust 的异步编程库 `async-std` 和 `tokio` 中常用的类型,用于传递异步任务执行的上下文信息。它包含了当前任务的调度器、Waker、时间信息等。要使用 `Context`,需要先创建一个 `Waker` 对象,它会在任务可以继续执行时被唤醒。 下面是一个示例代码,展示了如何在 `async-std` 中使用 `Context`: ```rust use async_std::task::{Context, Poll}; use async_std::task; use std::future::Future; use std::pin::Pin; async fn my_task() { println!("Start task"); task::sleep(std::time::Duration::from_secs(1)).await; println!("Task completed"); } struct MyFuture {} impl Future for MyFuture { type Output = (); fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { // 唤醒任务,使它可以执行 cx.waker().wake_by_ref(); Poll::Ready(()) } } fn main() { // 创建异步任务 let my_task = my_task(); // 执行异步任务 task::spawn(async move { // 等待异步任务完成 my_task.await; println!("My task is done"); // 创建 MyFuture 对象并执行它 let my_future = MyFuture {}; let mut my_future_pin = Pin::new(&mut my_future); my_future_pin.poll(&mut Context::from_waker(task::noop_waker_ref())).await; println!("MyFuture is done"); }); // 等待异步任务执行完成 task::block_on(async {}); } ``` 在上面的代码中,我们首先创建了一个异步任务 `my_task`,然后使用 `task::spawn` 执行它。在任务执行完成后,我们创建了一个 `MyFuture` 对象,并使用 `Context` 执行它。注意到 `poll` 方法中的 `cx.waker().wake_by_ref()`,它会唤醒任务,使得它可以继续执行。 需要注意的是,`Context` 的创建需要传递一个 `Waker` 对象。在示例代码中,我们使用了 `task::noop_waker_ref()`,它是一个空的 `Waker` 对象,可以在不需要唤醒任务的情况下创建 `Context`。如果需要唤醒任务,可以自定义一个 `Waker` 对象并传递给 `Context`。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值