矩阵连乘问题

动态规划算法的基本要素

  • 最优子结构性质
  • 重叠子问题性质

设计动态规划算法的步骤

  • 找出最优解的性质,并刻划其结构特征
  • 递归地定义最优值
  • 以自底向上的方式计算出最优值
  • 根据计算最优值时得到的信息,构造最优解

分析最优解的结构特征

假设我们已经知道了在第 k k k 个位置加括号会得到最优解,那么原问题就变成了两个子问题: ( A i A i + 1 . . . A k ) (A_iA_{i+1}...A_k) (AiAi+1...Ak) ( A k + 1 A k + 2 . . . A j ) (A_{k+1}A_{k+2}...A_j) (Ak+1Ak+2...Aj), 如下图所示。

矩阵连乘问题_第1张图片

原问题的最优解是否包含子问题的最优解呢?

假设 ( A i A i + 1 . . . A j ) (A_iA_{i+1}...A_j) (AiAi+1...Aj) 的乘法次数是 c c c ( A i A i + 1 . . . A k ) (A_iA_{i+1}...A_k) (AiAi+1...Ak) 的乘法次数是 a a a ( A k + 1 A k + 2 . . . A j ) (A_{k+1}A_{k+2}...A_j) (Ak+1Ak+2...Aj) 的乘法次数是 b b b ( A i A i + 1 . . . A k ) (A_iA_{i+1}...A_k) (AiAi+1...Ak) ( A k + 1 A k + 2 . . . A j ) (A_{k+1}A_{k+2}...A_j) (Ak+1Ak+2...Aj) 的结果矩阵相乘的乘法次数是 d d d,那么 c = a + b + d c=a+b+d c=a+b+d,无论两个子问题 ( A i A i + 1 . . . A k ) (A_iA_{i+1}...A_k) (AiAi+1...Ak) ( A k + 1 A k + 2 . . . A j ) (A_{k+1}A_{k+2}...A_j) (Ak+1Ak+2...Aj) 的计算次序如何,都不影响它们结果矩阵,两个结果矩阵相乘的乘法次数 d d d 不变。

因此,我们只需要证明如果 c c c 是最优的,则 a a a b b b 一定是最优的(即原问题的最优解包含子问题的最优解)。

反证法:如果 a a a 不是最优的, ( A i A i + 1 . . . A k ) (A_iA_{i+1}...A_k) (AiAi+1...Ak) 存在一个最优解 a ′ a' a a ′ < a a'a<a,那么, a ′ + b + d < c a'+b+da+b+d<c,所以 c c c 不是最优的,这与假设 c c c 是最优的矛盾,因此如果 c c c 是最优的,则 a a a 一定是最优的。同理可证 b b b 也是最优的。因此如果 c c c 是最优的,则 a a a b b b 一定是最优的。

因此,矩阵连乘问题具有最优子结构性质。

建立最优值递归式

矩阵连乘问题_第2张图片
矩阵连乘问题_第3张图片

可以递归地定义 m [ i , j ] m[i,j] m[i,j] 为:

m [ i ] [ j ] = { 0 , i = j min ⁡ i ≤ k < j { m [ i ] [ k ] + m [ k + 1 ] [ j ] + p [ i − 1 ] ∗ p [ k ] ∗ p [ j ] } , i < j m[i][j]= \begin{cases} 0, & i=j \\ \min \limits_{i \leq k < j} \left\{m[i][k] + m[k+1][j] + p[i-1] * p[k] * p[j] \right\}, & im[i][j]=0,ik<jmin{m[i][k]+m[k+1][j]+p[i1]p[k]p[j]},i=ji<j

计算出最优值

这里以 AIZU 的 Matrix-chain Multiplication 为例,题目链接:点击这里

矩阵连乘问题_第4张图片
矩阵连乘问题_第5张图片

递归(自顶向下)

计算 A [ 1...4 ] A[1...4] A[1...4] 的递归树如下图所示:

矩阵连乘问题_第6张图片

从上图可以看出很多子问题被重复运算。可以证明,该算法的计算时间 T ( n ) T(n) T(n) 有指数下界。设算法中判断语句和赋值语句为常数时间,则由算法的递归部分可得关于 T ( n ) T(n) T(n) 的递归不等式:

矩阵连乘问题_第7张图片

用数学归纳法可以证明 T ( n ) ≥ 2 n − 1 T(n) \geq 2^{n-1} T(n)2n1,因此,重叠递归的计算时间随 n n n 指数增长。

TLE代码:

#include
#include
#include

using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 110;

int n;
int p[N];

int fun(int l, int r)
{
	if(l == r)	return 0;
	
	int minn = INF;
	for(int k = l; k < r; k++)
	{
		int t = fun(l, k) + fun(k + 1, r) + p[l-1] * p[k] * p[r];
		if(t < minn)	minn = t;
	}
	
	return minn;
}

int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)	scanf("%d%d", &p[i-1], &p[i]);
	
	printf("%d\n", fun(1, n));
	
	return 0;
}

备忘录(自顶向下)

AC代码:

#include
#include
#include

using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 110;

int n;
int p[N];
int f[N][N];

