动态规划问题(矩阵链乘法,二叉搜索树,01背包问题)算法导论版复习

本文详细介绍了动态规划在矩阵链乘法和最优二叉搜索树中的应用,涉及递归定义、子问题结构特征、计算最优解的方法,以及01背包问题的基本状态动态规划算法。
摘要由CSDN通过智能技术生成

动态规划问题的分析步骤

  • 刻画一个最优解的结构特征 —> 如何使子问题最优
  • 递归的定义最优解的值
  • 计算最优解的值,一般采用自底向上的方法
  • 利用计算出的信息构造一个最优解

矩阵链:

步骤1:最优化方案的结构特征

  1. 问题定义:多个矩阵连乘,如何加括号使得总的乘法次数最小
  2. Ai…j表示AiAi+1…Aj乘积的结果矩阵
  3. 为了对其进行括号化,必须对某个整数k,首先计算Ai…k和Ak+1…j ,再计算他们的乘积。此方案的代价是前两个计算代价加上两者乘积的计算代价

步骤2:递归求解方案

  1. 变量构造:令m[i,j]表示计算Ai…j 所需标量乘法的最少次数,那么原问题的最优解是m[1,n]
    • 对于平凡问题,有m[i,i] = 0
    • 对于非平凡问题,假设Ai…j的最优分割点在Ak和Ak+1 之间,其中i<=k<j
      • 已知矩阵Ai 的大小为pi-1 * pi
      • 则有m[i,j] = m[i,k] + m[k+1 , j] +pi-1*pk *pj
    • 并且用s[i,j]保存最优括号方案的分割点k

最优计算代价𝑚[𝑖,𝑗]只依赖于那些长度更小的矩阵链相乘的最优计算代价

代码如下

def matrix_chain_order(p):
    n = len(p) - 1  # Number of matrices in the chain
    m = [[0] * n for _ in range(n)]  # Initialize a table to store optimal costs
    s = [[0] * n for _ in range(n)]  # Initialize a table to store split points

    for l in range(2, n + 1):  # l is the chain length
        for i in range(n - l + 1):
            j = i + l - 1
            m[i][j] = float('inf')
            
            for k in range(i, j):
                q = m[i][k] + m[k+1][j] + p[i] * p[k+1] * p[j+1]
                if q < m[i][j]:
                    m[i][j] = q
                    s[i][j] = k  # Record the split point

    return m, s

首先要将m[i][i]=0,然后从2开始遍历链的长度 ,用s数组来记录分割点的位置,后续需要打印括号矩阵时只需递归使用s数组:

def print_optimal_parentheses(s, i, j):
    if i == j:
        print(f'A{i+1}', end='')
    else:
        print('(', end='')
        print_optimal_parentheses(s, i, s[i][j])
        print_optimal_parentheses(s, s[i][j] + 1, j)
        print(')', end='')

在这里插入图片描述

例子:
在这里插入图片描述

二叉搜索树

定义

设T是二叉树,有以下性质:

  • T的左子树的所有元素比根节点的元素小
  • T的右子树的所有元素比根节点的元素大
  • T的左子树和右子树也是二叉搜索树
  • 注:二叉搜索树要求树中所有结点的元素值互异

最优二叉搜索树的引入:

想要在给定关键字频率的前提下,如何组织一颗二叉搜索树,使得所有搜索访问的节点总数最少。

• 最优二叉搜索树不一定是高度最矮的(平衡树不一定最优)。

• 概率最高的关键字不一定出现在最优二叉搜索树的根结点。

步骤一:最优二叉搜索树的结构

对于一个二叉搜索树的任意子树,其必然包含连续关键字ki , … , kj,1<= i <= j <= n

且其叶节点必然是伪关键字di-1,… ,dj (当查询的目标值小于ki,那么就会返回伪关键字

di-1,其他d同理)

对每个关键字𝑘𝑖,都有一个概率𝑝𝑖表示其搜索频率。

对每个伪关键字𝑑𝑖,都有一个概率𝑞𝑖表示其搜索频率。

