【算法设计技巧】动态规划

任何数学递推公式都可以直接翻译成递归算法,但基本实现是:编译器常常不能正确对待递归算法,结果导致低效的程序。当怀疑很可能是这种情况时,我们必须再给编译器提供一些帮助,将递归算法重新写成非递归算法,让后者把那些子问题的答案系统地记录在一个表内。一种利用这种方法的技巧叫作动态规划(dynamic programming)

用表代替递归

例如,计算斐波那契数的递归程序的运行时间 T(N) 是以与斐波那契数相同的速度在增长,从而是指数级的。

//计算斐波那契数的低效算法
long long fib(int n)
{
	if (n <= 1)
		return 1;
	else
		return fib(n - 1) + fib(n - 2);
}

另一方面,由于计算 FN 所需的只是 FN-1 和 FN-2,因此我们只需要记录最近算出的两个斐波那契数,由此可得下面的 O(N) 算法。

//计算斐波那契数的线性算法
long long fibonacci(int n)
{
	if (n <= 1)
		return 1;

	long long last = 1;
	long long nextToLast = 1;
	long long ans = 1;

	for (int i = 2; i <= n; ++i)
	{
		ans = last + nextToLast;
		nextToLast = last;
		last = ans;
	}
	return ans;
}

递归算法如此慢的原因在于算法那模仿了递推公式。为了计算 F(N),存在一个对 FN-1FN-2 的调用。然而,由于 FN-1 递归地对 FN-2FN-3 进行调用,因此存在两个单独计算 FN-2 的调用。如果跟踪整个算法,则可以发现,FN-3 被计算了3次,FN-4 计算了5次,而 FN-5 则是8次,等等。

冗余计算的增长是爆炸性的,如果编译器的递归模拟算法要是能保留一个所有预先算出的值的表,而对已经解决过的子问题不再进行递归调用,那么这种指数式的爆炸增长就可以避免。

第二个例子,求解递推关系

     C ( N ) = ( 2 / N ) ∑ i = 0 N − 1 C ( i ) + N C(N)=(2/N)\sum^{N-1}_{i=0}C(i)+N C(N)=(2/N)i=0N1C(i)+N

其中,C(0) = 1。

//递归函数
double eval(int n)
{
	if (n == 0)
		return 1;
	else {
		double sum = 0.0;
		for (int i = 0; i < n; ++i)
			sum += eval(i);
		return 2.0 * sum / n + n;
	}
}

//使用表的非递归例程
double eval(int n)
{
	vector<double> c(n + 1);

	c[0] = 1.0;
	for (int i = 1; i <= n; ++i)
	{
		double sum = 0.0;

		for (int j = 0; j < i; ++j)
			sum += c[j];
		c[i] = 2.0 * sum / i + i;
	}
	return c[n];
}

所有点对最短路径

在前面看到单源最短路径(single-source shortest-path)问题的一个算法,该算法找出从某个任意的点 s 到所有其他顶点的最短路径。这个(Dijkstra)算法对稠密的图以 O(|V|2) 时间运行,实际上对稀疏的图要更快。

运用动态规划,计算有向图 G = (V ,E) 中每一点对间赋权最短路径的一个算法,其运行时间为 O(|V|3),它是对 Dijkstra 算法 |V| 次迭代的一种渐进改进,但对非常稠密的图更快,原因是它的循环更紧凑。如果存在一些负的边值但没有负值圈,这个算法也能正确运行:而 Dijkstra 算法此时是无效的。

回忆一下,Dijkstra 算法从顶点 s 开始并分阶段工作。图中的每个顶点最终都要被选作中间顶点。如果当前所选的顶点是 v,那么对于每个 w ∈ V,置 dw = min(dw, dv + cv,w) 。即,从 s 到 w 的最佳距离或者是前面知道的从 s 到 w 的距离,或者是从 s(最优地) 到 v 然后再直接从 v 到 w 的结果。

Dijkstra 算法为动态规划算法提供了这样的想法:按照顺序选择这些顶点。将定义 Dk,i,j 为从 vi 到 vj 只使用 v1, v2, … , vk 作为中间顶点的最短路径的权。根据这个定义,D0,i,j = ci,j,其中若 (vi, vj) 不是该图的边则 ci,j 是 ∞ 。再有,D|V|,i,j 是图中从 vi 到 vj 的最短路径。

当 k >0 时可以给出 Dk,i,j 的一个简单公式。从 vi 到 vj 只使用 v1, v2, … , vk 作为中间顶点的最短路径,或者是根本不使用 vk 作为中间顶点的最短路径,或者是由两条路径 vi → vk 和 vk → vj 合并而成的最短路径,其中的每条路径只使用前 k - 1 个顶点作为中间顶点,由此得

    Dk,i,j = min{Dk-1,i,j, Dk-1,i,k + Dk-1,k,j}

/**
* 计算所有点对的路径
* a包含邻接矩阵,a[i][i]=0
* d包含最短路径的值
* 顶点从0开始编号
* 所有数组维数相等,如果
* d[i][i]置为负值,则负值圈存在
* 具体路径可以用path[][]来计算
* NOT_A_VERTEX = -1
*/
void allPairs(const matrix<int>& a, matrix<int>& d, matrix<int>& path)
{
	int n = a.numrows();

	//初始化d 和path
	for (int i = 0; i < n; ++i)
		for (int j = 0; j < n; ++j)
		{
			d[i][j] = a[i][j];
			path[i][j] = NOT_A_VERTEX;
		}

	for(int k=0;k<n;++k)
		//把每个顶点看作为一个中间节点
		for(int i=0;i<n;++i)
			for (int j = 0; j < n; ++j)
				if (d[i][k] + d[k][j] < d[i][j])
				{
					//更新最短路径
					d[i][j] = d[i][k] + d[k][j];
					path[i][j] = k;
				}
}

动态规划是强大的算法设计技巧,给问题的解决提供了一个起点。它基本上是首先求解更简单的问题的分治算法的范例,重要的区别在于这些更简单的问题不是原问题的明晰的分割。因为一些子问题被重复求解,所以重要的是将他们的解记录在一个表中而不是去重新计算它们。在某些情况下,解可以被改进(通常是比较难的),在另一些情况下,动态规划方法则是已知最好的处理方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhugenmi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值