[LintCode]大楼轮廓实现及优化

原文发表在我的博客:大楼轮廓实现及优化
求关注、求交流、求意见、求建议。

问题

LintCode:大楼轮廓

描述

水平面上有 N 座大楼,每座大楼都是矩阵的形状,可以用三个数字表示 (start, end, height) ,分别代表其在 x 轴上的起点,终点和高度。大楼之间从远处看可能会重叠,求出 N 座大楼的外轮廓线。
外轮廓线的表示方法为若干三元组,每个三元组包含三个数字 (start, end, height) ,代表这段轮廓的起始位置,终止位置和高度。

样例

给出三座大楼:

[
 [1, 3, 3],
 [2, 4, 4],
 [5, 6, 1]
]

外轮廓线为:

[
 [1, 2, 3],
 [2, 4, 4],
 [5, 6, 1]
]

实现

一开始看到这道题难度是超难我是不相信的,因为看起来很容易找到思路,然而测试用数据却非常大,很难不超时完成,这让我非常感兴趣,用了很久来做这道题,以下是我做这道题的思路,由于用语言描述过于复杂,所以采用动图的方式来描述绘制过程(手机浏览器可能会有些问题,建议用电脑看),拟定输入数据为:

[
 [7,9,5],
 [1,3,3],
 [2,5,4],
 [12,13,2],
 [4,10,2],
 [11,14,4]
]

无序逐个插入

问题分析

大楼的数组并不是有序的,所以首先想到了逐个插入,再根据包含、相交、被包含等不同的情况来绘制逐个绘制轮廓,如下图:

实现 - C++

class Solution {
public:
    vector<vector<int> > buildingOutline(vector<vector<int> > &buildings) {
        vector<vector<int>> result;
        vector<int> buffer1;
        int size = buildings.size();
        for(int j = 0; j < size; j++){
            buffer1 = buildings[j];
            if(result.size() == 0){
                result.push_back(buffer1);
                continue;
            }
            if(result.back()[1] < buffer1[0]){
                result.push_back(buffer1);
                continue;
            }
            if(result.front()[0] > buffer1[1]){
                result.insert(result.begin(), buffer1);
                continue;
            }
            for(int i = 0; i < result.size(); i++){
                if(i > 0){
                    if(result[i -1][1] != result[i][0]){
                        vector<int> buffer2;
                        buffer2.push_back(result[i - 1][1]);
                        buffer2.push_back(result[i][0]);
                        buffer2.push_back(buffer1[2]);
                        result.insert(result.begin() + i, buffer2);
                        continue;
                    }
                }
                if(buffer1[2] > result[i][2]){
                    if(result[i][1] < buffer1[0]){
                        continue;
                    }
                    if(result[i][0] > buffer1[1]){
                        continue;
                    }
                    if(buffer1[0] <= result[i][0] 
                        && buffer1[1] >= result[i][1]){
                        result[i][2] = buffer1[2];
                    }else if(buffer1[0] > result[i][0] 
                        && buffer1[1] >= result[i][1]){
                        vector<int> buffer2;
                        buffer2.push_back(buffer1[0]);
                        buffer2.push_back(result[i][1]);
                        buffer2.push_back(buffer1[2]);
                        result[i][1] = buffer1[0];
                        result.insert(result.begin() + i + 1, buffer2);
                        i++;
                    }else if(buffer1[0] <= result[i][0] 
                        && buffer1[1] < result[i][1]){
                        vector<int> buffer2;
                        buffer2.push_back(result[i][0]);
                        buffer2.push_back(buffer1[1]);
                        buffer2.push_back(buffer1[2]);
                        result[i][0] = buffer1[1];
                        result.insert(result.begin() + i, buffer2);
                        break;
                    }else if(buffer1[0] > result[i][0] 
                        && buffer1[1] < result[i][1]){
                        vector<int> buffer2;
                        buffer2.push_back(result[i][0]);
                        buffer2.push_back(buffer1[0]);
                        buffer2.push_back(result[i][2]);
                        result[i][0] = buffer1[1];
                        result.insert(result.begin() + i, buffer1);
                        result.insert(result.begin() + i, buffer2);
                        break;
                    }
                }
            }
            if(result.back()[1] < buffer1[1]){
                vector<int> buffer2;
                buffer2.push_back(result.back()[1]);
                buffer2.push_back(buffer1[1]);
                buffer2.push_back(buffer1[2]);
                result.push_back(buffer2);
            }
            if(result.front()[0] > buffer1[0]){
                vector<int> buffer2;
                buffer2.push_back(buffer1[0]);
                buffer2.push_back(result.front()[0]);
                buffer2.push_back(buffer1[2]);
                result.insert(result.begin(), buffer2);
            }
        }
        for(int i = 0; i < result.size(); i++){
            if(result[i][0] == result[i][1]){
                result.erase(result.begin() + i);
                i--;
            }
            if(i != 0){
                if(result[i - 1][2] == result[i][2] 
                    && result[i - 1][1] == result[i][0]){
                    result[i][0] = result[i - 1][0];
                    result.erase(result.begin() + i - 1);
                    i--;
                }
            }
        }
        return result;
    }
};

