算法学习笔记(8)-动态规划基础篇

目录

基础内容:

动态规划:

动态规划理解的问题引入:

解析:(暴力回溯)

代码示例:

暴力搜索:

Dfs代码示例:(搜索)

暴力递归产生的递归树:

记忆化搜索:

代码示例:

动态规划:

代码示例:(动态规划,从最小子问题开始)

执行过程(动态规划):

解析:(动态规划)

空间优化:

代码示例:

解析:


基础内容:

什么是动态规划,动态规划作为一种手段可以解决哪些问题,动态规划的分类,以及具体的分类可以解决的具体问题的分类。

动态规划:

是一个重要的算法范式,它将一个问题分解成一系列更小的子问题,并通过存储子问题解避免重复计算,从而大幅度提升时间效率。

动态规划理解的问题引入:

通过爬楼梯的案例来引入这个问题,给定一个共有n阶的楼梯,你每步可以上1阶或者2阶,请问有多少种方案可以爬到楼顶。

解析:(暴力回溯)

本题目的目标是求解方案数量,我们可以考虑通过回溯来穷举所有可能性。具体来说,将爬楼梯想象为一个多轮选择的过程:从地面出发,每轮选择上一阶或者二阶,每当达到楼梯顶部时就将方案数量加1,当越过楼梯顶部就将其剪枝。

代码示例

python 复制代码
# python代码示例
def backrack(choices,state,n,res) :
    if state == n :
        res[0] += 1 
    for choice in choices :
        if state + choice > n :
            continue
        backrack(choices,state+choice,n,res)
def climbing_stairs_backrack(n) :
    choices = [1,2]
    state = 0
    res = [0]
    backrack(choices,state,n,res)
    return res[0]
n = int(input())
print(climbing_stairs_backrack(n))
cpp 复制代码
// c++代码示例
void backrack(vector<int> &choices, int state, int n, vector<int> &res)
{
    if (state == n )
    {
        res[0]++ ;
    }
    for (auto &choice : choices)
    {
        if (state + choice > n)
        {
            continue ;
        }
        backrack(choices, state + choice, n, res)
    }
}

int climbingStairsBackrack(int n)
{    
    vector<int> choices = {1 , 2 } ;
    int state = 0 ;
    vector<int> res = [0] ;
    backrack(choices, state, n, res) ;
    return res[0] ;
}

暴力搜索:

回溯算法通常并不显式地对问题进行拆解,而是将问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。

我们可以尝试从问题分解的角度分析这道题。设爬到第i阶共有dp[i]中方案,那么dp[i]就是原问题,其子问题包括:

dp[i-1],dp[i-2],dp[1],dp[2]

由于每轮只能上1阶或者2阶,因此当我们站在第i阶楼梯上时,上一轮只可能站在第i-1或者i-2台阶上。换句话说,我们只能从第i-1阶或者第i-2阶迈向第i阶。

由此便可以得出一个重要的推论:爬到第i-1阶的方案加上爬到第i-2阶的方案数就等于爬到第i阶的方案数。公式如下:

dp[i] = dp[i-1] + dp[i-2]

这就意味着,爬楼问题中存在着递推的关系,原问题可由子问题的解构建来得到解决

Dfs代码示例:(搜索)

python 复制代码
# python 代码示例
def dfs(i : int) -> int :
    if i == 1 or i == 2 :
        return i
    count = dfs(i - 1) + dfs(i - 2)
    return count
def climbing_stairs_dfs(n : int) -> int :
    retunr dfs(n)
cpp 复制代码
// c++ 代码示例
int dfs(int i)
{
    if (i == 1 || i == 2)
    {
        return i ;
    }
    int count = dfs(i - 1) + dfs(i - 2);
    return count ;
}
int climbingStairsDFS(int n)
{
    retunr dfs(n) ;
}

暴力递归产生的递归树

