1. 程式人生 > >最大子矩形問題學習筆記

最大子矩形問題學習筆記

最大子矩形問題:在一個給定的矩形網格中有一些障礙點,要找出網格內部不包含任何障礙點,且邊界與座標軸平行的最大子矩形。

懸線法 O(nm)

個人理解:列舉在子矩形底邊上的一個點,將它儘可能地向上擴充套件成一條高線,然後將這條高左右儘可能地平移得到一個矩形,用此矩形更新答案。
列舉的複雜度已經達到了 O(nm),所以我們需要預處理擴充套件和平移操作。

我們可以dp(或者叫遞推也行)預處理:將每個點儘可能向上、向左、向右擴充套件到的位置存在陣列 u[][],l[][],r[][] 中(當然,存向上、向左、向右擴充套件的長度也行,但存位置對求答案來說更方便一點)

For i = 1 to n
    For j = 1 to m
        u[i][j] = (能從(i-1,j)走到(i,j)) ? u[i-1][j] : i;
        l[i][j] = (能從(i,j-1)走到(i,j)) ? l[i][j-1] : j;
    For j = m to 1
        r[i][j] = (能從(i,j+1)走到(i,j)) ? r[i][j+1] : j;

預處理後我們得到了點 (i,j) 的高線。但是對於向左和向右,我們需要知道的不是每個向左向右擴充套件的位置,而是每條高線向左向右擴充套件的位置,這個問題我們可以遞推出來:

For i = 2 to n
    For j = 1 to m
        if 能從(i-1,j)走到(i,j)
            l[i][j] = max(l[i][j], l[i-1][j]
            r[i][j] = min(r[i][j], r[i-1][j]

矩形面積就是 (r[i][j]l[i][j]+1)(iu[i][j]+1)

如此以來,我們就 O(nm) 地解決了這個問題。

單調棧 O(nm)

將每個點儘可能向上、向左、向右擴充套件到的長度

存在陣列 u[][],l[][],r[][] 中。具體做法如下:
For i = 1 to n 列舉矩形底邊,對每一次列舉維護一個單增棧,儲存的資料包括高度與寬度,For j = 1 to m,如果當前點高度大於棧頂元素高度,就直接入棧;否則,不斷彈出棧頂元素,最後將所有彈出元素的寬度之和加上自身寬度作為新的寬度入棧。這樣,我們就得到了一個點向左能擴充套件的最大長度(手動模擬一下就清楚了)。同理,For j = m to 1 就可以求出一個點向右擴充套件的最大長度。(當然,不用反過來for也能求出來,但是反過來for一遍挺方便的,何樂而不為?)
單調棧做法的思想其實和懸線法本質上是一樣的,只不過省去了遞推那一步。

注意:有些題並不是兩種方法都可以的

O(S2)演算法

以奶牛浴場為經典的一類題,詳見論文以及洛谷題解

例題

底邊已經定了,直接考察單調棧
{% fold 展開程式碼 %}

#include<algorithm>
#include<iostream>
#include<cstdio>
#include<stack>

using namespace std;

typedef long long LL;
typedef pair<int, int> pii;
#define mp(x, y) make_pair(x, y)

const int N = 100005;

int n, h[N], len[N];
LL ans;
stack<pii> sta;

int main(){
    while(scanf("%d", &n) && n){
        ans = 0;
        while(!sta.empty()) sta.pop();
        for(int i = 1; i <= n; i++){
            scanf("%d", &h[i]);
            int sumL = 0;
            while(!sta.empty() && h[i] <= sta.top().first){
                sumL += sta.top().second;
                sta.pop();
            }
            len[i] = sumL;
            sta.push(mp(h[i], sumL + 1));
        }
        while(!sta.empty()) sta.pop();
        for(int i = n; i >= 1; i--){
            int sumL = 0;
            while(!sta.empty() && h[i] <= sta.top().first){
                sumL += sta.top().second;
                sta.pop();
            }
            len[i] += sumL;
            sta.push(mp(h[i], sumL + 1));
        }
        for(int i = 1; i <= n; i++){
            len[i]++;
            ans = max(ans, 1ll * len[i] * h[i]);
        }
        printf("%lld\n", ans);
    }
    return 0;
}

{% endfold %}

列舉底邊,按高度排個序,就變成了上一題。
有趣的是,由於排了序,我們不用寫單調棧,運用它的思想即可。
{% fold 展開程式碼 %}

#include<algorithm>
#include<cstdio>
#include<stack>

using namespace std;

const int N = 1005;

int n, m, g[N][N], H[N][N], h[N], len[N], ans;
char c[N][N];

int main(){
    while(scanf("%d%d", &n, &m) != EOF){
        ans = 0;
        for(int i = 1; i <= n; i++){
            scanf("%s", c[i]+1);
            for(int j = 1; j <= m; j++){
                if(c[i][j] == '0')  H[i][j] = 0;
                else    H[i][j] = H[i-1][j] + 1;
            }
        }
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++)
                h[j] = H[i][j];
            sort(h+1, h+m+1);
            for(int j = 1; j <= m; j++){
                if(h[j] == h[j-1])  continue;
                else    ans = max(ans, h[j] * (m - j + 1));
            }
        }
        printf("%d\n", ans);
    }
    return 0;
}

