您的位置:首页 > 其它

通过矩阵连乘问题理解动态规划

2016-10-07 13:53 246 查看
这次,我们希望通过矩阵连乘问题来更好的理解下动态规划。

首先,给出矩阵连乘问题的描述(需要最基本的线性代数知识):

给你N个矩阵,设它们分别为A1,A2,...,AN。且假设它们的行列数允许它们连乘。

然后,因为矩阵相乘具有结合律(就是可以在不改变整体顺序的情况下,在连乘中对局部矩阵加上括号,让它们先进行计算,结果不变)。

而矩阵的乘法运算的复杂度与两个矩阵的行列数目有关(设有p行q列矩阵A和q行r列矩阵B,则AB相乘的时间复杂度大致为O(pqr))。

所以,我们可以发现,通过运用结合律,改变计算优先顺序,是可以让计算量显著减少的。

那么问题来了,如何运用结合律(对这N个矩阵加括号)来使得最终时间复杂度最小。

当然,这个问题可以用穷举法来做,但是这样效率低下,且这篇文章是希望通过矩阵连乘问题来理解动态规划的。所以,我们就直接用动态规划算法的思想来分析这个问题吧。

动态规划最基本的思路是:

首先获得一些初始状态,然后找到一个方法,能根据这些初始状态推出之后的状态,然后依次推进,最终获得我们想要的结果。

而且动态规划问题一般具有以下这些特点:

1.每个状态可以看作是原问题的子问题,(称之为最优子结构)。

2.状态的推进方法一般可以抽象一个状态转移方程用来表示(或者也可以用简单的伪代码表示)

3.后面的结果不会再影响前面的结果了(称之为无后效性)。

4.因为我们要根据子问题做推进,所以需要找地方先记录一下子问题的结果(一般称其为“备忘录”,一般用一个名为dp的一或多维数组来记录,而且做空间复杂度优化的时候还可以找找是否可以放弃记录一些过早的信息)。

下面我们就结合具体问题来分析吧!

首先,可以发现A1...AN的矩阵的最优连乘值,它所包含的子段也必须是最优的,因为如果子段不是最优的,那就可以用最优的代替,从而找出整体最优值。这样,显然这个问题就具备了动态规划问题的一大特点,具有最优子结构。

然后,我们也可以发现最优大段矩阵连乘可以通过找最优子段矩阵连乘做合适的拼接来找出,层层递进,所以后面的计算结果是不可能影响前面的计算结果的,这又是动态规划的一个重要特征,无后效性。

最后,动态规划问题最关键的步骤是找到递推的方法,并由此决定选取什么样的“备忘录”来保存结果。

不同问题,递推方式不同。

这个问题中,因为我们知道了最优大段矩阵连乘可以通过恰当的选取最优子段矩阵连乘的结果来得出。那么我们就希望通过分割的方式:

找一个分割点,把前面的和后面的分开,如AB则可以分成(A)(B),ABC则可能被分成(AB)(C)或(A)(BC)至于里面的则就根据子结果得出就行。

这样,一个大段连乘矩阵就被分割成很多小段,我们只要恰当的选取小段,最终就可以组合乘最优的大段连乘了。

比如,考虑N=1的情况,那么根本不用乘,所以结果是0。(这也是问题的初始状态,再正式推进前,需要初始化好的)

再是N=2的情况,只有一种乘法,所以结果固定。

然后N=3的情况下,我们就要考虑是先算前面的,还是先算后面的问题了,也就是前2个和后2个,显然,它们的结果在N=2的时候被算过了,所以我们只要根据N=2的结果,就可以推出N=3的结果了(尝试2种分割方式,取最小的)。

最后,再稍微提一下N=4,其实和N=3的情况差不多,就是找分割点,注意的是,无论是N=3,还是N=4,或是N>4,都只要找出一个分割点就行了,至于内部的子问题,肯定是被前面计算过的。

这时,我们知道我们采取了分割的方式来做递推,所以我们可以用一个二维数组DP[i][j]来保存子结果。DP[i][j]表示第i个到第j个位置的连乘的最优解。

初始状态刚刚说过了,所有DP[i][i]=0(一维二维下标相等时就为0,代表单个矩阵无需乘法)

struct Matrix
{
int row;
int col;
Matrix(int row, int col) :row(row), col(col) {}
Matrix() :row(0), col(0) {}
};
vector<Matrix> vm(n + 1);//这里从下标1开始,无参构造函数会帮助我们初始化


然后是递推过程,我们先上下代码:

//其中trace数组是记录DP选择路径,从而帮助后面打印结果
int i, j, k, r, t;
vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));
vector<vector<int>> trace(n + 1, vector<int>(n + 1, 0));
for (r = 1; r < n; ++r) {
for (i = 1; i <= n - r; ++i) {
j = i + r;
//i+1~j的间隔显然是小于i~j的,所以肯定是之前算过的,拿来用就行了
dp[i][j] = dp[i + 1][j] + vm[i].row * vm[i].col * vm[j].col;
trace[i][j] = i;
for (k = i + 1; k < j; ++k) {
t = dp[i][k] + dp[k + 1][j] + vm[i].row * vm[k].col * vm[j].col;
if (t < dp[i][j]) {
dp[i][j] = t;
trace[i][j] = k;
}
}
}
}
cout << "the result is " << dp[1]
<< endl;


递推的整体思路(感觉用文字描述更好):

我们希望循环来一次次的逐渐增加(i , j)的间隔,因为大间隔的最优值必须建立在小间隔的最优值的基础上。所以要从小的开始,后面大的才可以借助小的推出来。

首先从间隔1开始,意思就是各种(i,i+1)的情况,这时就是包含了两个矩阵的意思,两个矩阵因为只有一种乘法,所以只要一次就行。算出来填进去就可以了。然后后面间隔超越1的部分,不单要算Ai∗(Ai+1...AN)的计算次数,还要查一下被分割后是否可以取得更优的值((Ai+1 … Ak) 与 (Ak+1 … Aj)的乘法次数是否更优)。

最后再附下结果显示函数,一般很多DP问题的结果都可以通过保存它的优化选择路径然后反推来实现打印。

string getRes(vector<vector<int>> &trace, int begin, int end) {
string res;
if (begin == end) {
char c = 'A' + begin - 1;
res = c;
return res;
}
int k = trace[begin][end];
if (begin == k) {
res = getRes(trace, begin, k);
}
else {
res = "(" + getRes(trace, begin, k) + ")";
}
if (k + 1 == end) {
res += getRes(trace, k + 1, end);
}
else {
res += "(" + getRes(trace, k + 1, end) + ")";
}
return res;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  算法 动态规划