問題提出:
對於如下矩陣:
其中各矩陣A[i]下標為
計算其乘積的結果,以及我們需要計算其最小標量乘法次數。
問題分析:
首先我們需要明確的是何為標量:標量即為沒有方向的量,而有方向的量即為矢量。(嚴謹的定義自己百度去)
那麼標量乘法就變成了最基本的數字相乘。
其次對於兩個矩陣相乘,需滿足下示公式所示的形式:(左邊矩陣的列數與右邊矩陣的行數必須一致)
上述條件可從矩陣相乘的定義中看出:
在計算機,我們可以用一個二維數組來表示矩陣。
一個m行n列的矩陣與一個n行p列的矩陣相乘,會得到一個m行p列的矩陣,具體步驟如下:
其中第i行第j列位置上的數為第一個矩陣的第i行上的n個數與第二個矩陣第j列上的n個數對應相乘後所得的n個乘積之和。
下圖展示了一個簡單的例子:
從上圖我們可以得出兩個矩陣相乘城的次數,即為取得結果矩陣每個點所要做的標量乘法次數之和
而每個點的標量乘法次數為左邊矩陣的列數n,結果矩陣有m*p個點,那麼總數即為m*n*p;
另外我們也應該知道矩陣乘法符合乘法結合律,但不符合交換律;也就是說,我們可以為矩陣鏈乘設置括號,括號放置的位置不同,產生標量乘法的總量也是不一樣的哦,這一點我們必須明確,對這點不清楚的,可以自己舉個例子試試看。如果沒有這點,那這題就沒意義了。
前面科普了一大堆知識,現在進正題了:
對於矩陣鏈乘,我們可知道不同的括號化方案,會產生不同的計算代價;
算法導論書中指出了求完全括號化方案,所謂矩陣乘積鏈為完全括號化需滿足如下性質:
要麼是單一矩陣,要麼是兩個完全括號化的矩陣乘積鏈的積,且已加外括號。說實話這個定義讓人有點摸不著頭腦,待我仔細解釋下:
首先:該乘積鏈中只有一個矩陣,即單一矩陣,該乘積鏈必是完全括號化,且該矩陣外部不用加括號。
再者:必須是兩個括號化的矩陣鏈的乘積,我們可以把兩個括號化的矩陣鏈看作兩個結果矩陣(即兩個單一矩陣),該兩個矩陣相乘,並加上外括號把他們括住,即這種形式也叫著完全括號化的。
啰嗦了這麼多,感覺簡單的說法就是:單一矩陣(不加括號)是完全括號化,兩個矩陣(單一矩陣)加上外括號也是完全括號化的,完全括號化的乘積鏈可以當作單一矩陣處理。
下面上一道課後的證明題:
對n個元素的表達式進行完全括號化時,恰好需要n-1對括號。
證明:
注:采用數學歸納法
已知僅有一個元素時,不需要括號,即0對括號。
假設k個元素時,需要k-1對括號;
當有k+1個元素時,我們可以看作k個元素的完全括號化的乘積鏈外加1個新加的元素。已知我們可以把完全括號化的乘積鏈看作單一矩陣,那麼現有兩個單一矩陣,那麼需再加1對括號,才能再次完全括號化,而由假設知,k個元素的完全括號化需k-1對括號,那麼k+1個元素的完全括號化方案就需要k-1+1對括號;
命題得證!
額,又科普了部分知識,只不過這些科普對後續理解問題都是有用的。
應用動態規劃的方法的步驟:(出自書本)
1、刻畫一個最優解的結構特征
2、遞歸地定義最優解的值
3、計算最優解的值,通常采用自底向上的方法
4、利用計算出的信息構造一個最優解的值
第一步:刻畫一個最優解的結構特征
對於最開始提出的問題,我們看作為矩陣鏈乘設計最優的括號化方案。而設置括號的最終目的就是為了打亂計算順序,通過結合律來找到最小計算代價,就比如下面一個問題33*4*25,是個正常人就會先計算後面兩項,因為這樣好計算,同樣計算矩陣,我們也希望計算的少,計算的快啊,設置括號就變的尤為重要了。
對於完全括號化,每一個括號的作用是將一個鏈乘轉化成兩個完全括號化的鏈乘的積,前面有提到。
因此我最優解的結構特征就如下了:
對於第m個矩陣到第n個矩陣,他的最優解存在於對它的一次劃分中,它的劃分有n-m種情況,我們對這些情況中代價取最小,不就獲得了最優解
而對於每種情況,我們需要劃分後的兩部分矩陣鏈都是最優解,乘積後才可能最小啊,(這部分大家可以用“剪切--粘貼”去反證),這樣問題就轉換成了兩個子部分矩陣鏈的最優括號化方案的問題,以此不斷的遞歸下去。
第二步:遞歸地定義最優解的值
我們的劃分點k可從m到n-1,代價如下所示,每次的代價如下,而最優解只有一個,即它們的最小值:
其一般化的公式如下:
從這裡可以看出,這是一個遞歸的問題,看著很類似上篇所述的鋼條切割問題啊,仔細比對比對吧。
若采用遞歸策略,可能會有很多的重復子問題,無疑提高了計算的復雜度。也可以采取帶備忘的遞歸來降低復雜度。
第三步:計算最優解的值,通常采用自底向上的方法
另外從公式中可以看出,上述計算結果要依賴於m,n(m<n)的所有可能組合。區間[m,n]組合要依賴於它們之間的子區間組合。所以我們采取自下而上的策略,逐漸擴大m,n之間的區間長度,最終算到m=0,n=n,從而獲得最優的括號化方案,
具體方法為:matrix_chain_mutiply.cpp中的void matrix_divide_option(int *num,int** cost,int** point,int length)方法。
第四步、利用計算出的信息構造一個最優解的值
具體解析見matrix_chain_mutiply.cpp最後兩個方法
代碼如下:
matrix.h文件:
#include<iostream>
#ifndef MATRIX_H
#define MATRIX_H
/*
矩陣類
row為其行數
col為該矩陣的列數,
value表示一個二維數組,主要存儲該矩陣各點的值
*/
class matrix
{
public:
matrix(); //默認的構造函數
matrix(int row,int col); //給定行數和列數,構造一個矩陣
virtual ~matrix(); //析構函數
matrix(const matrix& other); //拷貝構造函數
matrix operator*(const matrix& a); //矩陣的乘法
matrix& operator=(const matrix&); //重寫賦值函數
matrix init(); //初始化矩陣
friend std::ostream& operator << (std::ostream&,const matrix &); //重寫輸出
protected:
private:
double **value; //主要用於存儲矩陣各點的值
int col; //列數
int row; //行數
};
#endif // MATRIX_H
matrix.cpp
#include<iostream>
#include "matrix.h"
#include<cstdlib>
#include<ctime>
matrix::matrix() //默認構造函數
{
col = row = 0;
value = nullptr;
}
matrix::matrix(int row, int col) //根據給定行,列數來初始化一個矩陣,並分配相應的內存
{
this->row = row;
this->col = col;
value = new double*[row];
for(int i=0;i<row;i++)
{
value[i] = new double[col];
}
}
matrix::matrix(const matrix& other) //復制構造函數,注意此處要用matrix&,重寫復制構造函數為了防止值復制造成
{
row = other.row;
col = other.col;
value = new double*[row]; //此處使用new分配內存
for(int i=0;i<row;i++)
{
value[i] = new double[col];
}
for(int i=0;i<row;i++)
{
for(int j=0;j<col;j++)
{
value[i][j]=(other.value)[i][j];
}
}
}
matrix::~matrix()
{
for(int i=0;i<row;i++) //因為構造函數中new分配有內存,所以在析構函數中要對內存進行釋放。
{
delete[] value[i];
}
delete[] value;
}
matrix& matrix::operator=(const matrix& other) //重寫了賦值函數
{
if(this == &other) //防止a=a的情況出現
{
return *this;
}
for(int i=0;i<row;i++) //先釋放原有區域的內存,從此處我們可以上面的判斷的重要性
{ //若沒有上述判斷,a=a在賦值的同時,也將內存釋放了,造成數據丟失
delete[] value[i];
}
delete[] value;
row = other.row; //將成員變量進行復制
col = other.col;
value = new double*[row]; //重新分配內存
for(int i=0;i<row;i++)
{
value[i] = new double[col];
}
for(int i=0;i<row;i++) //對二維數組進行復制
{
for(int j=0;j<col;j++)
{
value[i][j]=(other.value)[i][j];
}
}
return *this;
}
matrix matrix::operator*(const matrix& n) //矩陣的乘法
{
matrix result(row,n.col);
for(int i=0;i<row;i++)
{
for(int j=0;j<n.col;j++)
{
(result.value)[i][j] = 0;
for(int k=0;k<n.row;k++)
{
(result.value)[i][j] += value[i][k]*(n.value)[k][j];
}
}
}
return result;
}
matrix matrix::init() //初始化矩陣
{
matrix result(row,col);
srand((unsigned)time(NULL));
for(int i=0;i<row;i++)
{
for(int j=0;j<col;j++)
{
(result.value)[i][j] = rand()%100;
}
}
return result;
}
std::ostream& operator <<(std::ostream& os,const matrix& m)//將矩陣輸出
{
for(int i=0;i<m.row;i++)
{
for(int j=0;j<m.col;j++)
{
os << (m.value)[i][j] << '\t';
}
os << std::endl;
}
return os;
}
matrix_chain_mutiply.cpp(核心)
#include<iostream>
#include "matrix.h"
const int SIZE = 5; //SIZE的大小決定了矩陣的數量(SIZE-1),可自行改變
const int max_size = 2147483647;
void matrix_divide_option(int *,int **,int **,int); //確定最優的括號化方案
void show(int **); //輸出二維數組
void print(int**,int,int); //輸出矩陣鏈乘的括號化方案
matrix matrix_mutiply(int**,int,int,matrix*); //獲得矩陣鏈乘的最終結果矩陣
int main()
{
using namespace std;
cout << "input " << SIZE << " number(press enter to end your input):" << endl;
int *num = new int[SIZE]; //用於存儲矩陣A[i]下標為num[i]*num[i+1];
int i=0;
cin >> num[i]; //獲得矩陣的行列數
while(cin.get() != '\n')
{
i++;
cin >> num[i]; //讀取數組維數;
}
cout << endl << "what you input is:" << endl;
for(int j=0;j<SIZE;j++)
{
cout << num[j] << "-----"; //展示輸入的是否正確
}
cout << endl << endl;
matrix* test = new matrix[SIZE-1]; //為矩陣數組分配內存
for(int j=1;j<SIZE;j++)
{
matrix m(num[j-1],num[j]); //初始化矩陣大小
test[j-1] = m.init(); //初始化該矩陣各點的值
}
cout << "矩陣信息如下:" << endl;
for(int j=0;j<SIZE-1;j++)
{
cout << test[j] << endl; //輸出矩陣信息
}
int **cost = new int*[SIZE-1]; //存儲矩陣相乘時,各標量乘法次數,即代價表
int **point = new int*[SIZE-1]; //存儲矩陣括號化方案時的分割點
for(int j=0;j<SIZE-1;j++)
{
cost[j] = new int[SIZE-1]; //分配內存
point[j] = new int[SIZE-1];
}
matrix_divide_option(num,cost,point,SIZE-1); //獲取劃分方案,獲得最小的代價
cout << "各區間的計算代價:" << endl;
show(cost);
cout << endl;
cout << "區間劃分點所在的位置:" << endl;
show(point);
cout << endl;
cout << "輸出完全括號化方案:" << endl;
print(point,0,SIZE-2);
cout << endl << endl;
cout << "輸出結果矩陣:" << endl;
matrix result = matrix_mutiply(point,0,SIZE-2,test);
cout << result;
for(int j=0;j<SIZE-1;j++)
{
delete[] cost[j];
delete[] point[j];
}
delete[] cost;
delete[] point;
delete[] test;
delete[] num;
return 0;
}
/*
此方法主要獲得計算代價,以及括號化方案的劃分點
為此問題解決的核心方法。
num存儲的矩陣的下標,num[i-1]為第i個矩陣的行數,num[i]表示第i個矩陣的列數
因為數組存儲時,最初始的下標為0,所以下述的定義就變的有點不好理解!仔細想想也容易明白。
cost----二維數組,cost[i][j]代表的就是第(i+1)個矩陣到第(j+1)個矩陣的鏈乘的最小計算代價
point----二維數組,point[i][j]代表的就是第(i+1)個矩陣到第(j+1)個矩陣的鏈乘的最佳劃分處,假如為k,則k就是第k+1個矩陣,
(i+1)到(k+1)矩陣屬於左邊的完全括號化矩陣,其余為右側完全括號化矩陣
length代表的是矩陣的個數,即num的長度-1
*/
void matrix_divide_option(int *num,int** cost,int** point,int length)
{
//首先將代價表中的值重置
//下標i到下標j之間的矩陣鏈乘的最小值
for(int i=0;i<length;i++)
{
for(int j=0;j<length;j++)
{
if(i>=j)
{
cost[i][j] = 0; //單個矩陣不存在乘積,代價0,同時也將不合理的下標組合置為0
}
else
{
cost[i][j] = max_size; //先將各合理的組合代價設為最大,以便後續比較使用。
}
point[i][j] = -1; //將所有組合的劃分點置為-1,以便後續記錄。
}
}
/*
l代表矩陣鏈中矩陣的個數,我們這裡從2個矩陣起步.由公式可知,我們要算遍所有的合理組合,同時大區間的組合
還要依賴於其子區間的合理組合,因為完全括號化的性質,最終都歸根於2個矩陣的乘積,所以我們從2個矩陣起步,
逐步向上計算,最終就能計算(0,n)的組合的最優括號化的代價。
*/
for(int l=2;l<=length;l++)
{
for(int i=0;i<=length-l;i++) //此處要注意的i+l-1不能超過數組的個數
{
int j = i+l-1; //矩陣個數要為l,則j-i需滿足j-i+1=l,所以有該等式
int q = 0;
for(int k=i;k<j;k++) //注意k要在區間內移動,利用下述if條件,獲得所有可能值中的最小值,
{
q = cost[i][k] + cost[k+1][j] + num[i]*num[k+1]*num[j+1];
if(q < cost[i][j])
{
cost[i][j] = q;
point[i][j] = k;
}
}
}
}
}
void show(int **p)
{
using namespace std;
for(int i=0;i<SIZE-1;i++)
{
for(int j=0;j<SIZE-1;j++)
{
cout << p[i][j] << '\t';
}
cout << endl;
}
}
void print(int** point,int i,int j)
{
if(i == j)
{
std::cout << "A" << i+1;
}
else
{
std::cout << "(";
print(point,i,point[i][j]); //輸出劃分點左側的完全括號化表達式
print(point,point[i][j]+1,j); //輸出劃分點右側的完全括號化表達式
std::cout << ")";
}
}
/*矩陣鏈乘的方法與括號化方案表達式輸出的方法是同樣的道理*/
matrix matrix_mutiply(int** point,int i,int j,matrix* m)
{
if(i==j)
return m[i];
else
{
matrix m1 = matrix_mutiply(point,i,point[i][j],m);
matrix m2 = matrix_mutiply(point,point[i][j]+1,j,m);
return m1*m2;
}
}
該問題的解決代碼在上述三個文件中。
同時上述代碼也存在一定的問題,關於內存是否洩漏以及代碼是否符合規范,本人不是非常熟悉。望各位大神,看後覺得有什麼問題,直接回復,我也希望提高,畢竟code能力有待提高。
以上問題也屬於典型的動態規劃問題,也屬於遞歸向迭代的轉換。
總結:
應用動態規劃的方法的步驟:
1、刻畫一個最優解的結構特征
2、遞歸地定義最優解的值
3、計算最優解的值,通常采用自底向上的方法
4、利用計算出的信息構造一個最優解的值
可憐,100分還沒人理你,給我吧。
動態規劃問題可以有tD/eD的形式,n^t為問題的大小,n^e為問題所依賴的子問題的大小
1D/1D型,最長上升子序列
2D/0D型,最長公共子序列
2D/1D型,多源最短路徑
2D/2D型,雙背包問題
當然可以有3D/1D或者更高的。
動態規劃題目千變萬化,主要是要學會思考方法,要能看到題目很快找出題目中的狀態,找准狀態後就基本沒有難度了
#include <stdio.h>
#include <limits.h>
#include<stdlib.h>
#define LENGTH 6
void MatrixChainOrder(int p[],int m[][LENGTH],int s[][LENGTH])
{
int n=LENGTH;
int i,j,k,r,t;
for(i=0;i<n;i++)
m[i][i]=0;
for( r=1;r<n;r++)
{
for(i=0;i<n-r;i++)
{
j=i+r;
m[i][j]=m[i][i]+m[i+1][j]+p[i]*p[i+1]*p[j+1];
s[i][j]=i;
for(k=i+1;k<j;k++)
{
t=m[i][k]+m[k+1][j]+p[i]*p[k+1]*p[j+1];
printf("t=%d;,m[%d][%d]=%d\n",t,i,j,m[i][j]);
if(t<m[i][j])
{
m[i][j]=t;
s[i][j]=k;
}
}
}
}
}
int main()
{
int p[] = {30,35,15,5,10,20,25};
int m[LENGTH][LENGTH];
int s[LENGTH][LENGTH];
int i,j,k;
MatrixChainOrder(p,m,s);
printf("最少數乘次數:\n");
for(i = 0;i<LENGTH;i++)
{ for(j = 0 ;j<=i ;j++ )
printf(" ");
for(k = i; k<LENGTH;k++)
printf("%8d",m[i][k]);
printf("\n");
}
printf("斷開位置:\n");
for(i = 0;i<LENGTH;i++)
{ for(j = 0 ;j<=i ;j++ )
printf(" ");
for(k = i; k<LENGTH;k++)
printf("%4d",s[i][k]);
printf("\n");
......余下全文>>