十,算法-动态规划

递归的效率

 九,算法-递归-CSDN博客中讨论了递归函数的阅读、书写方法,同时也简单阐述了计算机中调用递归函数的方式,无限递归会导致内存溢出的问题。除了无限递归,还有一种情况也会导致递归的执行效率变得很低,示例如下:

int max(const std::vector<int>& array) {
	if (array.size() == 1) {
		return array[0];
	}

	if (array[0] > max(std::vector<int>(array.begin() + 1, array.end() ))) {
		return array[0];
	}
	else {
		return max(std::vector<int>(array.begin() + 1, array.end()));
	}
}

代码中if语句中都调用了一次max(std::vector<int>(array.begin() + 1, array.end())), 乍看之下没什么问题,但这是一个递归调用,每次对max的调用都会触发一系列的递归调用,以一个数组[1,2,3,4]来对这个递归函数进行分析:

  • 基准情形:max({4}),直接返回1;
  • 基准情形的上一情形:max({3,4}),该过程中重复调用两次max({4});
  • 继续分析:max({2, 3,4}),则max({3,4})被调用两次,max({4})被调用4次;
  • 再往上分析一层:max({1,2,3,4}),max函数将会被调用15次。

如果数据量变为N,则max的时间复杂度近似于O(2^{N})。怎么降低其执行的时间复杂度?使用变量存储中间调用过程的数值。将计算结果保存在变量中可以避免多余的递归调用,从而加快递归执行效率。

重复子问题

上述的max函数中重复递归的部分,其调用实参相同,只是重复调用;那么同时存在两次max函数的调用,且实参不同的情况该怎么处理呢?这里以实现斐波那契数为例:

int fib(int n) {
	if (n == 0 || n == 1) {
		return n;
	}

	return fib(n - 2) + fib(n - 1);
}

此处,在递归函数体内调用自身两次,且重复调用较多,时间复杂度近似O(2^{N}),这就是重复子问题。解决重复子问题的方法就是动态规划。

动态规划

动态规划是一种优化有重复子问题的递归问题的过程。通过动态规划优化算法有两种基本方法:记忆化自下而上

记忆化

记忆化的方法本质上和通过变量存储计算数值的方法相同,即记录过往计算过的函数来减少递归调用。比较适合用来记录计算过的数值的数据结构即哈希表。重复子问题的根源就在于重复进行相同的递归调用,而记忆化机通过哈希表记录每个新计算结果(递归函数调用结果)用来出现重复调用时使用。实现的方法就是在fib函数中增加一个哈希表参数,修改如下:

int fib(int n, std::unordered_map<int, int>& maps) {
	if (n == 0 || n == 1) {
		return n;
	}
	if (maps.find(n) == maps.end()) {
		maps[n] = fib(n - 2, maps) + fib(n - 1, maps);
	}
	return maps[n];
}

通过记忆化的方法,可以将时间复杂度降低到O(N)。

自下而上

自下而上就是放弃递归方法(哭笑不得的表情)。这不是在搞笑,因为动态规划的定义就是优化有重复子问题的递归问题的过程,修改代码如下:

int fib(int n) {
	if (n == 0) {
		return 0;
	}

	int a = 0;
	int b = 1;
	for (int i{ 1 }; i <= n; ++i) {
		int temp{ a };
		a = b;
		b = temp + a;
	}

	return b;
}

选择记忆化还是自下而上,本质上就是在选择是否要使用递归。如果递归的解法更加直观,则可以用记忆化的递归方法来解决问题,否则自下而上的方法通常是更好的选择。

### Floyd-Warshall算法动态规划实现原理 #### 算法概述 Floyd-Warshall算法是一种经典的多源最短路径算法,能够有效地找出带权重图中每一对顶点之间的最短距离。该算法特别适合于稠密图,并能处理负权重边的情况,只要不存在负权重环即可[^1]。 #### 动态规划的核心思想 此算法利用了动态规划的思想来逐步构建最终的结果矩阵。具体来说,在每次迭代过程中都会考虑一个新的中间节点k作为可能经过的一个点,从而更新当前已知的最佳路径长度。对于每一个顶点对(i, j),如果存在一条经由第k个顶点更优的新路线,则会用这条新路线上两个端点间的总成本替换旧的成本值。 #### 初始化阶段 初始化时设置邻接矩阵`dist[][]`中的元素代表直接相连两结点间的真实代价;而对于未直连者赋予无穷大∞表示不可达状态。另外还需要准备一个辅助性的前驱矩阵`pPath[][]`用来追踪具体的路由信息以便后续重建完整的最短路径链表[^2]。 ```cpp for (int i = 0; i < n; ++i){ for(int j=0;j<n;++j){ dist[i][j]=graph[i][j]; // graph存储初始输入的数据 if(graph[i][j]!=INF && i!=j)pPath[i][j]=i; else pPath[i][j]=-1; } } ``` #### 迭代优化过程 接下来进入核心循环部分,这里采用三重嵌套的方式遍历所有的顶点组合以及潜在中介站点: - 外层循环变量`k`指示正在评估哪个顶点可充当临时跳板; - 中间一层负责选取起点位置`i`; - 内部则针对终点位置`j`做判断并尝试刷新记录。 每当发现借助某个特定编号为`k`的中途站可以使某条既定线路变得更经济实惠时,就立即调整对应项下的数值配对关系。 ```cpp // 主体逻辑:逐轮引入新的候选转接枢纽直至穷尽全部可能性为止 for (int k = 0; k < n; ++k) { for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { if(dist[i][j]>dist[i][k]+dist[k][j]){ dist[i][j]=dist[i][k]+dist[k][j]; pPath[i][j]=pPath[k][j]; } } } } ``` #### 字交叉法解释 所谓“字交叉法”是指当我们在纸上手动模拟上述运算流程时所采取的一种直观形象化的手段。想象有一张网格纸上的表格被划分成了许多小格子,每个单元格内填写着相应索引处的距离度量。随着程序执行进度推进,“视线”沿着斜方向划过整个平面形成类似‘X’形状轨迹——即所谓的“字体”。这种方法有助于理解为什么在每一次加入新的中间节点后都要重新审视之前已经考察过的所有连接情况[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值