DAG(有向无环图)最长路——动态规划求解

1 问题描述

1.1 求整个DAG的最长路径(即不固定起点和终点)

1.2 固定终点,求DAG的最长路径

2 求解问题1

2.1 求解最长路径长度

给定一个有向无环图,怎么求解整个图所有路径中权值之和最大的那条。

如下图所示:

在这里插入图片描述

B->D->F->I就该图的最长路径,长度为9。

令dp[i]表示从i号顶点出发能获得的最长路径长度,这样所有dp[i]中的最大值就是整个DAG的最长路径。

求解dp数组,
如果从i号顶点出发能到达顶点有 就j1,j2,…jk,而dp[j1],dp[j2],…dp[jk]均已知,那么就有
d p [ i ] = m a x { d p [ j ] + l e n g h t [ i j ] ( i , j E ) } dp[i] = max\{dp[j] + lenght[i \longrightarrow j] |(i,j\in E)\} 在这里插入图片描述

如上图所示,需要逆拓扑序列求解dp数组,用递归求解(以邻接矩阵为例):

int DP(int i){
    if(dp[i] > 0) return dp[i];//dp[i]已计算完毕
    for (int j = 0; j < n; ++j)//遍历i的所有出边
    {
        if(G[i][j] != INF){
            dp[i] = max(dp[i], DP(j)+G[i][j]);
        }
    }
    return dp[i];//返回计算完毕的dp[i]
}

从出度为0的顶点出发的最长路径长度为0,因此边界为这些顶点的dp值为0。
但具体实现,可以对整个数组dp初始化为0,这样dp函数当前访问的顶点i的出度为0时,就会返回dp[i]= 0(以此为dp的边界),而出度不为0的顶点则会递归求解,递归过程中遇到已经计算过的顶点,则会直接返回对应的dp值。

2.2 求解最长路径

可以开一个int型数组choice来记录最长路径上顶点的后继顶点。【如果存在多条最长路径,把choice数组改为vector数组】

实现代码如下:

int DP(int i){
    if(dp[i] > 0) return dp[i];//dp[i]已计算完毕
    for (int j = 0; j < n; ++j)//遍历i的所有出边
    {
        if(G[i][j] != INF){
            int temp = DP(j) + G[i][j];//单独计算,防止if调用DP函数两次
            if(temp > dp[i]){
                dp[i] = temp;
                choice[i] = j;
            }
        }
    }
    return dp[i];//返回计算完毕的dp[i]
}

//调用前,要先求出最大的dp[i],然后将i作为路径起点传人
void printPath(int i){
    printf("%d\n", i);
    while(choice[i] != -1){//choice初始化为-1
        i = choice[i];
        printf("->%d\n", i);
    }
}
  • 记录每次决策所选择的策略,然后在dp数组计算完毕之后,根据具体情况进行递归或迭代来获取方案。

2.2.1 路径序列的字典序

模仿字符串来定义路径字典序:
如有两个路径 a1 \longrightarrow a2 \longrightarrow \longrightarrow am与把b1 \longrightarrow b2 \longrightarrow \longrightarrow bn,且a1=b1,a2= b2,……,ak = bk,ak+1<bk+1,那么称路径序列a1 \longrightarrow a2 \longrightarrow \longrightarrow am的字典序小于b1 \longrightarrow b2 \longrightarrow \longrightarrow bn
如果图中有多条最长路径,如何选取字典序最小的那条?只要让遍历i的邻接点的顺序从小到大即可。(上面代码自动实现)

如果令dp[i]表示从顶点i结尾的能获得的最长路径长度,求解公式变成 d p [ i ] = m a x d p [ j ] + l e n g h t [ j i ] , ( i , j E ) dp[i] = max{dp[j] + lenght[j \longrightarrow i] ,(i,j\in E)} (相应的求解顺序变为拓扑序),同样可以求得最长路径长度,但是不能直接得到字典序最小的方案。

在这里插入图片描述
如上图所示,
若dp[i]表示从i号顶点出发的获得的最长路径长度,那么计算dp[1],那么计算dp[1]只需要从V1和V2中选择字典序较小的V2即可;
若dp[i]表示从i号顶点结尾的获得的最长路径长度,且dp[4]、dp[5]已经计算得到,那么计算dp[6]时,如果选择了字典序较小的V4,则会导致错误的选择结果,理论上应该是V1 \longrightarrow V2 \longrightarrow V5最小,却选择了V1 \longrightarrow V3 \longrightarrow V4。

显然,由于字典序的大小总是根据序列中较前的部分来判断,因此序列中越靠前的顶点,其dp值应当越后计算。

3 求解问题2

3.1 求解最长路径长度

给定一个有向无环图,固定终点,求DAG最长路径。

如下图所示:
在这里插入图片描述
如固定H为路径的终点,那么最长路径就会变为B \longrightarrow D \longrightarrow F \longrightarrow H

