DAG模型的动态规划学习

来源:互联网 发布:尔雅网络通识课官网 编辑:程序博客网 时间:2024/06/17 21:34

思路

对于白书内容的学习,知道了这一类模型应该都是DAG模型。
有向无环图上的动态规划是学习动态规划的基础。很多问题都可以转化为DAG上的最长路和最短路或计数问题。

问题分析:(以矩形嵌套为例)

矩形之间的“可嵌套”关系是一个典型的二元关系,二元关系可以用图来建模。如果矩形X可以嵌套在Y里面,那么从X到Y就有一条有向边。这个图是无环的,因为一个矩形无法嵌套在自己内部。换句话说,它是一个DAG。这样,所要求的便是DAG上的最长路径。

对于DAG最长(短)路,有两种“对称”的状态定义方式:

1d(i)id(i)=max{d(j)+1|(i,j)E}(1)

2d(i)id(i)=max{d(j)+1|(j,i)E}(2)

仔细分析上面的两种状态定义方式,状态1是一种自顶向下考虑问题的方式。这种考虑问题的角度比较像递归,可以用记忆化搜索实现。他的求解是自底向上。状态2的考虑问题的方式就是它的求解方式,自底向上的考虑并求解。
这也说明了,递归求解和dp求解的区别。前者自顶向下,后者自底向上。


问题1(LIS)

最长上升子序列问题,定义就不说了。需要弄清序列和字串的关系。无非就是在相对顺序不变的情况下。前者不连续后者连续。

题目:[Longest Increasing Subsequence]

问题分析
上升依托于小于关系,小于关系是一个典型的二元关系。可以用图来建模。对于数组中任意两个元素arr[i]与arr[j]( i < j),如果arr[i] < arr[j],那么他们之间存在一条边。又因为自己无法小于自己。所以不存在环。问题规约为DAG上的最长路。需要注意的是i<j的相对顺序不能变。并不是任意两个元素之间只要存在小于关系就有边。

代码(自底向上)