结果分析

  • 结果:非常慢,代码实际写起来远比想象的要复杂,当新大楼与旧轮廓线相交时,需要分块儿遍历旧轮廓线,效率非常低,LintCode 测试数据跑到 53% 时就超时了。
  • 分析:当大楼无序插入时,需要考虑情况非常多,不仅代码复杂容易混乱且执行效率低下,所以也未进行进一步的改正便直接放弃。直接考虑排序后再进行逐个插入,以减少需要考虑的情况。

有序逐个插入

问题分析

由于考虑到无序插入时情况过多且遍历次数过多,导致时间复杂度很高,在数据量大时运行效率过低,所以便打算采用预排序来减少遇到的情况,具体过程如下图:

结果预测

由于再思考这种方法时想到了更高效的方法,所以没有进行代码实现。虽然上图的逻辑看起来比较简单,但是当大量大楼重叠时,需要频繁的修改旧的轮廓,相对于第一种方法来说也只是减少了遍历的次数,但是并不能免于分段遍历旧轮廓,而且判断是否与旧轮廓相交、包括、被包括需要做的对比也比较多。可想而知,此方法也并不能通过测试。

无序逐个插入 - 拆分为单位大楼

问题分析

由于采用 (start, end, height) 三元组的形式存储数据,当无序插入时只能采用遍历判断的方式来分情况讨论,由此导致的逻辑混乱使复杂度很难降低。便想出了将 三元组([1,4,3]) 转化为若干个连续的单位大楼([1,3],[2,3],[3,3])(因为每个单位大楼跨度都为一,所以 end 可以不需要表示了),这样存储时就可以将每栋楼的 start 作为下标。此时便只需要对比每个单位大楼的高度来决定是否修改轮廓,整体过程与第一种方法一致:

实现 - C++

class Solution {
public:
    vector<vector<int> > buildingOutline(vector<vector<int> > &buildings) {
        vector<vector<int>> result;
        vector<int> draft;
        int dSize = 0;
        int bSize = buildings.size();
        for(int i = 0; i < bSize; i++){
            //新加大楼超出draft范围则需要扩容
            int cEnd = buildings[i][1];
            if(dSize < cEnd){
                while(dSize < cEnd){
                    draft.push_back(0);
                    dSize++;
                }
            }
            //更新大楼轮廓
            int cValue = buildings[i][2];
            for(int j = buildings[i][0] - 1; j < cEnd - 1; j++){
                if(draft[j] < cValue){
                    draft[j] = cValue;
                }
            }
        }
        //将单位大楼模式转为三元组格式
        int start = 0;
        int value = 0;
        for(int i = 0; i < dSize; i++){
            if(draft[i] != value){
                if(start < i && value != 0){
                    vector<int> building;
                    building.push_back(start + 1);
                    building.push_back(i + 1);
                    building.push_back(value);
                    result.push_back(building);
                }
                start = i;
                value = draft[i];
            }
        }
        return result;
    }
};

结果分析

  • 结果:成功将 LintCode 测试数据跑到了 77% 。
  • 分析:转化思路后,从代码量上来看,逻辑复杂程度大大降低,拆分大楼后坐标按 vector 坐标存储,提高了随机存取的效率,但是由于拆分存储,空间复杂度大大提高。尽管每一步都是简单的大小对比,数据量较大时仍然会消耗大量的时间,这应该也是不能通过 LintCode 测试的根本原因。

有序插入 - 拆分为左右墙

问题分析

这种方法解决问题的基本思想就是将每座大楼用 左右两面墙表示([1,3],[4,-3]) 替换 三元组表示([1,4,3]),左墙的高度为正,右墙的高度为负,也可以理解为高度的跳跃,因为是从左到右扫描,所以左墙高度升高,右墙高度降低。拆分为墙之后按坐标排序,如果坐标相同则根据高度反向排序,因为优先左墙可以避免同样高且位置相同的两面墙先结束再开始的情况,而且优先更高的墙也可以减少先低墙后高墙是否需要划轮廓的不必要判断。然后从左至右逐个墙面进行扫描,如下图:

实现 - C++

