矩陣相乘
實際上矩陣相乘題是典型的區間動態規劃
區間DP的操作如圖,區間範圍由小到大逐步增加
首先我們來看看動態規劃的四個步驟:
- 找出最優解的性質,並且刻畫其結構特性;
- 遞歸的定義最優解;
- 以自底向上的方式刻畫最優值;
- 根據計算最優值時候得到的信息,構造最優解
其中改進的動態規劃算法:備忘錄法,是以自頂向下的方式刻畫最優值,對於動態規劃方法和備忘錄方法,兩者的使用情況如下:
一般來講,當一個問題的所有子問題都至少要解一次時,使用動態規劃算法比使用備忘錄方法好。此時,動態規劃算法沒有任何多餘的計算。同時,對於許多問題,常常可以利用其規則的表格存取方式,減少動態規劃算法的計算時間和空間需求。當子問題空間中的部分子問題可以不必求解時,使用備忘錄方法則較爲有利,因爲從其控制結構可以看出,該方法只解那些確實需要求解的問題。
對於動態規劃算法,我們必須明確兩個基本要素,這兩個要素對於在設計求解具體問題的算法時,是否選擇動態規劃算法具有指導意義:
- 算法有效性依賴於問題本身所具有的最優子結構性質:設計算法的第一步通常是要刻畫最優解的結構。當問題的最優解包含了子問題的最優解時,稱該問題具有最優子結構性質。問題的最優子結構性質提供了該問題可以使用動態規劃算法求解的重要線索
在矩陣連乘積最優次序問題中注意到,若A1A2…An的最優完全加括號方式在Ak和Ak+1之間斷開,則由此可以確定的子鏈A1A2A3…Ak和Ak+1Ak+2…An的完全加括號方式也最優,即該問題具有最優子結構性質。在分析該問題的最優子結構性質時候,所使用的方法具有普遍性。首先假設由原問題導出的子問題的借不是最優解,然後在設法說明在這個假設下可以構造出比原問題最優解更好的解,從而導致矛盾。
在動態規劃算法中,利用問題的最優子結構性質,以自底向上的方式遞歸的從子問題的最優解逐漸構造出整個問題的最優解。算法考察的子問題的空間規模較小。例如在舉證連乘積的最優計算次序問題中,子問題空間由矩陣鏈的所有不用的子鏈組成。所有不用的子鏈的個數爲o(n* n),因而子問題的空間規模爲o(n* n)
- 可以用動態規劃算法求解問題應該具備另一個基本要素是子問題的重疊性。在用遞歸算法自頂向下求解此問題時候,每次產生的子問題並不總是新問題,有些子問題被反覆計算多次。動態規劃算法正是利用了這種子問題的重疊性質,對每一個子問題都只是求解一次,而後將其保存到一個表格中,當再次需要解此問題時,只是簡單使用常數時間查看一下結果。 通常,不同子問題個數隨着問題大小呈多項式增長。因此使用動態規劃算法通常只是需要多項式時間,從而獲得較高的解題效率。
逐步分析
P=<30,35,15,5,10,20> 它對應5個矩陣
A1:30*35 A2:35*15 A3:15*5 A4:5*10 A5:10*20
先計算:
當r=1
表示兩個矩陣相乘的運算量
m[1,1]=0 m[2,2]=0 m[3,3]=0 m[4,4]=0 m[5,5]=0
當r=2
表示兩個矩陣相乘的運算量
m[1,2]=303515=15750
m[2,3]=35155=2625
m[3,4]=15510=750
m[4,5]=51020=1000
當r=3
表示3個矩陣相乘的運算量
m[1,3]=min{m[1,2]+30155,m[2,3]+30355}=min{15750+2625,2625+5250}=7875 A1(A2A3) s[1,3]=1
m[2,4]=min{m[2,3]+35510,m[3,4]+351510}={2625+1750,750+5250}=4375 (A2A3)A4 s[2,4]=3
m[3,5]=min{m[3,4]+151020,m[4,5]+15520}=2500 A3(A4A5) s[3,5]=3
當r=4
表示4個矩陣相乘的運算量
m[1,4]=min{m[2,4]+303510,m[1,2]+m[3,4]+301510,m[1,3]+30510}
=min{4375+10500,15750+750+4500,7875+1500}
=9375 [A1A2A3]A4---->(A1(A2A3))A4 s[1,4]=3
m[2,5]=min{m[3,5]+351520,m[2,3]+m[4,5]+35520,m[2,4]+351020}
=min{2500+10500,2625+1000+3500,4375+7000}
=7125 (A2A3)(A4A5) s[2,5]=3
當r=5
表示5個矩陣相乘的運算量
m[1,5]=min{m[2,5]+303520,m[1,2]+m[3,5]+301520,m[1,3]+m[4,5]+30520,m[1,4]+301020}
=min{7125+21000,15750+2500+9000,7875+1000+3000,9375+9000}
=11875 [A1A2A3](A4A5)---->(A1(A2A3))
(A4A5) s[1,5]=3
優化函數備忘錄:
r=1 m[1,1]=0 m[2,2]=0 m[3,3]=0 m[4,4]=0 m[5,5]=0
r=2 m[1,2]=15750 m[2,3]=2625 m[3,4]=750 m[4,5]=1000
r=3 m[1,3]=7875 m[2,4]=4375 m[3,5]=2500
r=4 m[1,4]=9375 m[2,5]=7125
r=5 m[1,5]=11875
標記函數
r=2 s[1,2]=1 s[2,3]=2 s[3,4]=3 s[4,5]=4
r=3 s[1,3]=1 s[2,4]=3 s[3,5]=3
r=4 s[1,4]=3 s[2,5]=3
r=5 s[1,5]=3
根據 s[1,5]=3
推出最後一次劃分的位置爲3 [A1A2A3][A4A5]
然後再由[A1A2A3]
找s[1,3]=1
推出最後一次劃分位置爲1 A1(A2A3)
所以最終答案爲:(A1(A2A3))(A4A5)
運算次數爲:11875
次
練習:
再將P=<20,70,25,30,5,35,10>計算出來
Java實現
public class Strassen {
/*
* array[i][j]表示Ai...Aj相乘最少計算次數
* s[i][j]=k,表示Ai...Aj這(j-i+1)個矩陣中最優子結構爲Ai...Ak和A(k+1)...Aj
* p[i]表示Ai的行數,p[i+1]表示Ai的列數
*/
private int array[][];
private int p[];
private int s[][];
public Strassen(){
p=new int[]{2,4,5,5,3};
array=new int[4][4];
s=new int[4][4];
}
public Strassen(int n,int []p){
this.p=new int[n+1];
this.array=new int[n][n];
this.s=new int[4][4];
for(int i=0;i<p.length;i++)
this.p[i]=p[i];
}
/*********************方法一,動態規劃**********************************/
public void martixChain(){
int n=array.length;
for(int i=0;i<n;i++)
array[i][i]=0;
for(int r=2;r<=n;r++){
for(int i=0;i<=n-r;i++){
int j=i+r-1;
array[i][j]=array[i+1][j]+p[i]*p[i+1]*p[j+1];
s[i][j]=i;
for(int k=i+1;k<j;k++){
int t=array[i][k]+array[k+1][j]+p[i]*p[k+1]*p[j];
if(t<array[i][j]){
array[i][j]=t;
s[i][j]=k;
}
}
}
}
}
/*
* 如果待求矩陣爲:Ap...Aq,then a=0,b=q-p
*/
public void traceBack(int a,int b){
if(a<b){
traceBack(a, s[a][b]);
traceBack(s[a][b]+1, b);
System.out.println("先把A"+a+"到A"+s[a][b]+"括起來,在把A"+(s[a][b]+1)+"到A"+b+"括起來,然後把A"+a+"到A"+b+"括起來");
}
}
/*********************方法二:備忘錄方法*****************************/
public int memorizedMatrixChain(){
int n=array.length;
for(int i=0;i<n;i++){
for(int j=i;j<n;j++)
array[i][j]=0;
}
return lookUpChain(0,n-1);
}
public int lookUpChain(int a,int b){
if(array[a][b]!=0)
return array[a][b];
if(a==b)
return 0;
array[a][b]=lookUpChain(a, a)+lookUpChain(a+1, b)+p[a]*p[a+1]*p[b+1];
s[a][b]=a;
for(int k=a+1;k<b;k++){
int t=lookUpChain(a, k)+lookUpChain(k+1, b)+p[a]*p[k+1]*p[b+1];
if(t<array[a][b]){
array[a][b]=t;
s[a][b]=k;
}
}
return array[a][b];
}
public static void main(String[] args) {
Strassen strassen=new Strassen();
//strassen.martixChain();
strassen.memorizedMatrixChain();
strassen.traceBack(0, 3);
}
}
C++實現
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
LL a[100],d[100][100],b[100][100],n,cost;
//a爲矩陣鏈各向量長度,d爲優化函數,b爲標記函數
void link(int l,int r) {
if (l==r) {
printf("A%d",l);
return;
}
//輸出左邊矩陣/矩陣鏈的括號表示法
if (b[l][r]-l>0) putchar('(');
link(l,b[l][r]);
if (b[l][r]-l>0) putchar(')');
//輸出右邊矩陣/矩陣鏈的括號表示法
if (r-b[l][r]>1) putchar('(');
link(b[l][r]+1,r);
if (r-b[l][r]>1) putchar(')');
}
int main()
{
//讀入矩陣相乘的長度
scanf("%lld",&n);
for (int i=0;i<=n;i++) scanf("%lld",a+i);
//逐層計算r長度矩陣鏈的最優運算量,r=k+1
for (int k=1;k<n;k++) {
//p枚舉區間右端,所計算區間爲[p-k,p]
for (int p=k+1;p<=n;p++) {
//枚舉標記點,按照題意,是左區間末端
for (int i=p-k;i<p;i++) {
cost=d[p-k][i]+d[i+1][p]+a[p-k-1]*a[p]*a[i];
if (!b[p-k][p] || cost<d[p-k][p]) {
b[p-k][p]=i; //標記
d[p-k][p]=cost; //更新優化函數
}
}
}
}
//優化函數備忘錄
puts("優化函數備忘錄");
for (int k=0;k<n;k++) {
printf("r=%d",k+1);
//p枚舉區間右端,所計算區間爲[p-k,p]
for (int p=k+1;p<=n;p++)
printf(" m[%d,%d]=%lld",p-k,p,d[p-k][p]);
printf("\n");
}
//標記函數
puts("標記函數");
for (int k=1;k<n;k++) {
printf("r=%d",k+1);
//p枚舉區間右端,所計算區間爲[p-k,p]
for (int p=k+1;p<=n;p++)
printf(" s[%d,%d]=%lld",p-k,p,b[p-k][p]);
printf("\n");
}
//括號表示法用axx表示矩陣
puts("括號表示法");
link(1,n);
printf("\n");
return 0;
}
/*
6
20 70 25 30 5 35 10
優化函數備忘錄
r=1 m[1,1]=0 m[2,2]=0 m[3,3]=0 m[4,4]=0 m[5,5]=0 m[6,6]=0
r=2 m[1,2]=35000 m[2,3]=52500 m[3,4]=3750 m[4,5]=5250 m[5,6]=1750
r=3 m[1,3]=50000 m[2,4]=12500 m[3,5]=8125 m[4,6]=3250
r=4 m[1,4]=19500 m[2,5]=24750 m[3,6]=6750
r=5 m[1,5]=23000 m[2,6]=17750
r=6 m[1,6]=22250
標記函數
r=2 s[1,2]=1 s[2,3]=2 s[3,4]=3 s[4,5]=4 s[5,6]=5
r=3 s[1,3]=2 s[2,4]=2 s[3,5]=4 s[4,6]=4
r=4 s[1,4]=1 s[2,5]=4 s[3,6]=4
r=5 s[1,5]=4 s[2,6]=4
r=6 s[1,6]=4
括號表示法
(A1(A2(A3A4)))(A5A6)
*/