class Solution {public:    int lengthOfLIS(vector<int>& nums) {        int sz = nums.size();        if(!sz) return 0;        std::vector<int> dp(sz, int());        dp[0] = 1; // dp[i]表示以i结尾的最长路 dp[i] = max( dp[j] + 1, (j,i) in E )        int max = dp[0];        for(int i = 1; i < sz; ++i){            dp[i] = 1;            for(int j = 0; j < i; ++j){                if(nums[j] < nums[i]) // (j,i) in E                    dp[i] = std::max(dp[j]+1, dp[i]);            }            max = std::max(dp[i], max);        }        return max;    }};

代码(自顶向下)

class Solution {public:    int lengthOfLIS(vector<int>& nums) {        int sz = nums.size();        if(!sz) return 0;        std::vector<int> dp(sz, int());        dp[sz-1] = 1; // dp[i]表示从i出发的最长路。dp[i] = max( dp[j] + 1, (i,j) in E )        int max = dp[sz-1];        for( int i = sz-2; i >=0; --i ){            dp[i] = 1;            for(int j = i + 1; j < sz; ++j){                if( nums[i] < nums[j] ) // (i,j) in E                    dp[i] = std::max( dp[i], dp[j] + 1 );            }            max = std::max(dp[i], max);        }        return max;    }};

问题2(LDS)

题目:[拦截导弹]

思路

最长下降子序列。其实没区别,都是二元关系。二元关系用图建模。还是DAG模型。可以两种办法。我只采用dp建议的方式,自底向上。

代码

/*测试用例:8186 186 150 200 160 130 197 220*/#include <iostream>#include <algorithm>#include <fstream>//#define LOCALconst int maxn = 100;int arr[maxn];int dp[maxn]; // dp[i]表示以i结尾的最长路 dp[i] = max( dp[j] + 1, (j,i) in E )int cal_lis( int* a, int n );int main( void ){#ifdef LOCAL    std::ifstream cin( "input.dat" );#endif    int n = 0;    while(std::cin >> n){        for( int i = 0; i < n; ++i ){            std::cin >> arr[i];        }        int ans = cal_lis( arr, n );        std::cout << ans << std::endl;    }#ifdef LOCAL    cin.close();#endif    return 0;}int cal_lis( int* a, int n ){    dp[0] = 1;    int max = dp[0];    for( int i = 1; i < n; ++i ){        dp[i] = 1;        for( int j = 0; j < i; ++j ){            if( a[j] >= a[i] ) // (j,i) in E                dp[i] = std::max(dp[i], dp[j]+1);        }        max = std::max( dp[i], max );    }    return max;}

问题3(合唱队形)

题目:[合唱队形]

代码

注意状态的定义,这个题对于lis和lds不是说用任意的一种状态定义都可以解决。而是分别必须用固定的状态才可以解决。

#include <iostream>#include <algorithm>#include <fstream>//#define LOCALconst int maxn = 100;int arr[maxn];int dp1[maxn]; // dp1[i]表示以i结尾的最长路int dp2[maxn]; // dp2[i]表示从i开始的最长路int cal_lis( int* a, int n );int cal_lds( int* a, int n );int main( void ){#ifdef LOCAL    std::ifstream cin( "input.dat" );#endif    int n = 0;    while(std::cin >> n){        for( int i = 0; i < n; ++i ){            std::cin >> arr[i];        }        cal_lis( arr, n );        cal_lds( arr, n );        int max = dp1[0] + dp2[0];        for( int i = 0; i < n; ++i ){            max = std::max( max, dp1[i] + dp2[i] );        }        std::cout << n-max+1 << std::endl;;    }#ifdef LOCAL    cin.close();#endif    return 0;}int cal_lis( int* a, int n ){    dp1[0] = 1;    int max = dp1[0];    for( int i = 1; i < n; ++i ){        dp1[i] = 1;        for( int j = 0; j < i; ++j ){            if( a[j] < a[i] ) // (j,i) in E                dp1[i] = std::max(dp1[i], dp1[j]+1);        }        max = std::max( dp1[i], max );    }    return max;}int cal_lds( int* a, int n ){    dp2[n-1] = 1;    int max = dp2[n-1];    for( int i = n-2; i >= 0; --i ){        dp2[i] = 1;        for( int j = i+1; j < n; ++j ){            if( a[i] > a[j] ) // (j,i) in E                dp2[i] = std::max(dp2[i], dp2[j]+1);        }        max = std::max( dp2[i], max );    }    return max;}

问题4(矩形嵌套)

题目:[nyoj-16]

思路

这个题目需要说明以下。说它时DAG模型没有任何问题。
最上面的思路已经分析了。“可嵌套关系”可以看做是存在一条边的关系。这样原问题构成了DAG模型上的最长路问题。

但是这个题目和LIS有不同,并且很容易混淆。
LDS和LIS都是以及矩形嵌套都是DAG模型,但是前两道本质时同一个问题。但是矩形嵌套和他们是不一样的。
用上述的DP方法无法正确得到正确答案。

当然,其实对于矩形嵌套,简单的一中考虑就是把它当做数字。然后不就是一个LIS问题了嘛?有什么不一样。下面来说一说。
对于LIS和LDS,他们的序列给出之后,顺序是不能变的。比如,[4,1,2,5]这样的序列。最长路只能是1,2,5。但是,如果我要这么问呢?请选出几个数,使得他们的序列时最长路。那你可以选,1,2,4,5。这样最长路是4。当然,如果问题变成这样,那也就失去意义了。对于一个序列,你排序就好了。然后返回序列长度即可。

但是,对于矩形嵌而言则不一样,因为他们并不是和数字一样可以完全比大小。对于数字而言,3和4。要么小于要么大于,但是对于矩形(1,4)和(3,2)而言,不存在这样的一种非黑即白的关系。对于数字而言,4不小于3,那就以意味着3小于4。但是,上面的两个矩形,是互相都无法嵌套的。它是这样的一种性质。

所以,才导致了问题和LIS不完全一样。可以改变原序列当中元素的位置,来获得最长路!此时,我觉得用memo做就挺好的。思路清晰嘛!

代码(dfs-TLE)

#include <iostream>#include <fstream>#include <vector>#include <algorithm>//#define LOCALstruct Rec{    int a_;    int b_;    Rec( int a = 0, int b = 0 ) : a_(a), b_(b)     {}    bool operator<( const Rec& rhs ) const     {        return ( a_ < rhs.a_ && b_ < rhs.b_ )||( a_ < rhs.b_ && b_ < rhs.a_ );    }};int dfs( int i, std::vector< std::vector<int> > edges ){    int n = edges.size();    int ans = 0;    for( int j = 0; j < n; ++j )    {        if( edges[i][j] )            ans = std::max( ans, dfs(j, edges) + 1 );    }    return ans;}int main( void ){#ifdef LOCAL    std::ifstream cin("input.dat");#endif    int t = 0;    std::cin >> t;    while(t--)    {        int n = 0;        std::cin >> n;        std::vector<Rec> Rec_vec;        for( int i = 0; i < n; ++i )        {            int a, b;            std::cin >> a >> b;            Rec r(a, b);            Rec_vec.push_back(r);        }        std::vector< std::vector<int> > edges( n, std::vector<int>(n, int()) );        for( int i = 0; i < n; ++i )        {            for( int j = 0; j < n; ++j )            {                if(i != j && Rec_vec[i] < Rec_vec[j] )                    edges[i][j] = 1;            }        }        int ans = 0;        for( int i = 0; i < n; ++i )        {            ans = std::max( ans, dfs(i, edges) );        }        std::cout << ans+1 << std::endl;    }#ifdef LOCAL    cin.close();#endif    return 0;}

代码1(dfs+memo)

在dfs的基础上加入记忆化过程即可。

#include <iostream>#include <fstream>#include <vector>#include <algorithm>//#define LOCALstruct Rec{    int a_;    int b_;    Rec( int a = 0, int b = 0 ) : a_(a), b_(b)     {}    bool operator<( const Rec& rhs ) const     {        return ( a_ < rhs.a_ && b_ < rhs.b_ )||( a_ < rhs.b_ && b_ < rhs.a_ );    }};int dfs( int i, std::vector< std::vector<int> > edges, std::vector<int>& dp ){    // 有记忆    if(dp[i] > -1)        return dp[i];    // 无记忆    int n = edges.size();    dp[i] = 0;    for( int j = 0; j < n; ++j ){        if( edges[i][j] == 1 )            dp[i] = std::max( dp[i], dfs(j, edges, dp) + 1 );    }    return dp[i];}int main( void ){#ifdef LOCAL    std::ifstream cin("input.dat");#endif    int t = 0;    std::cin >> t;    while(t--)    {        int n = 0;        std::cin >> n;        std::vector<Rec> Rec_vec;        for( int i = 0; i < n; ++i )        {            int a, b;            std::cin >> a >> b;            Rec r(a, b);            Rec_vec.push_back(r);        }        std::vector< std::vector<int> > edges( n, std::vector<int>(n, int()) );        for( int i = 0; i < n; ++i )        {            for( int j = 0; j < n; ++j )            {                if(i != j && Rec_vec[i] < Rec_vec[j] )                    edges[i][j] = 1;            }        }        int ans = 0;        std::vector<int> dp(n, -1);        for( int i = 0; i < n; ++i )        {            ans = std::max( ans, dfs(i, edges, dp) );        }        std::cout << ans+1 << std::endl;    }#ifdef LOCAL    cin.close();#endif    return 0;}

代码1(dfs+memo)

memo做的时候要注意,本质还是深搜的思路。
所以,用递归实现。自顶向下的思路。
采用状态1实现。

#include <iostream>#include <fstream>#include <cstring>#include <vector>#include <algorithm>//#define LOCAL#define N 1000 + 1struct element{    int a_;    int b_;    element(){ std::memset(this, 0, sizeof(element)); }    element( int a, int b ) : a_(a), b_(b) {}    bool operator<( const element& rhs ) const {        return (this->a_ < rhs.a_ && this->b_ < rhs.b_)||(this->a_ < rhs.b_ && this->b_ < rhs.a_);    }};element arr[N];int dp[N]; // dp[i]表示从i出发的最长路 dp[i] = max{ dp[j] + 1 | (i,j) in E }int graph[N][N];void create_graph( int n );int dfs( int i, int n );int main( void ){#ifdef LOCAL    std::ifstream cin("input.dat");#endif    int t = 0;    std::cin >> t;    while(t--){        int n = 0;        std::cin >> n;        for( int i = 0; i < n; ++i ){            int a,b;            std::cin >> a >> b;            arr[i].a_ = a;            arr[i].b_ = b;        }        create_graph(n);        std::memset(dp, 0, sizeof(dp));        int max = dfs(0, n);        for(int i = 1; i < n; ++i){            max = std::max( dfs(i, n), max );        }        std::cout<<max<<std::endl;;    }#ifdef LOCAL    cin.close();#endif    return 0;}void create_graph( int n ){    for( int i = 0; i < n; ++i ){        for( int j = 0; j < n; ++j ){            graph[i][j] = (arr[i] < arr[j])?1:0;        }    }}int dfs( int i, int n ){    if( dp[i] > 0 )        return dp[i];    else{        dp[i] = 1;        for( int j = 0; j < n; ++j ){            if( graph[i][j] )                dp[i] = std::max(dp[i], dfs(j, n) + 1);        }        return dp[i];    }}

思路2

我又学习了其他的方法,确实,问题分析和我想的一样。虽然和LIS都是DAG模型。但是,前者的DP方法。无法直接应用。用传统的memo即可。
不过我看到了一种新的结题思路,原文链接[矩形嵌套]

如果只需要求得最多可以嵌套多少个矩形,而不要求输出序列,定义一个结构体,内含有变量a,b,输入时保证a>b(a为长,b为宽)对a进行排序,最后求b的最长上升子序列(状态转移时要加上A[j].a

代码

转化了问题。好方法。不过,可以看出。问题和我的分析是一致的,正因为不要求元素保证原来位置不变。这样才可以进行排序。

我之所以没有想到这个办法是因为,对于最初的序列。题目的意思是找出尽可能多的,由于矩阵嵌套又不是非黑即白的关系,所以不存在按大小排序之后全是lis。所以,原数组的元素顺序可以改变。

但是,在如何排序的时候我没有想明白。我不知道该怎样的对原始序列进行排序。因为,既然不是非黑即白,那你排序的依据是什么呢?

答案的方法很秒,是按第一个元素先来从小到大排序。注意,第一个元素是较短的那一条边。然后,在这个基础上,问题就转化为在第二条边上的lis问题。这点是非常秒的,非常好的办法。并且,对于第二条边判断的时候,其实 也还是要考虑第一条边,因为要避免相等的情形。也就说它把相等的情形放在这里处理了。这是非常秒的办法。对于前面的排序,如果你处理了相等的情形,那么没法排序了。总之,非常好的办法。

#include <iostream>#include <fstream>#include <cstring>#include <algorithm>#define N 1000//#define LOCALstruct element{    int a_;    int b_;    element(){std::memset(this, 0, sizeof(element));}    element(int a, int b) : a_(a), b_(b) {}    bool operator<( const element& rhs ) const {        if( this->a_ != rhs.a_ ) return this->a_ < rhs.a_;        else return this->b_ < rhs.b_;    }};element arr[N];int dp[N]; // dp[i]表示以i结束的最长路int cal_lis( int n );bool exist_edge( const element& lhs, const element& rhs );int main( void ){#ifdef LOCAL    std::ifstream cin("input.dat");#endif    int t = 0;    std::cin >> t;    while(t--){        int n = 0;        std::cin >> n;        for( int i = 0; i < n; ++i ){            int a, b;            std::cin >> a >> b;            arr[i].a_ = std::min(a,b);            arr[i].b_ = std::max(a,b);        }        std::sort( arr, arr+n );        int ans = cal_lis(n);        std::cout << ans << std::endl;    }#ifdef LOCAL    cin.close();#endif    return 0;}int cal_lis( int n ){    dp[0] = 1;    int max = dp[0];    for( int i = 1; i < n; ++i ){        dp[i] = 1;        for( int j = 0; j < i; ++j ){            if( exist_edge( arr[j], arr[i] ) )                dp[i] = std::max( dp[i], dp[j] + 1 );        }        max = std::max(max, dp[i]);    }    return max;}bool exist_edge( const element& lhs, const element& rhs ){    return ( lhs.a_ < rhs.a_ && lhs.b_ < rhs.b_ )||( lhs.a_ < rhs.b_ && lhs.b_ < rhs.a_ );}

0 0