假设规定终点为T,那么可以令dp[i]表示从顶点i出发到达终点T能获得的最长路径长度。同样的,如果从i号顶点出发能到达顶点有 就j1,j2,…jk,而dp[j1],dp[j2],…dp[jk]均已知,那么就有 d p [ i ] = m a x d p [ j ] + l e n g h t [ i j ] ( i , j E ) dp[i] = max{dp[j] + lenght[i \longrightarrow j] |(i,j\in E)}

与第一问题的区别在,边界应该设置为dp[T] = 0。

如果对整个数组dp赋初值为0,会出问题 。由于从某些顶点出发可能无法到达终点T(如出度为0的点),如果按照第一问的做法会出现错误的结果(如出度为0的顶点会得到0),这从含义上说是错误的。

合适的做法为初始化dp数组作为一个负的大数,来保证“无法达到终点”的含义得到正确表达。然后设置一个vis数组来表示顶点是否已被计算。

实现代码:

int Dp(int i){
    if(vis[i])  return dp[i];
    vis[i] = true;
    for (int j = 0; j < n; ++j)
    {
        if(G[i][j] != INF){
            dp[i] = max(dp[i], Dp(j) + G[i][j]);
        }
    }
    return Dp[i];
}

如果用dp[i]表示以i号结尾能获得的最长路径长度,那么dp[T]就是结果。

3 应用

  • 1 求关键路径
  • 2 如经典的矩形嵌套问题

矩形嵌套问题:给出n个矩形的长和宽,定义矩形的嵌套关系为:如果有两个矩形A和矩形B,其中矩形A的长和宽分别为a、b,矩形B的长和宽分别为c、d,且满足a<c,b<d,或a<d,b<c,则称矩形A可以嵌套与矩形B内。现在要求一个矩形序列,使得这个序列中任意两个相邻的矩形都满足前面的矩形可以嵌套于后一个矩形内,且序列长度最长,如果有多个这样的最长序列,选择矩形编号序列的字典序最小的那个。

这个例子就是典型的DAG最长路径,求解方法为将每个矩形都看成一个顶点,并且将嵌套关系看成顶点之间的有向边,边权均为1,就可以转换为DAG最长路径问题。

3.1 实际例题

问题 A: 矩形嵌套
[命题人 : 外部导入]
时间限制 : 1.000 sec 内存限制 : 64 MB

题目描述
有n个矩形,每个矩形可以用a,b来描述,表示长和宽。矩形X(a,b)可以嵌套在矩形Y(c,d)中当且仅当a<c,b<d或者b<c,a<d(相当于旋转X90度)。例如(1,5)可以嵌套在(6,2)内,但不能嵌套在(3,4)中。你的任务是选出尽可能多的矩形排成一行,使得除最后一个外,每一个矩形都可以嵌套在下一个矩形内。
输入
第一行是一个正正数N(0<N<10),表示测试数据组数,
每组测试数据的第一行是一个正正数n,表示该组测试数据中含有矩形的个数(n<=1000)
随后的n行,每行有两个数a,b(0<a,b<100),表示矩形的长和宽
输出
每组测试数据都输出一个数,表示最多符合条件的矩形数目,每组输出占一行

样例输入 
1
10
1 2
2 4
5 8
6 10
7 9
3 1
5 8
12 10
9 7
2 2
样例输出 
5

参考代码:

#include <cstdio>
#include <algorithm>
#include <cstring>

using std::swap;
using std::fill;
using std::max;

const int MAXN = 1010;
const int INF = -1000000000;
int dp[MAXN];
int G[MAXN][MAXN];
int edge[MAXN][MAXN];
int n;

//求最长路径长度
int Dp(int i){
    if(dp[i] > 0) return dp[i];

    for (int j = 0; j < n; ++j)
    {
        if(G[i][j] != 0){
            int temp = Dp(j) + G[i][j];
            if(temp > dp[i]){
                dp[i] = temp;
            }
        }
    }

    return dp[i];
}

int main(int argc, char const *argv[])
{
    int N;
    scanf("%d", &N);
    while(N--){
        memset(G, 0, sizeof(G));//初始化
        fill(dp, dp + MAXN, 0);

        scanf("%d", &n);

        int a = 0, b = 0, maxIndex;
        for (int i = 0; i < n; ++i)
        {
            scanf("%d%d", &edge[i][0], &edge[i][1]);

            if(edge[i][0] < edge[i][1]){//长的边靠前
                swap(edge[i][0], edge[i][1]);
            }

            if(edge[i][0] > a && edge[i][1] > b){//求最大的矩形,即源点
                a = edge[i][0];
                b = edge[i][1];
                maxIndex = i;
            }

            for (int j = 0; j < i; ++j)//构建有向图
            {
                if(edge[i][0] > edge[j][0] && edge[i][1] > edge[j][1]){//矩形i能嵌套矩形j
                    G[i][j] = 1;
                }else if(edge[i][0] < edge[j][0] && edge[i][1] < edge[j][1]){//矩形j能被矩形i嵌套
                    G[j][i] = 1;
                }
            }
        }
        int ans = Dp(maxIndex);
        printf("%d\n", ans + 1);//顶点数为最长路径长度(边数)+1
    }
    return 0;
}


发布了377 篇原创文章 · 获赞 52 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_33375598/article/details/104456795
今日推荐