{% endfold %}

懸線法
{% fold 展開程式碼 %}

#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 1005;

int T, n, m, ans, g[N][N], l[N][N], u[N][N], r[N][N];
char ch[10];

int main(){
    scanf("%d", &T);
    while(T--){
        ans = 0;
        scanf("%d%d", &n, &m);
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                scanf("%s", ch);
                g[i][j] = ch[0] == 'F' ? 1 : 0;
                u[i][j] = l[i][j] = r[i][j] = 0;
                if(g[i][j] == 1)
                    u[i][j] = i, l[i][j] = r[i][j] = j;
            }
        }
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                if(g[i][j] == 0)    continue;
                if(i != 1)    u[i][j] = g[i-1][j] == 1 ? u[i-1][j] : i;
                if(j != 1)    l[i][j] = g[i][j-1] == 1 ? l[i][j-1] : j;
            }
            for(int j = m; j >= 1; j--){
                if(g[i][j] == 0)    continue;
                if(j != m)    r[i][j] = g[i][j+1] == 1 ? r[i][j+1] : j;
            }
        }
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                if(g[i][j] == 0)    continue;
                if(g[i][j] == 1 && g[i-1][j] == 1){
                    l[i][j] = max(l[i][j], l[i-1][j]);
                    r[i][j] = min(r[i][j], r[i-1][j]);
                }
                ans = max(ans, (r[i][j] - l[i][j] + 1) * (i - u[i][j] + 1));
            }
        }
        printf("%d\n", ans * 3);
    }
    return 0;
}

{% endfold %}

懸線法
{% fold 展開程式碼 %}

#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 2005;

int n, m, l[N][N], r[N][N], u[N][N], g[N][N], ans;

int main(){
    while(scanf("%d%d", &n, &m) != EOF){
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= m; j++)
                scanf("%d", &g[i][j]);
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= m; j++)
                u[i][j] = l[i][j] = r[i][j] = 0;
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                if(g[i][j] == 0)    continue;
                u[i][j] = g[i-1][j] == 1 ? u[i-1][j] : i;
                l[i][j] = g[i][j-1] == 1 ? l[i][j-1] : j;
            }
            for(int j = m; j >= 1; j--){
                if(g[i][j] == 0)    continue;
                r[i][j] = g[i][j+1] == 1 ? r[i][j+1] : j;
            }
        }
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                if(g[i][j] == 0)    continue;
                if(g[i-1][j] == 1){
                    l[i][j] = max(l[i][j], l[i-1][j]);
                    r[i][j] = min(r[i][j], r[i-1][j]);
                }
                ans = max(ans, (r[i][j] - l[i][j] + 1) * (i - u[i][j] + 1));
            }
        }
        printf("%d\n", ans);
        ans = 0;
    }
    return 0;
}

{% endfold %}

最大子矩形一定要麼全是 a,要麼全是 b,要麼全是 c
假設最大子矩形全是 a,那麼把所有能變成 a 的全變成 a 一定比不變某些字母更好,以此求一次答案;同理,再全變成 b 求一次答案,再全變成 c 求一次答案。
{% fold 展開程式碼 %}

#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 1005;

int n, m, l[N][N], r[N][N], u[N][N], g[N][N], ans;
char c[N][N];

