1. 程式人生 > >動態規劃-巢狀矩陣問題

動態規劃-巢狀矩陣問題

有向無環圖上的動態規劃是學習動態規劃的基礎。很多問題都可以轉化為DAG上的最長路、最短路或路徑計數問題。

巢狀矩陣問題。有n個矩形,每個矩形可以用a,b來描述,表示長和寬。矩形X(a,b)可以巢狀在矩形Y(c,d)中當且僅當a<c,b<d或者b<c,a<d(相當於旋轉X90度)。例如(1,5)可以巢狀在(6,2)內,但不能巢狀在(3,4)中。你的任務是選出儘可能多的矩形排成一行,使得除最後一個外,每一個矩形都可以巢狀在下一個矩形內。如果有多解,矩陣編號的字典序應該儘量小。

矩陣之間的可巢狀關係是一個典型的二元關係,二元關係可以用圖來建模。如果矩陣X可以巢狀在矩陣Y裡,就從X到Y連一條有向邊。這個有向圖是無環的,因為一個矩陣無法直接或間接地巢狀在自己內。換句話說,它是一個DAG。這樣,所要求的便是DAG上的最長路徑。

如何求DAG中不固定起點的最長路徑呢?仿照數字三角形的做法,設d(i)表示從結點i出發的最長路長度,應該如何寫狀態轉移方程呢?第一步只能到它的相鄰點。因此:

d(i) = max \left \{ d(j) + 1|(i,j)\in E \right \}

其中,E為邊集。最終答案是所有d(i)中的最大值。根據前面的介紹,可以嘗試按照遞推或記憶化搜尋的方式計算上式。不管怎樣,都需要先把圖建立出來,假設用鄰接矩陣儲存在矩陣G中(在編寫主程式之前需測試和除錯程式,以確保建圖過程正確無誤)。接下來編寫記憶化搜尋程式(呼叫前無需初始化d陣列的所有值為0):

int G[10][10],d[100],n;
int dp(int i)
{
    /*d(i) = max{d(j)+1}
    如果矩形X可以巢狀在矩形Y中 就從X到Y連一條有向邊 這個有向圖是無環的
    因為矩形無法間接或直接巢狀到自己內部*/
    int &ans=d[i],j; //d[i]為從節點i出發的最長路長度
    if(ans>0) //長度不可以為0 d需要初始化為0
        return ans;
    ans=1;
    for(j=1;j<=n;j++)
        if(G[i][j])
           ans=max(ans,dp(j)+1);
    return ans;
}

這裡用到了一個技巧:為表項d[i]宣告一個引用ans。這樣,任何對ans的讀寫實際上都是在對d[i]進行。當d[i]換成d[i][j][k][l][m][n]這樣很長的名字時,該技巧的優勢就會很明顯。

原題還有一個要求:如果有多個最優解,矩形編號的字典序應最小。將所有d值計算出來以後,選擇最大d[i]所對應的i。如果有多個i,選擇最小的i,這樣才能保證字典序最小。接下來可以選擇d(i)=d(j) + 1且 (i,j)\in E的任何一個j。為了讓方案的字典序最小,應選擇其中最小的j。

void print_ans(int i)
{
    //列印字典序最小的解
   printf("%d ",i);
   for(int j=1;j<=n;j++)
        if(G[i][j]&&d[i]==d[j]+1)
        {
            print_ans(j);//找到一個滿足d[i]==d[j]+1 的節點j後就應該立刻遞迴
            break;       //列印從j開始的路徑,在遞迴結束後退出迴圈
        }
}

注意,當找到一個滿足 d[i]==d[j]+1的結點j後就應立刻遞迴列印從j開始的路徑,並在遞迴返回後退出迴圈。如果要列印所有方案,只把break語句刪除是不夠的(列印完一條路徑後,接著列印的是另一條路徑的最後一個結點,是不正確的)。正確的方法是記錄路徑上的所有點,在遞迴結束時才一次性輸出整條路徑。

有趣的是,如果把狀態定義成 “d(i)表示結點i為終點的最長路徑長度”,也能順利求出最優值,卻難以打印出字典序最小的方案(當前結點j,選擇d[j]對應的i,如果有多個i,選擇最小的i。問題是此時最小不一定是全域性最小。全域性有卻可能最大,比如第一個結點是最大的)