DAG模型的动态规划学习

思路

对于白书内容的学习,知道了这一类模型应该都是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建议的方式,自底向上。

代码

/*
测试用例:
8
186 186 150 200 160 130 197 220
*/
#include 
#include 
#include 
//#define LOCAL

const 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 
#include 
#include 
//#define LOCAL

const 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 
#include 
#include 
#include 
//#define LOCAL

struct 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_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 
#include 
#include 
#include 
//#define LOCAL

struct 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_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 
#include 
#include 
#include 
#include 
//#define LOCAL

#define N 1000 + 1
struct 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<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 
#include 
#include 
#include 
#define N 1000
//#define LOCAL

struct 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_ );
}

你可能感兴趣的:(ACM-动态规划,ACM-搜索)