在这里插入图片描述

  • 最优子结构性质

    如果一颗最优二叉搜索树 T T T有一颗包含ki,…kj的子树 T ′ T' T ,那么 T ′ T' T 必然是包含ki,…kj的子问题的最优解

    • 如何证明:

      剪切-粘贴法证明:如果存在 T ′ ′ T'' T′′期望值比 T ′ T' T低,那么用 T ′ ′ T'' T′′代替 T ′ T' T得到一颗期望值低于T 的二叉搜索树 , 与T最优的假设矛盾

  • 利用子问题的最优解来构造原问题的最优解:

    给定关键字序列ki,…kj,假设其中某关键字kr(i <= r <=j ) , 是这些关键字最优子树的根节点。

    kr的左子树包含关键字𝑘𝑖, … 𝑘𝑟−1(和伪关键字𝑑𝑖−1, … 𝑑𝑟−1

    其右子树包括关键字𝑘𝑟+1, … 𝑘𝑗(和伪关键字𝑑𝑟 , … 𝑑𝑗

    注意空子树

    • 假设以ki为根节点,𝑘𝑖的左子树包含关键字𝑘𝑖 , … 𝑘𝑖−1, 这代表着该子树不包含任意关键字,但包含单一伪关键字di-1

步骤二:建立递归公式:

  • 子问题域:求解包含关键字𝑘𝑖 , … 𝑘𝑗的最优二叉搜索树,其中𝑖 ≥ 1, 𝑗 ≤ 𝑛且𝑗 ≥ 𝑖 − 1(当𝑗 = 𝑖 − 1时,子树只包含伪关键字𝑑𝑖−1)。

  • 定义𝑒[𝑖,𝑗]为在包含关键字𝑘𝑖 , … 𝑘𝑗的最优二叉搜索树中进行一次搜索的最优代价。我们期望得到的是e[i,j] 。

  • 当𝑗 = 𝑖 − 1时, 𝑒 [𝑖, 𝑖 − 1] = 𝑞𝑖−1。就是查伪关键字𝑑𝑖−1的代价,因为只包含一个伪关键节点 1 * qi-1

  • 当𝑗 ≥ 𝑖时,我们从𝑘𝑖, … 𝑘𝑗中选择根节点𝑘r , 然后构建左右最优二叉搜索树 , 包含的关键字分别为 𝑘𝑖 , … 𝑘𝑟−1 和𝑘𝑟+1, … 𝑘r

    • **注意:**当一颗子树成为一个节点的子树时,这颗子树的每个节点的深度都会加1 , 这颗子树的期望搜索代价增加 , 且增加值等于这颗子树所有节点的概率之和 (层数加1 , 1*概率)
    • 重点:包含𝑘𝑖, … 𝑘𝑗 的子树的所有概率之和 w ( i , j ) = ∑ l = i j p l + ∑ l = i − 1 j q l w(i,j) = \sum_{l= i}^{j} p_l + \sum_{l= i-1}^{j} q_l w(i,j)=l=ijpl+l=i1jql 理解:使用两个已有的子树和一个节点,新建一个树时额外增加的代价,那么也可以写为 w ( i , j ) = w ( i , r − 1 ) + 1 ∗ p r + w ( r + 1 , j ) w(i,j) = w(i,r-1)+ 1*p_r + w(r+1 , j) w(i,j)=w(i,r1)+1pr+w(r+1,j)
  • e [ i , j ] = p r + ( e ( i , r − 1 ) + w ( i , r − 1 ) ) + ( e ( r + 1 , j ) + w ( r + 1 , j ) ) e[i,j] = p_r + (e(i,r-1) + w(i,r-1)) + (e(r+1,j)+w(r+1 , j)) e[i,j]=pr+(e(i,r1)+w(i,r1))+(e(r+1,j)+w(r+1,j))

    结合之前的公式,可以发现

    e [ i , j ] = e ( i , r − 1 ) + e ( r + 1 , j ) + w ( i , j ) e[i,j] = e(i,r-1) + e(r+1,j)+w(i,j) e[i,j]=e(i,r1)+e(r+1,j)+w(i,j) 若 i <= j

步骤三:动态规划计算最优二叉搜索树的期望搜索代价

  • 表𝑒[1. . 𝑛 + 1, 0. . 𝑛]保存𝑒[𝑖,𝑗]的值。
    • 第一维下标上界为𝑛 + 1而不是 𝑛,是因为对于只包含𝑑𝑛的子树,我们需要计算并保存𝑒[𝑛 + 1, 𝑛]。第二 维下标下界为0而不是1,是因为对于只包含𝑑0的子树,我们需要计算并保存𝑒[1,0]。
  • 只使用满足𝑗 ≥ 𝑖 − 1的表项𝑒[𝑖,𝑗]。还使用表项root[𝑖,𝑗]记录包含 𝑘𝑖 , … 𝑘𝑗的子树的根。
  • 为了避免每次计算𝑒[𝑖,𝑗]时都重新计算𝑤(𝑖,𝑗),我们将这些值保存在表 𝑤[1. . 𝑛 + 1, 0. . 𝑛]中。对于𝑗 = 𝑖 − 1的情况,𝑤(𝑖, 𝑖 − 1) = 𝑞𝑖−1(1 ≤ 𝑖 ≤ 𝑛 + 1)。对于𝑗 ≥ 𝑖的情况,𝑤(𝑖,𝑗 ) = 𝑤( 𝑖,𝑗 − 1) + 𝑝𝑗 + 𝑞𝑗
  • 在这里插入图片描述

img

01背包问题

在这里插入图片描述

设01背包问题的子问题的最优值为m[i][j],即背包容量为j,可选择物品为i,i+1,…,n时的最优值。

在这里插入图片描述

在这里插入图片描述

// 基本状态的动态规划算法Knapsack求解0-1背包问题
//时间复杂度为O(nc)
template <class Type>
void Knapsack(Type* v, int* w, int c, int n, Type** m) {
	int jMax = min(c, w[n] - 1);
	for (int j = 0; j <= jMax; j++) {
		m[n][j] = 0; // 只看最后一个物品,不可以装下的全部为 0 
	}
	for (int j = w[n]; j <= c; j++) {
		m[n][j] = v[n];
	} // 处理最后一个物品完毕
	for (int i = n - 1; i > 1; i--) {
		jMax = min(c, w[i] - 1);
		for (int j = 0; j <= jMax; j++) { // 第i个物品放不下的情况 
			m[i][j] = m[i + 1][j];	 
		}
		for (int j = w[i]; j <= c; j++) {// 第i个物品可以放下的情况 
			m[i][j] = max(m[i + 1][j], m[i + 1][j - w[i]] + v[i]);
		}
	}
	 
	if (c >= w[1]) { 
		m[1][c] = max(m[2][c], m[2][c - w[1]] +v[1]); // 最后结果 
	}
	else
	{
		m[1][c] = m[2][c]; // 处理边界
	}
} 

// 求解最后物品选取的状态
//时间复杂度为O(n)

template <class Type>
void Traceback(Type** m, Type* w, int c, int n, int* x) { // x下标范围(0,n]
	for (int i = 1; i < n; i++) {
		if (m[i][c] == m[i + 1][c]) {
			x[i] = 0;
		} else {
			x[i] = 1;
			c -= w[i];
		}
	}
	x[n] = (m[n][c]) ? 1 : 0; // 看最后一个物品 
} 

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值