动态规划 (二) 最优矩阵链乘

背景分析

最优矩阵链乘是二维的动态规划问题,也是较为经典的动态规划入门问题。《算法导论》和刘汝佳的《算法竞赛入门经典》中都有详细描述。

问题描述

在线性代数里,我们都学过矩阵的乘法。矩阵的乘法不满足分配律,但是满足结合律,因此 (A x B)x C 与 A x(B x C)的运算结果是一样的,但是中间的运算量却可能不一样。

例如:假设A、B、C矩阵分别是 2 x 3、3 x 4、4 x 5 的,则 (A x B)x C   的运算量是 2 x 3 x 4 + 2 x 4 x 5 = 64,但是 A x(B x C)的运算量是 3 x 4 x 5 + 2 x 3 x 5 = 90。(想想运算量为什么是这么计算的。)显然第一种计算方法比较节省计算量。

好,现在考虑下面的问题:给出n个矩阵组成的序列,设计一种方法,把他们按照一定的顺序依次乘起来(你也可以理解为不断地加括号),是的总的计算量尽量小。假设第i个矩阵 A
是p
i-1 X p
i的。

思路分析

我们先把这n个矩阵 A
X A
X A
X …… X A
n分成两份,P = A
X A
X … X A
k和 Q = A
k+1 X A
k+2 X … X A
n,其中 1<= k < n。显然所求的结果可以由PXQ得到,且答案是k从1到n-1循环一边,运算结果最小的那个。这是一个多阶段决策的最优化问题,我们可以用动态规划解决。现在我们用dp[i][j]表示矩阵A
i X A
i+1 X … X A
j(显然 i < j,否则无意义)链乘的最少运算次数,那么可以得到下面的状态转移方程:  

f(i, j) = min { f(i, k) + f(k+1, j) + P(i-1)P(k)P(j) }, i <= k < j 

两个相邻的状态,可以通过一次矩阵链乘得到。若还是不理解这个方程,可以把 k 从 i 到 j-1 列举出来,相应的加上括号做乘法,再选一个最大值。

算法实现

我们可以先用好理解的记忆化搜索来实现(废话比较多,可以直接只看search函数)

const int INF_NUM = 0x3f3f3f3f;
const int MAX = 1002;
int dp[MAX][MAX];	// dp[i][j]表示第i个矩阵链乘到第j个的最少运算次数
int p[MAX];	// 矩阵维数
int matrix_chain_memorized()
{
	int n = strlen(p) - 1; // n为矩阵的个数,即p数组长度减一
	read_data(n); // 读入数据,此处略去
	memset(dp, 0x3f, sizeof(dp));
	return search(1, n);
}

int search(int i, int j)
{
	// 记忆化
	if(dp[i][j] < INF_NUM)	return dp[i][j];
	// 初始化条件
	if(i == j)	dp[i][j] = 0;
	// tag
	else {
		// 循环一遍,找到最小的
		for(int k = i; k < j; ++ k)
		{
			int ans = search(i, k) + search(k+1) + p[i-1]*p[k]*p[j];
			if(ans < dp[i][j])	
				dp[i][j] = ans;
		}
	}
	return dp[i][j];
}

但是如果要写成递推式,就要考虑一下循环的顺序了。递推和记忆化搜索的区别就在这儿,记忆化搜索不用担心循环的顺序,但是递推只能调用前面已经算出来过的式子。书上一般给出的解决方法时,从长度短的递推到长度长的。就是说,先把所有两个矩阵链乘的情况都算出来,再算三个矩阵链乘的情况,那么此时算两个矩阵的情况就是三个的最优子问题了。

我们可以在外面循环设置一个变量 len 来表示此时矩阵链乘的长度,当然 j 的值也就固定位 i + len – 1了。

代码如下:

void matrix_chain()
{	
	// 初始化,即长度为 1 的情况
	for(i = 0; i < n; ++ i)
		dp[i][i] = 0;

	// 从长度为 2 开始循环
	for(len = 2; len <= n; ++ len)
	{
		for(i = 1; i <= n - len + 1; ++ i)
		{
			j = i + len - 1;
			for(k = i; k < j; ++ k)
			{
				int ans = dp[i][k] + dp[k+1][j] + p[i-1] * p[k] * p[j];
				if(dp[i][j] > ans)
					dp[i][j] = ans;
			}	
		}
	}	
	
	cout << dp[1][n] << endl;	
}

我们来尝试走一次循环,以便理解为什么 for 循环的边界要这样子设置 首先是长度 len = 2,表示这一遍循环会计算所有A
i X A
i+1的情况,即两个连续的矩阵相乘的情况。i 会 从 1 循环到 n – 1,j 总是比 i 大 1,由于我们在初始化中已经把所有长度为1的情况,即dp[i][i]赋值为0,所以k循环里不会用到没有算过的值。

动手时间

假如都理解了上面的代码,那么可以去OJ上A一下这个题目了,基本上就上面的代码稍微改一下就行,链接如下:
POJ 1651

    原文作者:动态规划
    原文地址: https://blog.csdn.net/zhangxb35/article/details/38959047
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