inline void work(char ch){
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            if(ch == 'a' && (c[i][j] == 'a' || c[i][j] == 'w' || c[i][j] == 'y' || c[i][j] == 'z')) g[i][j] = 1;
            else if(ch == 'b' && (c[i][j] == 'b' || c[i][j] == 'w' || c[i][j] == 'x' || c[i][j] == 'z'))    g[i][j] = 1;
            else if(ch == 'c' && (c[i][j] == 'c' || c[i][j] == 'x' || c[i][j] == 'y' || c[i][j] == 'z'))    g[i][j] = 1;
            else    g[i][j] = 0;
            u[i][j] = i, l[i][j] = r[i][j] = j;
        }
    }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            if(g[i][j] == 0)    continue;
            if(i > 1)   u[i][j] = g[i-1][j] == 1 ? u[i-1][j] : i;
            if(j > 1)   l[i][j] = g[i][j-1] == 1 ? l[i][j-1] : j;
        }
        for(int j = m; j >= 1; j--){
            if(g[i][j] == 0)    continue;
            if(j < m)   r[i][j] = g[i][j+1] == 1 ? r[i][j+1] : j;
        }
    }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            if(g[i][j] == 0)    continue;
            if(g[i-1][j] == 1){
                l[i][j] = max(l[i][j], l[i-1][j]);
                r[i][j] = min(r[i][j], r[i-1][j]);
            }
            ans = max(ans, (r[i][j] - l[i][j] + 1) * (i - u[i][j] + 1));
        }
    }
}

int main(){
    while(scanf("%d%d", &n, &m) != EOF){
        for(int i = 1; i <= n; i++)
            scanf("%s", c[i] + 1);
        work('a');
        work('b');
        work('c');
        printf("%d\n", ans);
        ans = 0;
    }
    return 0;
}

{% endfold %}

懸線法
{% fold 展開程式碼 %}

#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;

const int N = 2005;
int n, m, g[N][N], l[N][N], r[N][N], u[N][N], ans1, ans2;

int main(){
    memset(g, -1, sizeof g);
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            scanf("%d", &g[i][j]);
            u[i][j] = i, l[i][j] = r[i][j] = j;
        }
    }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            if(i != 1)  u[i][j] = g[i][j] != g[i-1][j] ? u[i-1][j] : i;
            if(j != 1)  l[i][j] = g[i][j] != g[i][j-1] ? l[i][j-1] : j;
        }
        for(int j = m; j >= 1; j--)
            if(j != m)  r[i][j] = g[i][j] != g[i][j+1] ? r[i][j+1] : j;
    }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            if(i != 1 && g[i][j] != g[i-1][j]){
                l[i][j] = max(l[i][j], l[i-1][j]);
                r[i][j] = min(r[i][j], r[i-1][j]);
            }
            int t1 = r[i][j] - l[i][j] + 1;
            int t2 = i - u[i][j] + 1;
            ans1 = max(ans1, min(t1, t2) * min(t1, t2));
            ans2 = max(ans2, t1 * t2);
        }
    }
    printf("%d\n%d\n", ans1, ans2);
    return 0;
}

{% endfold %}

奶牛浴場

洛谷上的題解 很詳盡
{% fold 展開程式碼 %}

#include<bits/stdc++.h>

using namespace std;

const int N = 5010;

int L, W, n, x, y, ans;

struct Point{
    int x, y;
}p[N];
bool cmp(Point a, Point b){
    if(a.y == b.y)  return a.x < b.x;
    return a.y < b.y;
}
bool cmp1(Point a, Point b){
    return a.x < b.x;
}

int main(){
    scanf("%d%d%d", &L, &W, &n);
    for(int i = 1; i <= n; i++)
        scanf("%d%d", &p[i].x, &p[i].y);
    p[++n] = (Point){0, 0};
    p[++n] = (Point){0, W};
    p[++n] = (Point){L, 0};
    p[++n] = (Point){L, W};
    sort(p+1, p+n+1, cmp1);
    for(int i = 2; i <= n; i++)
        ans = max(ans, (p[i].x - p[i-1].x) * W);
    sort(p+1, p+n+1, cmp);
    for(int i = 1; i <= n; i++){
        int u = 0, d = L;
        for(int j = i + 1; j <= n; j++){
            if(p[j].y == p[i].y)    continue;
            ans = max(ans, (p[j].y - p[i].y) * (d - u));
            if(p[j].x == p[i].x)    u = d = p[j].x;
            else if(p[j].x > p[i].x)    d = min(d, p[j].x);
            else if(p[j].x < p[i].x)    u = max(u, p[j].x);
        }
        ans = max(ans, (W - p[i].y) * (d - u));
    }
    for(int i = n; i >= 1; i--){
        int u = 0, d = L;
        for(int j = i - 1; j >= 1; j--){
            if(p[j].y == p[i].y)    continue;
            ans = max(ans, (p[i].y - p[j].y) * (d - u));
            if(p[j].x == p[i].x)    u = d = p[j].x;
            else if(p[j].x > p[i].x)    d = min(d, p[j].x);
            else if(p[j].x < p[i].x)    u = max(u, p[j].x);
        }
        ans = max(ans, p[i].y * (d - u));
    }
    printf("%d\n", ans);
    return 0;
}

{% endfold %}