DAG上的動態規劃

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

9.2.1DAG模型

嵌套矩形問題

分析:

矩形之間的可嵌套關係是一個典型的二元關係,二元關係可以用圖來建模,如果矩形X可以嵌套在矩形Y裏,就從X到Y練一條有向邊,這個有向圖是無環的,因爲一個矩形無法套在自己內部,所以他就是一個DAG,所以要求的便是DAG上的最長路徑

銀幣問題

分析:

將每種面值看做一個點,表示“還需要湊足的面值”,則初始狀態爲S,目標狀態爲0,若當前在狀態i,每使用一個銀幣j,狀態便轉移到i-Vj,本題的起點必須爲S,終點必須爲0

9.2.2最長路及其字典序

首先思考嵌套矩形,設d(i)表示從結點i出發的最長路長度,第一步只能走到他的相鄰點,因此

d(i)=max{d(j)+1}

其中j是i的相鄰邊。最終答案是所有d(i)中的最大值。首先把圖先建立出來,假設用鄰接矩陣保存矩陣在G中

記憶化搜索程序

int dp(int i){
    int& ans=d[i];
    if(ans>0) return ans; //d數組以全被初始化爲0
    ans=1;
    for(int j=1lj<=n;j++)
        if(G[i][j])
        ans=max(ans,dp(j)+i);
    return ans;
}
原題還有一個要求:如有多個最優解,矩形編號的字典序應最小

將所有d值計算出來後,選擇最大的d[i]對應的i,如果有對個i,選擇最小的,程序如下

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);
        break;
    }
}
9.2.3固定終點的最長路和最短路

接下來考慮“銀幣問題”。最長路和最短路的求法是類似的,下面只考慮最長路。由於終點固定,d(i)的確切含義變爲“從結點i出發到0結點的最長路徑”

int dp(int S){
    int& ans=d[S];
    if(ans>=0)
        return ans;
    ans=0;
    for(int i=1;i<=n;i++)
        if(S>=V[i])
        ans=max(ans,dp(S-V[i])+1);
    return ans;
}
此程序有個致命的錯誤,就是結點S不一定真的能到達結點 0,所以需用特殊的d[S]表示“無法到達”

int dp(int S){
    int& ans=d[S];
    if(ans!=-1)
        return ans;
    ans=-(1<<30);
    for(int i=1;i<=n;i++)
        if(S>=V[i])
        ans=max(ans,dp(S-V[i])+1);
    return ans;
}
在記憶化搜索中,如果用特殊值表示“還沒算過”,則必須將其和其他特殊值區分開

上述錯誤都是很常見的。另一個解法是用一個數組VIS[i]表示狀態i時候訪問過

int dp(int S){
    if(vis[S]) 
        return d[S];
    vis[S]=1;
    int& ans=d[S];
    ans=-(1<<30);
    for(int i=1;i<=n;i++)
        if(S>=V[i])
        ans=max(ans,dp(S-V[i])+1);
    return ans;
}
本題要求最小,最大兩個值,記憶化搜索必須寫兩個。在這種情況下,用遞推更加方便

minv[0]=maxv[0]=0;
for(int i=1;i<=S;i++){
    minv[i]=INF;
    maxv[i]=-INF;
}
for(int i=1;i<=S;i++)
    for(int j=1;j<=n;j++)
if(i>=V[j]){
    minv[i]=min(minv[i],minv[i-V[j]+1);
    maxv[i]=max(maxv[i],maxv[i-V[j]+1)'
}
printf("%d %d\n",minv[S],maxv[S]);
//輸出字典序最小的方案
void print_ans(int* d,int S){
    for(int i=1;i<=n;i++)
    if(S>=V[i]&&d[S]==d[S-V[i]]+1){
        printf("%d ",i);
        print_ans(d,S-V[i]);
        break;
    }
}
很多用戶喜歡另外一種打印路徑的方法:遞推時直接用min_coin[S]記錄滿足min[S]->min[S-V[i]]+1的最小i

for(int i=1;i<=S;i++)
    for(int j=1;j<=n;j++)
if(i>=V[j]){
    if(min[i]>min[i-V[j]]+i){
        min[i]=min[i-V[j]]+1;
        min_coin[i]=j;
    }
    if(max[i]<max[i-V[j]]+1){
        max[i]=max[i-V[j]]+1;
        max_coin[i]=j;
    }
}
9.2.4小結與應用程序

例題9-1城市裏的間諜

分析:時間是單向流逝的,是一個天然的“序”。影響到決策的只有當前時間和所處的車站,所以可以用d(i,j)表示時刻i,你在車站j,最少還需要等待多長時間

邊界條件是d(T,n)=0,其他d(T,i)爲正無窮

決策1:等一分鐘

決策2:搭乘往右開的車(如果有)

決策3:搭乘往左邊開的車(如果有)

主過程的代碼如下

for(int i=1;i<=n-1;i++)
    dp[T][i]=INF;
dp[T][n]=0;
for(int i=T-1;i>=0;i--)
for(int j=1;j>=n;j++){
    dp[i][j]=dp[i+1][j]+1;//等待一個單位
    if(j<n&&has_train[i][j][0]&&i+t[j]<=T)
        dp[i][j]=min(dp[i][j],dp[i+t[i]][j+1]); //右
    if(j>1&&has_train[i][j][1]&&i+t[j-1]<=T)
        dp[i][j]=min(dp[i][j],dp[i+t[j-1]][j-1]);//左
}
cout<<"Case Number "<<++kase<< ":";
if(dp[0][1]>=INF)
    cout<<"impossible\n";
else
    cout<<dp[0][1]<<"\n";
上面代碼中有一個has_train數組,其中has_train[t][i][0]表示時刻t,在車站i是否有往右開的火車,has_train[t][i][1]類似,不過記錄的是往左開的火車









發佈了38 篇原創文章 · 獲贊 2 · 訪問量 8032
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章