解决上述递归树中的重复问题,采用记忆化搜索的方式,可以把大量重复构建的相同子树进行去掉,从而达到提高计算效率。(重叠子问题

记忆化搜索:

将所有重叠的子问题只进行一遍计算,需要声明一个数组nem来记录每个子问题的解,并在搜索过程中将重叠子问题剪枝。

  1. 当首次计算dp[i]时,将其记录在nem[i],便于后续的使用
  2. 当再次计算dp[i]时,直接在nem[i]中进行获取结果,避免重复子问题的计算。

代码示例:

python 复制代码
# python 代码示例
def dfs(i : int, mem : list[int]) -> int :
    if i == 1 or i == 2 :
        return i
    if mem[i] != -1 :
        return mem[i]
    count = dfs(i - 1, mem) + dfs(i - 2, mem)
    # 记录dfs(i)
    mem[i] = count
    return count
def climbing_stairs_dfs_mem(n : int) -> int :
    mem = [-1] * (n + 1)
    return dfs(n, mem)
cpp 复制代码
// c++ 代码示例
int dfs(int i, vector<int> &mem)
{
    if (i == 1 || i == 2)
    {
        return i ;
    }
    if (mem != -1)
    {
        return mem[i] ;
    }
    int count = dfs(i - 1, mem) + dfs(i - 2, mem) ;
    mem[i] = count ;
    return count ;
}
int climbingStairsDFSMem(int n)
{
    vector<int> mem(n + 1, -1) ;
    return dfs(n, mem) ; 
}

经过记忆化处理后,所有重叠的子问题都只计算一次,时间复杂度优化到了O(n)

动态规划:

记忆化搜索是一种"从顶至低"的方法,我们从原问题(根节点)开始,递归地将较大子问题分解成较小子问题,直至解已知的最小子问题(叶节点)。之后,通过回溯逐层收集子问题的解,构建出原问题的解。

与之相反,动态规划是一种"从底至顶"方法:从最小子问题的解开始,迭代地构建更大子问题的解,直至得到原问题的解。

由于动态规划不包含回溯过程,因此只需要使用循环迭代实现,无须使用递归。

代码示例:(动态规划,从最小子问题开始)

python 复制代码
# python 代码示例
def clibing_stairs_dp(n) :
    if n == 1 or n == 2 :
        return n
    dp = [0] * (n + 1)
    dp[1], dp[2] = 1, 2
    for i in range(3,n + 1) :
        dp[i] = dp[i-1] + dp[i- 2]
    return dp[n]
cpp 复制代码
// c++ 代码示例

int climbingStairsDP(int n) 
{
    if (n == 1 || n == 2)
    {
        retunr n ;
    }
    vector<int> dp(n + 1, -1) ;
    dp[1] = 1 ;    
    dp[2] = 2 ;
    for (int i = 3 ; i <= n ; i++)
    {
        dp[i] = dp[i - 1] + dp[i- 2] ;
    }
    return dp[n] ;
}

执行过程(动态规划):

解析:(动态规划)

相似于回溯算法,动态规划也使用"状态"概念来表示问题求解的特定阶段,每个状态都对应一个子问题以及相应的局部最优解。例:爬楼梯问题的状态定义为当前所在楼梯的阶数i

根据以上内容,我们可以总结为动态术语的常用术语:

  1. 将数组dp称为{dp表},dp[i]表示状态i对应子问题的解
  2. 将最小子问题对应的状态,(第一阶和第二阶楼梯)称为初始状态
  3. 将递推公式dp[i] = dp[i-1] + dp[i-2]称为状态方程

空间优化:

dp[i] 只跟 dp[i-1] 和 dp[i-2] 有关

无须使用一个数组来存储所有子问题的解,只需要两个变量滚动前进即可。

代码示例:

python 复制代码
# python 代码示例
def clibing_stairs_dp_comp(n) :
    if n == 1 or n == 2 :
        return n
    a, b = 1, 2
    for _ in range(3, n + 1) :
        a, b = b , a + b
    return b
cpp 复制代码
// c++ 代码示例
int climbingStairsComp(int n) 
{
    if (n == 1 || n == 2)
    {
        return n ;
    }
    int a = 1 , b = 2 ;
    for (int i = 3 ; i <= n ; i++)
    {
        int temp = b ;
        b = a + b ;
        a = temp ;
    }
    return b ;
}

解析:

省去了数组dp所占用的空间,空间复杂度由O(n)降为O(1)

在动态规划问题中,当前状态仅与前面有限个状态有关,这时我们可以只保留必要的状态,通过"降维"来节省内存空间**。这种空间优化技巧被称为"滚动变量"或"滚动数组"。**

相关推荐
帅云毅15 分钟前
Web3.0的认知补充(去中心化)
笔记·学习·web3·去中心化·区块链
豆豆16 分钟前
day32 学习笔记
图像处理·笔记·opencv·学习·计算机视觉
逢生博客25 分钟前
使用 Python 项目管理工具 uv 快速创建 MCP 服务(Cherry Studio、Trae 添加 MCP 服务)
python·sqlite·uv·deepseek·trae·cherry studio·mcp服务
堕落似梦32 分钟前
Pydantic增强SQLALchemy序列化(FastAPI直接输出SQLALchemy查询集)
python
nenchoumi311937 分钟前
VLA 论文精读(十六)FP3: A 3D Foundation Policy for Robotic Manipulation
论文阅读·人工智能·笔记·学习·vln
ChoSeitaku40 分钟前
17.QT-Qt窗口-工具栏|状态栏|浮动窗口|设置停靠位置|设置浮动属性|设置移动属性|拉伸系数|添加控件(C++)
c++·qt·命令模式
凉、介43 分钟前
PCI 总线学习笔记(五)
android·linux·笔记·学习·pcie·pci
SuperSwaggySUP1 小时前
4/25 研0学习日志
学习
黄昏ivi1 小时前
电力系统最小惯性常数解析
算法
独家回忆3641 小时前
每日算法-250425
算法