int fun(int l, int r)
{
	if(f[l][r])	return f[l][r];
	
	if(l == r)	return 0;
	
	int minn = INF;
	for(int k = l; k < r; k++)
	{
		int t = fun(l, k) + fun(k + 1, r) + p[l-1] * p[k] * p[r];
		if(t < minn)	minn = t;
	}
	
	return f[l][r] = minn;
}

int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)	scanf("%d%d", &p[i-1], &p[i]);
	
	printf("%d\n", fun(1, n));
	
	return 0;
}

动态规划(自底向上)

AC代码:

#include
#include
#include

using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 110;

int n;
int p[N];
int f[N][N];

int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)	scanf("%d%d", &p[i-1], &p[i]);
	
	for(int len = 2; len <= n; len++)
	{
		for(int i = 1; i + len - 1 <= n; i++)
		{
			int j = i + len - 1;
			f[i][j] = INF;
			for(int k = i; k < j; k++)
				f[i][j] = min(f[i][j], f[i][k] + f[k+1][j] + p[i-1] * p[k] * p[j]);
		}
	}
	
	printf("%d\n", f[1][n]);
	
	return 0;
}

构造出最优解

上面得到的最优值只是矩阵连乘的最小的乘法次数,并不知道加括号的次序,需要从记录表中还原加括号次序,构造出最优解。

用二维数组 s [   ] [   ] s[\ ][\ ] s[ ][ ] 来存放各个子问题的最优决策(即加括号的位置)。

根据最优决策信息数组 s [   ] [   ] s[\ ][\ ] s[ ][ ] 递归构造最优解:

  • s [ 1 ] [ n ] s[1][n] s[1][n] 表示 A 1 A 2 . . . A n A_1A_2...A_n A1A2...An 最优解的加括号位置,即 ( A 1 A 2 . . . A s [ 1 ] [ n ] ) ( A s [ 1 ] [ n ] + 1 . . . A n ) (A_1A_2...A_{s[1][n]})(A_{s[1][n]+1}...A_n) (A1A2...As[1][n])(As[1][n]+1...An)
  • 再递归构造两个子问题 ( A 1 A 2 . . . A s [ 1 ] [ n ] ) (A_1A_2...A_{s[1][n]}) (A1A2...As[1][n]) ( A s [ 1 ] [ n ] + 1 . . . A n ) (A_{s[1][n]+1}...A_n) (As[1][n]+1...An) 的最优解加括号位置
  • 一直递归到子问题只包含一个矩阵为止。

在备忘录的代码基础上构造出最优解:

#include
#include
#include

using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 110;

int n;
int p[N];
int f[N][N];
int s[N][N];

int fun(int l, int r)
{
	if(f[l][r])	return f[l][r];
	
	if(l == r)	return 0;
	
	int minn = INF;
	for(int k = l; k < r; k++)
	{
		int t = fun(l, k) + fun(k + 1, r) + p[l-1] * p[k] * p[r];
		if(t < minn)
		{
			minn = t;
			s[l][r] = k;						// 记录决策信息 
		}
	}
	
	return f[l][r] = minn;
}

void print(int i, int j)
{
    if(i == j)
    {
		printf("A[%d]", i);
		return;
    }
    printf("(");
    print(i, s[i][j]);
    print(s[i][j] + 1, j);
    printf(")");
}

int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)	scanf("%d%d", &p[i-1], &p[i]);
	
	printf("%d\n", fun(1, n));
	
	print(1, n);								// 输出路径 
	
	return 0;
}

在动态规划的代码基础上构造出最优解:

#include
#include
#include

using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 110;

int n;
int p[N];
int f[N][N];
int s[N][N];

void print(int i, int j)
{
    if(i == j)
    {
		printf("A[%d]", i);
		return;
    }
    printf("(");
    print(i, s[i][j]);
    print(s[i][j] + 1, j);
    printf(")");
}

int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)	scanf("%d%d", &p[i-1], &p[i]);
	
	for(int len = 2; len <= n; len++)
	{
		for(int i = 1; i + len - 1 <= n; i++)
		{
			int j = i + len - 1;
			f[i][j] = INF;
			for(int k = i; k < j; k++)
			{
				int t = f[i][k] + f[k+1][j] + p[i-1] * p[k] * p[j];
				if(t < f[i][j])
				{
					f[i][j] = t;
					s[i][j] = k;				// 记录最优决策 
				}
			}
		}
	}
	
	printf("%d\n", f[1][n]);
	
	print(1, n);								// 输出路径 
	
	return 0;
}

过程图解

矩阵连乘动态规划的过程图解(帮助理解):

初始化:

矩阵连乘问题_第8张图片

计算 2 2 2 个矩阵相乘的最优值:

矩阵连乘问题_第9张图片

计算 3 3 3 个矩阵相乘的最优值:

矩阵连乘问题_第10张图片

计算 4 4 4 个矩阵相乘的最优值:

矩阵连乘问题_第11张图片

计算 5 5 5 个矩阵相乘的最优值:

矩阵连乘问题_第12张图片

构造最优解过程:

矩阵连乘问题_第13张图片

你可能感兴趣的:(计算机算法设计与分析,王晓东)