拉普达记
首发于拉普达记
从一道 Google 面试题说开去

从一道 Google 面试题说开去

随着 GB 第二封信的降临,感觉失去了人生的希望,
我的存在意义也从在自己过题变成尽可能的帮助 Delton (@东仙队长) 过题。
然而这更加困难,Delton 是一位思维十分优秀的选手,
但是因为并不长使用 C++,以及缺少一些算法的基础知识,制约了他的发展。

恰好覃超大魔王前几天空降 Camp,向我们讲述了他是如何和 1 Billion Dollar 失之交臂的。于是想起了一道以前在他的专栏里看到的一道 Google 面试题

————————————————————————————————————
于是首先试图讲解一下栈,这个东西在很多地方都会出现。
(各种 dfs(),求凸包的 Graham Scan 算法,etc ...)

LeetCode 84. Largest Rectangle in Histogram
POJ 2559. Largest Rectangle in a Histogram
POJ 3494. Largest Submatrix of All 1’s

先看 POJ 2559,朴素的做法要枚举所有区间,顺便得到区间最小值 RMQ,O(n2) 显然会超时。考虑极大化思想【1】,仅考察每个极大子矩形。在这道题中,极大子矩形定义为两边都不能向外扩展的子矩形,也就是我们只要能够求出,以 i 位置为最低点时,左右最远分别能够扩展的距离即可。这个正向反向分别求一次,分别求出。

也可以只扫一遍。
const int N = int(1e5) + 9;
stack<int> s; int h[N];
int n;

int main(){

#ifndef ONLINE_JUDGE
      freopen("in.txt", "r", stdin);
    //freopen("out.txt", "w", stdout);
#endif

    while (RD(n)){
        CLR(s); REP_1(i, n) RD(h[i]); h[++n] = 0; s.push(0);
        LL z = 0; REP_1(i, n){
            while (h[i] < h[s.top()]){
                int a = h[s.top()]; s.pop(); int b = i-s.top()-1;
                checkMax(z, (LL)a * b);
            }
            s.push(i);
        }
        cout << z << endl;
    }
}

每次弹出栈的时候,h[s.top()] 记录的是当前的高度,
s.top() 和 i 分别是左右所能扩展到的位置。

有了 POJ 2559 的 O(n) 做法,只要枚举 base line,
就可以在 O(n2) 时间复杂度内解决 POJ 3494 了。
(当然后者也可以直接 DP,参见悬线法【1】)

【1】浅谈用极大化思想解决最大子矩形问题,IOI2003国家集训队论文,王知昆

————————

接下来,看一道加强版的问题:

Programming Problems and Competitions :: HackerRank

这个题需要统计每个 a*b 的子矩形出现了多少次,
需要 O(n2) 算法,显然我们不能考察每个子矩形。

我们依然考察每个极大子矩形,显然一个极大子矩形,会包含很多更小的子矩形。我们用一个数组进行标记,最后再统计一遍,得到所有子矩形出现的次数。

依然考虑枚举 base line,那么对于一个高度为 h 的极大子矩形,
统计阶段每一个高度 1..h 的子矩形都应被计数 1 次。
而对于每个宽度为 w 的极大子矩形,

每个宽度为 w 的子矩形,应该被计数 1 次。
每个宽度为 w-1 的子矩形,应该被计数 2 次。
...
每个宽度为 1 的子矩形,应该被计数 w 次。

(这里似乎与组合数有关,考虑到阶数比较低,可以直接 for 两次)
1 0 0 0 0
1 1 1 1 1
1 2 3 4 5
1 3 6 10 15
1 4 10 20 35
最后考虑判重的问题即可。
当弹出高度为 h 的栈中元素时,其左右两侧较高的部分 max(h[j], h[sta.top()]);
之前已经被统计过了,标记上即可。


(参见
Coffee Central

const int N = int(1.1e3) + 9;

char a[N][N]; int s[N][N], f[N][N];
int h[N], n, m;

int main(){

#ifndef ONLINE_JUDGE
    //freopen("in.txt", "r", stdin);
    //freopen("out.txt", "w", stdout);
#endif

    RD(n, m); REP_1(i, n) RS(a[i]+1);

    h[0] = 0, h[m+1] = 0; REP_1(i, n){
        REP_1(j, m) h[j] = f[i][j] = (a[i][j] == '1' ? f[i-1][j] + 1 : 0);

        stack<int> sta; sta.push(0); REP_1(j, m+1){
            while (h[j] < h[sta.top()]){
                int hr = h[sta.top()]; sta.pop(); int ww = j-sta.top()-1;
                int hl = max(h[j], h[sta.top()]);
                --s[hl][ww]; ++s[hr][ww];
            }
            sta.push(j);
        }
    }

    DWN_1(i, n, 1) DWN_1(j, m, 1) s[i][j-1] += s[i][j];
    DWN_1(i, n, 1) DWN_1(j, m, 1) s[i][j-1] += s[i][j];
    DWN_1(i, n, 1) DWN_1(j, m, 1) s[i-1][j] += s[i][j];

    REP_1(i, n){
        REP_1(j, m) printf("%d ", s[i][j]);
        puts("");
    }
}


练习:

1. SPOJ.com - Problem VISION
考虑数轴的整点处,分布着 n 个建筑,建筑的顶端坐标为 (i, h_i)。
问人站在 (0, y) 时,可以看见多少建筑。
我们将建筑也看成线段,定义「看见」为:
视点到那个建筑顶端的线段,不被其他建筑形成的线段相交。

2. BZOJ 1057. [ZJOI2007]棋盘制作

给定一个 N*M 个正方形的格子组成的矩形纸片,每个格子被涂有黑白两种颜色之一,一个子矩形可以被用作棋盘,当且仅当其中的 0/1 交错出现。

问其中最大的正方形棋盘面积和最大的矩形棋盘面积分别是多少?














——————————————————
练习 1 解答:

此题初看无从下手,如果直接用「看见」的定义,还要写线段相交。
但是如果我们将询问有序化,从高到低依次处理询问,那么所能看到的建筑是不断减少的。对于每个建筑,我们只需要求出其第一次被遮挡的时间(高度),这样对于每个询问,就可以用二分查找,在 O(logn) 的时间复杂度内完成。

于是现在只需要考虑预处理出每个建筑第一次被遮挡的时间。

1. 方法一
我们注意到,所有遮挡事件中最小的时刻,一定发生在某组相邻【1】【2】的建筑中(反证法,考虑所有建筑顶端的连线与 x = 0 的交点)。
【1】Ural 1010. Discrete Function
【2】Pretty Good Proportion - GCJ Final 2015 C

一个朴素的想法是,当一个建筑被遮盖后,将它删除,继续迭代这个过程,我们可以用优先队列或者线段树维护这个过程。

复杂度 O(nlogn)。

2. 方法二
我们发现上面的从左向右依次处理每个建筑,只要维护一个 ↗ 上凸壳即可。(不在上凸壳中的点必然不可能构成遮挡事件)

复杂度 O(n)。

编辑于 2016-02-07

文章被以下专栏收录