背景分析
最优矩阵链乘是二维的动态规划问题,也是较为经典的动态规划入门问题。《算法导论》和刘汝佳的《算法竞赛入门经典》中都有详细描述。
问题描述
在线性代数里,我们都学过矩阵的乘法。矩阵的乘法不满足分配律,但是满足结合律,因此 (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
i 是p
i-1 X p
i的。
思路分析
我们先把这n个矩阵 A
1 X A
2 X A
3 X …… X A
n分成两份,P = A
1 X A
2 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