紫书第九章-----动态规划初步(DAG上的动态规划之嵌套矩形问题)

本文参考刘汝佳《算法竞赛入门经典》(第2版)

动态规划的核心是状态和状态转移方程

嵌套矩形问题

【题目描述】

有n个矩形,每个矩形可以用a,b来描述,表示长和宽。矩形X(a,b)可以嵌套在矩形Y(c,d)中当且仅当a<c,b<d或者b<c,a<d(相当于旋转X90度)。例如(1,5)可以嵌套在(6,2)内,但不能嵌套在(3,4)中。你的任务是选出尽可能多的矩形排成一行,使得除最后一个外,每一个矩形都可以嵌套在下一个矩形内。如果有多解,矩形编号的字典序应尽量小。

【分析】
矩形之间的”可嵌套”关系是一个典型的二元关系,二元关系可以用图来建模。如果矩形X可以嵌套在矩形Y里,我们就从X到Y连一条有向边。这个有向图是无环的,因为一个矩形无法直接或间接地嵌套在自己的内部。换句话说,它是一个DAG。这样,我们的任务便是求DAG上的最长路径。

下面代码用例用图(为了使图看起来比较清晰,笔者省去了一些边)
这里写图片描述

/*
    状态:d(i)表示从节点(矩形)i出发的最长路长度(节点个数,即矩形个数)
    状态转移方程:d(i)=max{d(j)+1|graph[i][j]=1}
*/

#include<iostream>
#include<cstring>

using namespace std;

const int maxn=1005;

//矩形
typedef struct{
    int length;
    int width;
}rectangle;

rectangle rec[maxn];//矩形数组
int graph[maxn][maxn];//图
int n;//矩形个数

int d[maxn];//d数组初始化为0,因为d[i]至少是1,初始化一个不影响d[i]的非正数即可
int path[maxn];//打印所有路径的时候用

void read(){
    int x,y;
    cin>>n;
    for(int i=0;i<n;i++){
        cin>>x>>y;
        rec[i].length=(x>y?x:y);
        rec[i].width=(x>y?y:x);
    }
}

void createGraph(){
    memset(graph,0,sizeof(graph));
    for(int i=0;i<n;i++){
        for(int j=i+1;j<n;j++){
            if(rec[i].length<rec[j].length && rec[i].width<rec[j].width){
                graph[i][j]=1;
            }
            else if(rec[i].length>rec[j].length && rec[i].width>rec[j].width){
                graph[j][i]=1;
            }
        }
    }
}

//记忆化搜索计算动态转移方程
int dp(int i){
    int &res=d[i];//这里用到这个技巧,对于d[i][j][k][l]等情况实在输入变得太方便了
    if(res>0) return res;//记忆化搜索
    res=1;
    //从i节点出发,若有从i节点出发的边,就递归搜索
    for(int j=0;j<n;j++){
        if(1==graph[i][j]){
            res=max(res,dp(j)+1);//不断更新从i节点出发的最大长度
        }
    }
    return res;
}

//打印字典序最小的方案
void print_ans(int i){
    cout<<i<<" ";
    for(int j=0;j<n;j++){
        if(graph[i][j]==1 && d[j]+1==d[i]){
            print_ans(j);
            break;
        }
    }
}

//打印从i出发的满足最多矩形个数的所有路径
void print_ans2(int i,int cnt){
    path[cnt]=i;
    if(1==d[i]){
        for(int ii=0;ii<=cnt;ii++){
            cout<<path[ii]<<" ";
        }
        cout<<endl;
        return;
    }
    for(int j=0;j<n;j++){
            //下面一定不要少了d[i]==d[j]+1
            //参考图示例,点2和点5之间虽然有边,但不能打印这条路径
            //如果把d[i]==d[j]+1去掉,相当于dfs了
        if(graph[i][j]==1 && d[i]==d[j]+1){
            print_ans2(j,cnt+1);
        }
    }
}

int main()
{
    memset(d,0,sizeof(d));
    read();
    createGraph();

    int ans=1;
    int mark=0;//标记一下取得最大长度的时候对应的下标
    for(int i=0;i<n;i++){
        int tmp=dp(i);
        if(tmp>ans){
            ans=tmp;
            mark=i;
        }
    }
    cout<<ans<<endl<<endl;
    cout<<"字典序最小的路径:";
    print_ans(mark);
    cout<<endl;
    cout<<"按照字典序顺序打印出只满足矩形数最多的路径:"<<endl;
    int all[maxn];//记录所有满足矩形数最多的起点i
    int cnt=0;
    for(int i=0;i<n;i++){
        if(d[i]==ans){
            all[cnt++]=i;
        }
    }
    //打印出共所有满足矩形最多的起点出发的所有路径
    for(int i=0;i<cnt;i++){
        print_ans2(all[i],0);
    }
    return 0;
}

/*

7
1 2
3 4
3 5
4 6
5 6
4 8
7 8

*/

这里写图片描述
【注意上面的记忆化搜索】
如果不用记忆化搜索,也能达到正确的结果,只是计算的次数会迅速增加,耗时太长。时间复杂度是O(n^2)和O(2^n)的区别!!!如下不用记忆化搜索:

int dp(int i){
    d[i]=1;
    //从i节点出发,若有从i节点出发的边,就递归搜索
    for(int j=0;j<n;j++){
        if(1==graph[i][j]){
            d[i]=max(d[i],dp(j)+1);//不断更新从i节点出发的最大长度
        }
    }
    return d[i];
}

这里写图片描述
这个原因还是比较明显的,因为我们的矩形(节点)本身就是按照从0编号开始排列的,并且后续的所有求解过程都是基于这个排列进行的,正着求,显然是字典序最小的路径被首先打印出来。然而,逆着求解的时候,每次向前找最小的值,这样保证了从后往前是依次最小的,但是无法保证从前往后是依次最小的,因而无法保证字典序最小。

猜你喜欢

转载自blog.csdn.net/ccnuacmhdu/article/details/81133637