#include <set>
//墙的数据结构
struct JUMP  
{  
    int index; 
    int height;  
    JUMP(int a, int b) : index(a), height(b){}
    //操作符<的定义,用于排序
    bool operator< (const JUMP &j)  const  {
        if(index != j.index){
            return index < j.index;
        }else{
            //相等时由高到底排序
            return height > j.height;
        }
    }
}; 
class Solution {
public:
    vector<vector<int> > buildingOutline(vector<vector<int> > &buildings) {
        //数量提前取出来,避免多次调用浪费时间
        int numOfBuilding = buildings.size();
        int sizeOfJumps = numOfBuilding * 2;
        //拆分为墙存储
        vector<JUMP> jumps;
        for(int i = 0; i < numOfBuilding; i++){
            jumps.push_back(JUMP(buildings[i][0], buildings[i][2]));
            jumps.push_back(JUMP(buildings[i][1], - buildings[i][2]));
        }
        //对墙进行排序
        sort(jumps.begin(), jumps.end());
        //绘制轮廓
        vector<vector<int>> result;
        //没有结束的大楼存起来,multiset插入时会自动排序
        multiset<int> current;
        int prevJump = 0;
        for(int i = 0; i < sizeOfJumps; i++){
            //没有没结束的大楼,添加左墙高度,设置线段起点
            if(current.empty()){
                current.insert(jumps[i].height);
                prevJump = jumps[i].index;
                continue;
            }
            //如果是左墙
            if(jumps[i].height > 0){
                //如果高于没结束的最高的大楼,且线段长度大于0
                if(jumps[i].height > *current.rbegin() 
                && prevJump < jumps[i].index){
                    //画线 设置为起点
                    vector<int> jump;
                    jump.push_back(prevJump);
                    jump.push_back(jumps[i].index);
                    jump.push_back(*(--current.end()));
                    result.push_back(jump);
                    prevJump = jumps[i].index;
                }
                //添加墙
                current.insert(jumps[i].height);
            }else{
                //右墙 而且是存起来最高的大楼并且只有一座 线段长度大于0
                if(jumps[i].height == -*current.rbegin() 
                && current.count(-jumps[i].height) < 2 
                && prevJump < jumps[i].index){
                    //画线 设置为起点
                    vector<int> jump;
                    jump.push_back(prevJump);
                    jump.push_back(jumps[i].index);
                    jump.push_back(*current.rbegin());
                    result.push_back(jump);
                    prevJump = jumps[i].index;
                }
                //大楼结束 删除存储的大楼
                current.erase(current.lower_bound(-jumps[i].height));
            }
        }
        return result;
    }
};

结果分析

  • 结果:经测试 C++ 最快可以 3326ms 通过 LintCode 测试。

  • 分析:由于判断的条件少,并且一次性绘制轮廓不进行修改,效率很高,经受住了大数据量的考验。

  • 细节:

  • 遍历前取出次数,避免重复执行影响遍历效率

  • 数据结构设计有缺陷,左右通过正负判断降低了代码的简洁程度

  • 这道题数据结构的选择很重要,要说的比较多,后面使用单独一节来说明。

数据结构的选择

最终作为选择的容器分别为 vectormultiset

  • vector:由于 vector 属于顺序容器,内部由 数组 实现,随机存取效率高,尾部插入删除效率高,中间插入删除效率低。
  • multiset:与 set 唯一的不同就是可以添加等值元素,都属于关联容器,内部由 红黑树 实现,插入时会自动排序,检索效率高。

同样是排序,墙的排序由于不需要插入一次取一次,所以选择了 vectorstd::sort()vector 的排序是优化过的快排,效率高于 红黑树 的堆排序。
最开始是存数组然后自己写快排排序,然后发现 std::sort() 的排序比手写的要快,所以采用了 vector 搭配 std::sort() 进行墙的存储排序。
相对于墙的存储,未结束墙存储使用的 multiset,由于每次插入后都要取一次最大值,multiset 的堆排序作为插入排序的一种,每次插入一个元素都是有序的,相对于快排的一次成型,更适合存储未结束墙的情况。
起先是打算用 数组存储 + 直接插排 的方式,简直是低估了这道题的难度,最终不得不选用 multiset

总结

因为写两个语言的代码实在是太浪费时间,所以实现就只使用 C++ 了,主要还是看思路。
通过对这道题的执着,学到了不少的东西,最重要的是做题的心态,这里要感谢 @Cicy_Lee 在我不知道难度的情况提问我这道题,如果当时看到这道题难度是超难的话,可能都不会去感兴趣甚至打开,何况是做出来。所以遇到问题还是不要停留在思考一下的程度上,尽量去做一下,因为做起来可能远比想的复杂。
另外,描述很多东西总是免不了用动画来表示, 但是 After Effect 画动图效率忒低了,却又找不到很好的图形化 HTML5 动画模块的制作工具(是模块不是网页),如果谁有很好的工具,求推荐一个。如果实在没有的话,有人有兴趣一起开发一个的话请留言告诉我。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • 标签(空格分隔): STL 运用STL,可以充分利用该库的设计,让我为简单而直接的问题设计出简单而直接的解决方案,...
    认真学计算机阅读 1,470评论 0 10
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,587评论 18 399
  • 教你如何迅速秒杀掉:99%的海量数据处理面试题 本文经过大量细致的优化后,收录于我的新书《编程之法》第六章中,新书...
    Helen_Cat阅读 7,403评论 1 39
  • (一)Java部分 1、列举出JAVA中6个比较常用的包【天威诚信面试题】 【参考答案】 java.lang;ja...
    独云阅读 7,071评论 0 62
  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,724评论 0 33