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]类似,不过记录的是往左开的火车









评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值