本文参考刘汝佳《算法竞赛入门经典》(第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编号开始排列的,并且后续的所有求解过程都是基于这个排列进行的,正着求,显然是字典序最小的路径被首先打印出来。然而,逆着求解的时候,每次向前找最小的值,这样保证了从后往前是依次最小的,但是无法保证从前往后是依次最小的,因而无法保证字典序最小。