摘要:本文先從例子出發,講解動態規劃的一個實際例子,而後再導出動態規劃的《運籌學》定義和通常解法。接着運用《運籌學》中的階段、狀態和狀態轉移方程三個關鍵詞來分析例2的解法。緊接着又給出了《算法導論》中動態規劃的定義和通常解法,並運用《算法導論》中的最優子結構、子問題重疊、自下而上三個關鍵詞來分析例3.並比較了這兩種作法的優劣。最後列舉了幾個例子,並給出了部分實現代碼。適合初學者學習動態規劃。java
問題描述:咱們有面值爲1元、3元和5元的硬幣若干枚,如何用最少的硬幣湊夠11元?算法
問題分析:爲何是湊夠11元,而不是其餘的數目(好比說是10元、9元等等)?這裏將11改爲10元,改變問題的本質嗎?深刻思考一下發現,咱們將問題抽象出來,如何用最少的硬幣湊夠i(i<11)元,i的取值只是決定了問題的解的規模,而沒有改變的問題的本質。因而咱們從i=0開始提及。app
咱們規定:d(i)=j;表示i元至少須要j個硬幣。ide
i=0,函數
顯然只須要0個硬幣能夠湊成0元,即有d(0)=0;學習
i=1,測試
要湊夠一元,咱們只能使用1元的硬幣,即d(1)=d(0)+1=1;spa
I=2,翻譯
要湊夠兩元,咱們仍是隻能使用1元的硬幣,即d(2)=d(1)+1=2;3d
I=3,
要湊夠三元,就有兩種狀況了,咱們既可使用1元的硬幣,同時也可使用3元的硬幣,因而有d(3)=d(3-3)+1=1(使用3元的硬幣);還能夠有:d(3)=d(3-1)+1=d(2)+1=3;經過比較這兩種方法,發現經過使用3元硬幣使得使用的硬幣數目最少,因此d(3)=1;
I=4,
要湊夠4元,可使用3元硬幣,也可使用1元硬幣,若是使用3元硬幣:d(4)=d(4-3)+1=d(1)+1=2;若是使用1元硬幣:d(4)=d(4-1)+1=d(3)+1=2;由此可得d(4)=2;
I=5,
要湊夠5元,
使用1元硬幣:d(5)=d(5-1)+1=d(4)+1=3;
使用3元硬幣:d(5)=d(5-3)+1=d(2)+1=3;
使用5元硬幣:d(5)=d(5-5)+1=d(0)+1=1;
因而有d(5)=1;
到此爲止,咱們能夠找到遞推公式:
( d(i)=min{d(i-vj)+1 | vj<=i && vj屬於{1,3,5}})
…依次類推,能夠推到d (11).
解析:咱們如今回顧是如何解決這個問題的。題目要求咱們求解11元的最小分解時,咱們沒有直接去求解d(11),而是將這個問題分解成了許多相同(或類似)的子問題。而這些子問題的解的方式相同(或類似)。同時注意到,這些子問題的解具備層次性,一個子問題的解須要用到其餘子問題的解。說到這裏,咱們彷佛對動態規劃有了一個初步認識。
《運籌學》給出的定義是在多階段決策問題中,各個階段所採起的決策通常來講是與時間(也多是空間,根據實際狀況而定)相關的,決策依賴於當前的狀態,又隨即引發狀態的轉移,一個決策序列就是在變化的狀態中產生出來的,故有」動態「的含義。所以把處理它的方法稱爲動態規劃方法。說白了動態規劃算法是按照階段將原問題分解成一個一個的狀態, 當前的狀態將由上一次的狀態利用狀態轉移方程推導出。動態規劃主要須要抓住三個關鍵詞:階段、狀態和狀態轉移方程。
1.1 階段
把所給問題的過程,恰當的分解成若干個相互聯繫的階段,以便可以按照必定的次序去求解。例如上面按照須要分解的錢的值(即變量i)來劃分階段。
1.2 狀態
狀態是問題的子問題,大部分狀況下,狀態之間是相關的,且狀態只與前面出現的狀態有關。咱們須要用一張表來保留各個階段的狀態。並且狀態是由底層往上推導的。
1.3狀態轉移方程
如何由前一個狀態推導出後一個狀態,這就須要狀態轉移方程。狀態轉移方程對於全部的子問題來講是通用的。狀態轉移方程是動態規劃的核心內容,它表現瞭如何利用前面的狀態進行決策的過程。
乘熱打鐵,咱們運用上面的方法能夠進行更加深刻的討論了,以上的例1是一維的動態規劃,下面咱們將介紹如何解決二維的動態問題。
問題描述:在M*N個格子裏,每一個格子裝着若干個蘋果,用A[i][j]表示(i,j)位置的蘋果數目。你從左上角的格子開始, 每一步只能向下走或是向右走,每次走到一個格子上就把格子裏的蘋果收集起來, 這樣下去,你最多能收集到多少個蘋果。
問題分析:按照《運籌學》對動態規劃的闡述,咱們主要抓住三點:階段、狀態、狀態轉移方程。題目求解的是最多可以收集到多少個蘋果,沒有走動的步驟限制,即理論上能夠到達任意一個格子的位置。緊接着考慮何時收集的蘋果數目最多?固然是走到(M,N)位置的時候,即右下角時候收集的蘋果數目最多。再接着考慮,怎樣走到右下角?能夠從(M-1,N)的位置往下走一行或者從(M,N-1)的位置往右走一行便可到達(M,N)位置,那麼怎麼取捨呢?固然是選擇這兩種方式中蘋果比較多的一個位置來移動到(M,N)位置。這樣分析而來就能夠很容易知道狀態和狀態轉移方程分別是什麼了。
(a)位置(m,n)表示目前所處的階段;
(b)D(m,n)表示走到(m,n)時可以收集到的最多的蘋果數目,爲狀態;
(c)D(m,n)=max{D(m-1,n)(m>0);D(m,n-1)(n>0)}+A(m,n)爲狀態轉移方程。
根據初始狀態和狀態轉移方程,能夠獲得遞歸版本的解見Program-2-1:遞歸版本(java)。同時咱們提供了一個非遞歸版本的解法,見Program-2-2:非遞歸版本(java)。讀者能夠先看看這二者之間的區別,後面會對遞歸和非遞歸進行詳細的探討。
Program-2-1:遞歸版本(java)
//這裏採用的是遞歸解法。 public static int getMaxApples(int i,int j,int[][] maxGet,int[][] apple){ if(i==0 && j==0) maxGet[i][j]=apple[i][j]; else{ int temp1=0; int temp2=0; if(i>0) temp1=getMaxApples(i-1,j,maxGet,apple); //從上面過來 if(j>0) temp2=getMaxApples(i,j-1,maxGet,apple); //從左面過來 maxGet[i][j]=temp1>temp2? temp1+apple[i][j] : temp2+apple[i][j]; } return maxGet[i][j]; }
Program-2-2:非遞歸版本(java)
//非遞歸版本 public static int getMinNumber_(int total,int[] coins){ int minNumber[]=new int[total+1];//保存每一步驟的結果 minNumber[0]=0;//初始狀態 for(int i=1;i<=total;i++){ minNumber[i]=Integer.MAX_VALUE; for(int j=0;j<coins.length;j++){ if(coins[j]<=i && minNumber[i-coins[j]]+1<minNumber[i]){ minNumber[i]=minNumber[i-coins[j]]+1; } } } return minNumber[total]; }
例2拓展:如今咱們改變一下所求問題。其餘條件不變,求走動K步後可以獲得的最多的蘋果數目。
經過例1和例2的訓練,咱們差很少已經知道這類問題的通常性解法了。主要抓住階段、狀態和狀態轉移方程三個關鍵詞。仔細思考如何將原來的問題按照這三個標準來進行分解。筆者認爲《運籌學》是一本很好的很切合實際的教材,因此將其思想列在前面,後續篇章將從《算法導論》中汲取知識,進行總結。這是本文的重點所在。
問題描述:一個汽車公司在有2條裝配線的工廠內生產汽車,每條裝配線有n個裝配站,不一樣裝配線上對應的裝配站執行的功能相同,可是每一個站執行的時間是不一樣的。在裝配汽車時,爲了提升速度,能夠在這兩天裝配線上的裝配站中作出選擇,便可以將部分完成的汽車在任何裝配站上從一條裝配線移到另外一條裝配線上。裝配過程以下圖所示:
裝配過程的時間包括:進入裝配線時間e、每裝配線上各個裝配站執行時間a、從一條裝配線移到另一條裝配線的時間t、離開最後一個裝配站時間x。舉個例子來講明,如今有2條裝配線,每條裝配線上有6個裝配站,各個時間以下圖所示:
這道題看上去很複雜的樣子,仔細理一理思路,仍是很簡單很基礎的一道動態規劃題目,若是運用《運籌學》的思想,很容易抽出三個關鍵詞。
D[i,j]表示到達第i條裝配線的第j個站點時所需的最短期;那麼它可能由第1條裝配線的第j-1個站點而來,也多是由第2條裝配線的第j-1個站點而來,故狀態轉移方程爲:
D[1,j]=min{D[1,j-1]+a[1,j] , D[2,j-1]+t[2,j-1]+a[1,j]}
D[2,j]=min{D[2,j-1]+a[2,j] , D[1,j-1]+t[1,j-1]+a[2,j]}
初始狀態有:
D[1,1]=9;
D[2,1]=12;
最終狀態有:
Dfinal=min{D[1,n]+x1 , D[2,n]+x2}。
如今,咱們換一種思路,暫時忘掉階段、狀態、狀態轉移方程三個關鍵詞。咱們將引進最優子結構、子問題重疊、自底向上三個關鍵詞。
和分治算法同樣,動態規劃是經過組合子問題[1]的解而解決整個問題的。分值算法是將問題分紅一些獨立的子問題,遞歸的求解各個子問題,而後合併子問題的解而獲得原問題的解。與此不一樣,動態規劃適用於子問題不是獨立的狀況,也就是各子問題包含公共的子子問題[2]。在這種狀況下,若用分治算法則會作出許多沒必要要的工做,即重複的求解公共子問題。動態規劃算法對每一個子子問題只求解一次,將其結果保存在一張表[3]中,從而避免每次遇到子問題時從新計算。
[1] 組合子問題:動態規劃是將原問題的解分解成若干個子問題,那麼這種分解是否須要知足什麼規律?-最優子結構。
[2] 各子問題包含公共的子子問題:即分解產生的若干子問題,他們的子子問題具備公共部分。-子問題重疊。
[3]結果保存在一張表:將每次獲得的一個子問題的解保存在一張表中,這張表自底向上依次構建,下次遇到相同的子問題時,查閱該表便可。-自底向上。
經過上面的解析,咱們下面重點講解一下:最優子結構、子問題重疊和自底向上三個重要的關鍵詞。
1.1 最優子結構
用動態規劃求解的第一步是找出最優子結構。最若是問題的一個最優解包含了子問題的最優解,則該問題具備最優子結構。什麼意思呢?拿例3來講,對於D[1,j]來講,他能夠是前面一個1號線裝配站[1,j-1]過來的,也能夠是2號線裝配站[2,j-1]過來的。有且僅有這兩種選擇。咱們假設選擇了k號線過來的裝配站,那麼對於裝配站來講[k,j-1]也必須是耗時最短的。由於若是存在另一條線路使得[k,j-1]的耗時更短,咱們就選擇更短的那個耗時,而不是原來的那個。(提及來有點拗口,可是原理和貪心算法相似)
1.2 子問題重疊
子問題重疊是動態規劃算法區別於分治算法的重要緣由。子問題重疊的意思是不一樣子問題可能會用到相同的子子問題。例如再計算D[1,6]的時候,他必然會用到D[1,2],計算D[1,5]的時候,他也必然會用到D[1,2],因此子問題D[1,6]和D[1,5]的子問題有重疊。這也是動態規劃可以提升計算效率的本質緣由。
1.3 自底向上
能夠說這個是動態規劃可以有效提升計算效率的關鍵技術所在。(另一種技術是使用備忘錄,也能夠起到相同的效果,詳細請參考《算法導論》)動態規劃建表的順序是自底向上的,由最底層的子問題開始,逐步網上推導,最終獲得原問題的解。
上面介紹了動態規劃的定義,下面簡單講解一下如何運用這種思想來解題。第一步:找到最優子結構。觀察原問題,嘗試修改原問題的規模,好比將11元改爲10元等等,看看原問題是否能夠分解,並且這種分解是否還知足最優子結構性質。如何檢查問題是否知足最優子結構性質,能夠採用「剪貼法」。第二步:判斷子問題是否重疊。第三步:自底向上簡歷表格。咱們獲得例3的動態規劃解法,見Program-3-1.
Program-3-1.非遞歸版(java)
public static void main(String args[]){ /*測試數據 2 4 6 7 9 3 4 8 4 8 5 6 4 5 7 2 3 1 3 4 2 1 2 2 1 3 2 */ Scanner input=new Scanner(System.in); System.out.println("請輸入分別進入裝配線1,2所須要的時間:"); int e1=input.nextInt(); int e2=input.nextInt(); System.out.println("請輸入裝配線的機器數目:"); int N=input.nextInt(); System.out.println("請輸入1.2 裝配線機器加工時間"); int a[][]=new int[2][N]; //每臺機器裝配所需時間 for(int i=0;i<N;i++) a[0][i]=input.nextInt(); for(int i=0;i<N;i++) a[1][i]=input.nextInt(); int t[][]=new int[2][N-1]; //交換裝配線所需的轉移時間 System.out.println("請輸入轉移裝配線所需時間:"); for(int i=0;i<N-1;i++) t[0][i]=input.nextInt(); for(int i=0;i<N-1;i++) t[1][i]=input.nextInt(); System.out.println("請輸入輸出裝配線所需時間:"); int x1=input.nextInt(); int x2=input.nextInt(); //函數主程序 int d[][]=new int[2][N];//存儲到達[i,j]裝配站時的最小時間 int d_final=0; //存儲最終所需最小時間 //初始狀態 d[0][0]=e1+a[0][0]; d[1][0]=e2+a[1][0]; //狀態轉移方程 for(int j=1;j<N;j++){ //修改d[0][j] d[0][j]=d[0][j-1]<d[1][j-1]+t[1][j-1] ? d[0][j-1]+a[0][j] : d[1][j-1]+t[1][j-1]+a[0][j]; //修改d[1][j] d[1][j]=d[1][j-1]<d[0][j-1]+t[0][j-1] ? d[1][j-1]+a[1][j] : d[0][j-1]+t[0][j-1]+a[1][j]; } d_final=d[0][N-1]+x1<d[1][N-1]+x2 ? d[0][N-1]+x1 : d[1][N-1]+x2; System.out.println("所需最小時間爲:"+d_final); }
看到這裏,咱們能夠作一個簡單的總結。
《運籌學》從階段、狀態、狀態轉移方程三個關鍵詞來描述問題、創建模型和解決問題;《算法導論》從最優子結構、子問題重疊、自下而上三個關鍵詞來描述問題、創建模型和解決問題。這二者之間有什麼聯繫和區別呢?仔細想一想就能夠發現確實有一一對應關係。狀態對應子問題,狀態轉移方程對應最優子結構!他們是對同一類問題從不一樣的角度出發去解決問題,都有本身的優缺點。筆者認爲《運籌學》從細節出發,去發現採用什麼樣的粒度將問題分解比較合適,注重問題分解的過程。而《算法導論》一上來就是先將問題分解,而後再思考怎麼將這些分解的問題自底向上合併起來。此外,算法導論還說明了若是問題不知足最優子結構,則不能使用動態規劃,這種說法很是嚴謹,而運籌學沒有強調這個關鍵性問題。
既然《算法導論》更爲嚴謹,那筆者爲何還要介紹《運籌學》呢?由於筆者腦子不夠用啊(= =||),確實如此,若是接觸很少動態規劃,誰可以第一眼就看出最優子結構?(對於接下來筆者要解析的例4和例5,讀者能夠本身嘗試一下)若是咱們按照運籌學的思想,將問題抽絲剝繭,分紅階段、狀態,進而找到狀態轉移方程,豈不是很好的一種入門手段?
因此說,對於初學者來講,能夠先利用《運籌學》的思想,來找到狀態轉移方程(最優子結構),而後再利用《算法導論》思想,講其轉換成非遞歸的自下而上逐層建表的模式。咱們將在例4詳細闡述這種作法。
問題描述:給定n個矩陣構成的一個鏈<A1,A2,A3,…An>,矩陣Ai爲pi-1*pi維數(i=1,2,3,…n),對乘積A1A2A3…An以一種最小化標量乘法次數的方式進行加所有括號。
問題分析:對於n個矩陣相乘,他們是怎麼進行運算的呢?舉個例子來講,當兩個矩陣相乘時,只有一種運算方式,直接相乘便可。當三個矩陣相乘時,能夠先將前兩個矩陣相乘,而後再與第三個矩陣相乘,也能夠先將後兩個矩陣相乘,而後再與第一個矩陣相乘。可見隨着矩陣數量的增長,這種相乘的順序會急劇增加。如何採用一種相乘方式來使得乘法運算總次數最少呢?這麼一分析,咱們已經找到了狀態。
必須注意到這樣一個現象:對於任意的n>1,他均可以分解成兩個矩陣的乘積。具體來講對於矩陣鏈A1A2A3…An來講,它能夠分解成(A1A2A3…Ak)和(Ak+1AK+2Ak+3…An)這兩個矩陣的乘積,其中k=1,2,…n-1.爲了敘述簡便,咱們記
Ai…j表示AiAi+1Ai+2…Aj。
M[i,j]表示Ai…j所需最少的乘法運算次數。
那麼要使得A1…n乘法運算次數最少,這裏的k必須使得A1…k 和 Ak+1…n 乘法運算次數之和加上p0pk-1pn(表示分解成A1…k和Ak+1…n後的兩個矩陣相乘時乘法運算次數)的值最少。要求得最優的M[1,n],則對於分解後的A1…k和 Ak+1…n來講,他們的子分解也必須是最優的。因而有M[1,n]=min{M[1,k]+M[k+1,n]+ p0pk-1pn}(1<=k<n)。這樣咱們就找到了最優子結構,同時也找到了狀態轉移方程。
因而就獲得了遞歸方程:
根據遞歸方程,咱們能夠獲得一個遞歸解(見Program-4-1)。可是須要注意的是,若是用遞歸來解決這個題目,就違背了動態規劃的本質。動態規劃是遞歸,遞歸不必定是動態規劃。這就是動態規劃和遞歸的本質區別。動態規劃強調的是自底向上構建一個表,遇到重疊的子問題,直接查找表格便可,而不是再次的去計算。
那麼如何構建這樣的一個表呢?
圖1.
如圖1所示,咱們從最底層出發,逐層往上求解,便可求得【1,6】的值(見Program-4-2)。
Program-4-1:遞歸版本(java)
public static int getMinSubMuti(int[] P, int start, int end){ //P:矩陣的維數;start:起始矩陣;end:終止矩陣。 if(start==end) return 0; else{ int tempMin=Integer.MAX_VALUE; for(int i=start;i<end;i++) if(getMinSubMuti(P,start,i)+getMinSubMuti(P,i+1,end)+P[start-1]*P[i]*P[end]<tempMin){ tempMin=getMinSubMuti(P,start,i)+getMinSubMuti(P,i+1,end)+P[start-1]*P[i]*P[end]; } return tempMin; } }
Program-4-2:非遞歸版本(java)
public static int getMinMuti_(int[] P){ int M[][]=new int[P.length-1][P.length-1];//存儲最少乘法次數 for(int i=0;i<P.length-1;i++){ M[i][i]=0;//初始狀態 } for(int i=0;i<P.length-2;i++){ M[i][i+1]=P[i]*P[i+1]*P[i+2]; //相鄰的兩個矩陣相乘的乘法次數 } for(int i=2;i<P.length-1;i++){ for(int j=0;j+i<P.length-1;j++){ //求M[j,j+i];表示從第i+1個矩陣到第j+i+1個矩陣 M[j][j+i]=Integer.MAX_VALUE; for(int k=j+1;k<j+i+1;k++){//k表示從第k的矩陣分裂 if(M[j][k-1]+M[k][j+i]+P[j]*P[k]*P[j+i+1]<M[j][j+i]) M[j][j+i]=M[j][k-1]+M[k][j+i]+P[j]*P[k]*P[j+i+1]; } } } return M[0][P.length-2]; }
問題描述:給定兩個序列X=<x1,x2,x3,…,xm>和Y=<y1,y2,y3,…,yn>,找出X和Y的最大長度公共子序列。
問題分析:這道題第一次看到的時候,不知道該怎麼下手。首先咱們來看看最大長度公共子序列的性質:
假設Z=<z1,z2,z3,…,zk>是X和Y的任意一個最大長度公共子序列,則
(1) 若xm= yn ,則有Zk-1是Xm-1和Yn-1的一個最大長度公共子序列。
(2) 若xm yn ,則由zk yn能夠得出Zk是Xm和Yn-1的一個最大長度子序列
(3) 若xm yn ,則由zk xm能夠得出Zk是Xm-1和Yn的一個最大長度子序列
簡單解釋一下這三條性質的含義。
(1) 第一條的意思是若是X和Y的末尾含有相同元素,則此相同元素必定在最長共公共子序列中,那麼X、Y和Z就能夠同時減掉最後一個相同元素。
(2) 第二條和第三條是說若是X和Y的末尾元素不相同,那麼Z的末尾元素不可能同時和xm,yn相等。(即有三種狀況:(a)zk yn,zk=xm,(b)zk xm ,zk yn,(c)zk yn,zk xm)果Z末尾元素不是xm,則能夠將xm從X的末尾移除而不影響結果;一樣的道理適用於Y。
有了這些性質,咱們該怎樣運用呢?即未知變量該如何設置?這個未知變量要可以同時包含X和Y的相關信息,設M[i,j]表示X=< x1,x2,x3,…,xi >和Y=<y1,y2,y3,…,yj>的最大公共子串的長度。咱們將上面的性質翻譯成數學符號:
根據遞歸式,能夠獲得一個遞歸解(見Program-5-1)。
Program-5-1(java)
//遞歸版本的解: public static String getLCS(String X,String Y){ String Z=""; Z=getSubLCS(X,X.length()-1,Y,Y.length()-1); return Z; } public static String getSubLCS(String X,int i,String Y,int j){ if(i==-1 || j==-1) //空的字符串和任意字符串的LCS都是空。 return ""; else if(X.charAt(i)==Y.charAt(j)){ //若是兩個字符串的末尾元素相同,則繼續往頭找LCS,並將相同的元素記錄下來。 return getSubLCS(X,i-1,Y,j-1)+X.charAt(i); } else{ //若是兩個字符串的末尾元素不一樣,則求其分別剪枝後的最長LCS。 String s1=getSubLCS(X,i-1,Y,j); String s2=getSubLCS(X,i,Y,j-1); return s1.length()>s2.length() ? s1 : s2; } }
同時,咱們繼續思考如何構建一個表格來創建自下而上的求解過程。這個表的創建過程相似於例2,如圖2所示。
圖2.
如圖展現了長度爲4的字符串和長度爲3的字符串計算子問題時的順序。由第一層出發,逐層往上計算。具體代碼見Program-5-2
Program-5-2(java)
//非遞歸版本的解: public static String getLCS_(String X,String Y){ int x_length=X.length(); int y_length=Y.length(); int M[][]=new int[y_length+1][x_length+1];//所須要創建的表 int flag[][]=new int[y_length+1][x_length+1];//用來記錄查找過程 for(int i=0;i<=x_length;i++) M[0][i]=0; //初始化第一行的數據爲0; for(int j=0;j<=y_length;j++) M[j][0]=0; //初始化第一列數據爲0; int layer=x_length<y_length?x_length:y_length; for(int k=1;k<=layer;k++){ for(int j=k;j<=x_length;j++){ //掃描第k層的一行 //求M[k,j]:k表示Y中前k個元素,j表示X中前j個元素 if(Y.charAt(k-1)==X.charAt(j-1)){ M[k][j]=M[k-1][j-1]+1; flag[k][j]=2;//表示從左上角過來的 } else{ if(M[k-1][j]>M[k][j-1]){ M[k][j]=M[k-1][j]; flag[k][j]=1; //表示從上面過來的 } else{ M[k][j]=M[k][j-1]; flag[k][j]=0;//表示從左邊過來的 } } } for(int i=k;i<=y_length;i++){//掃描第k層的一列 //求解M[i][k] if(X.charAt(k-1)==Y.charAt(i-1)){ M[i][k]=M[i-1][k-1]+1; flag[i][k]=2;//表示從左上角過來的 } else{ if(M[i-1][k]>M[i][k-1]){ M[i][k]=M[i-1][k]; flag[i][k]=1;//表示從上面過來的 } else{ M[i][k]=M[i][k-1]; flag[i][k]=0;//表示從左邊過來的 } } } } int end_x=y_length; //終點所在的行 int end_y=x_length; //終點所在的列 String z=""; while(end_x!=0 && end_y!=0){ if(flag[end_x][end_y]==2){ z=X.charAt(end_y-1)+z; end_x--; end_y--; } else if(flag[end_x][end_y]==1) end_x--; else end_y--; } return z; }