常用演算法思想之動態規劃的區間子集思想
思路:運用動態規劃去解決問題,這個時候子問題並不是屬於父問題的"字首",也不是屬於父問題的"字尾",而是屬於父問題的某個區間之內。
示例
矩陣執行緒
給一個矩陣序列 ABCD,它相乘的方式可以表示為 (ABC)D=AB(CD)=A(BCD)=...
,不同的新增括號方式會導致不同的計算次數,比如
A: 10 × 30 matrix B : 30 × 5 matrix C : 5 × 60 matrix 複製程式碼
那麼
(AB)C = (10×30×5) + (10×5×60) = 1500 + 3000 = 4500 操作 A(BC) = (30×5×60) + (10×30×60) = 9000 + 18000 = 27000 操作 複製程式碼
針對這種現象,如何新增括號才能使得操作次數最少呢?
在輸入中,矩陣用一個數組表示,比如輸入 40 20 30 10 30
表示矩陣A有40行,20列,矩陣B有個20行,30列,矩陣C有30行10列,矩陣D有10行30列
輸入規則為
2//表示總共有兩個輸入 5//下一個要輸入的陣列大小 1 2 3 4 5//陣列的值 3 3 3 複製程式碼
分析如下:
假設有如下形式的矩陣做乘法

如果直接按照順序來計算,先乘A.B,得到的結果再乘C

如果優先運算 B.C ,結果再乘A

可以看到第二種方式消耗的時間會更少。擴充套件到假設有n個矩陣相乘,無論是怎麼新增括號,改變執行順序,最後一定是其它的都計算完畢,只需要計算剩餘的兩個矩陣相乘。假設區分最後兩部分的下標是k,那麼最後一次執行為
要達到最後一步,則需要把兩個部分的結果分別計算出來,假設先計算( ),類推上面的經驗,必定存在一個節點i來劃分得到
可以看到要得到最終問題的解,這樣一層層倒推下來,需要解決類似 這樣的,屬於原始問題的某個區間內子集的問題。
以資料長度為4舉例,即3個矩陣ABC相乘,希望得到最少的計算次數。
最終要計算的結果用dp(0,3),其中0表示輸入的矩陣陣列中的下標為0的位置,3是下標為3的位置,以此表示最終要囊括ABC三個矩陣。
按照上述分析,要計算dp(0,3),它的最後一步有一下兩種劃分方式
- dp(0,1)與dp(1,3),即計算規則為A(BC)
- dp(0,2)與dp(2,3),即計算規則為(AB)C
比較二者那種方式計算最少即可得到最終結果
要得到dp(1,3)則需要知曉dp(1,2)與dp(2,3)的需要最少的次數
當然這裡只需要直接相乘即可
同理計算dp(0,2)
整個過程用圖表示如下:

目標為計算圖中的問號dp(0,3),表格中的橫軸表示開始計算的下標,縱軸表示結束計算的下標,這種表示方式,當橫軸值大於縱軸值時(如座標2,0),可以忽略,不需要計算。根據最後的劃分結果,要得到dp(0,3)先的計算A方案:dp(0,1)與dp(1,3) 或者是 B方案:dp(0,2)與dp(2,3)

A方案的計算用 x 表示計算過,B方案的計算用 o 表示計算過
dp(0,1)和dp(2,3)分表表示一個矩陣,不涉及操作,也就是作為初始值為0
dp(0,2)和dp(1,3)可以分別再劃分為
- dp(0,2):dp(0,1)與dp(1,2),其中dp(0,1)的結果可以複用dp(0,1)
- dp(1,3):dp(1,2)與dp(2,3),其中dp(2,3)的結果可以複用dp(2,3)
特意只說明dp(0,1)和dp(2,3)的複用,是為了表明結果的可複用性,不需要重複計算

再次回顧上述過程
- 目標要得到dp(0,3)
- 需要先計算dp(0,1) dp(1,3) dp(0,2) dp(2,3)
- 為得到2的結果,需要先獲取dp(0,1) dp(1,2) dp(2,3)的結果
- 為得到3,從下標之間的關係可以看出,他們就是初始值,即只要有初始化的過程即可
現在逆向來看(從4到1),計算的過程可以抽象為如下的一個過程

先按照藍線箭頭部分計算對應位置的值,將它儲存起來,然後計算綠線部分的值,它會複用藍線部分的結果,最終得到目標dp(0,3)。
class GFG { public static void main (String[] args) throws IOException{ //處理資料的輸入 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int round=Integer.parseInt(br.readLine()); GFG fgf=new GFG(); for(int i=1;i<=round;i++){ int dataNum=Integer.parseInt(br.readLine()); if(dataNum<=2){ //少於1個矩陣沒有必要計算 System.out.println(0); //記得要讀掉這部分資料,不然順序就亂了 br.readLine(); continue; } int[] arr=new int[dataNum]; int count=0; for(String dataStr:br.readLine().split(" ")){ arr[count++]=Integer.parseInt(dataStr); } int[][] dp=new int[dataNum][dataNum]; for(int L=2;L<dataNum;L++){ //L表示,從start開始後面還有幾個字元,L用來標識從表格左下角到右上角的一個過程 for(int start=0;start<dataNum;start++){ //start 表示開始計算的地方,start表示從表格左上角到右下角的一個過程 int end=start+L; if(end>=dataNum){ //不能超過陣列的長度 end=dataNum-1; } if(end-start<=1){ //賦予初始值 dp[start][end]=0; continue; } int min=Integer.MAX_VALUE; //比較當前所有可能的取值,並獲取最小的值作為子問題的最優解 for(int k=start+1;k<end;k++){ int tempMin=dp[start][k]+dp[k][end]+arr[start]*arr[k]*arr[end]; if(tempMin<min){ min=tempMin; } } dp[start][end]=min; } } System.out.println(dp[0][dataNum-1]); } } } 複製程式碼