[算法学习] 单调栈

单调栈在子矩阵方面的应用

大致的题型
给定你一个\(n\times m\)的矩形,询问有多少个子矩阵,使得这个子矩阵满足一定的条件。

类型1

题目链接:洛谷P1950 长方形

Description

给定你一个\(n\times m\)的矩阵,询问有多少个内部全是\(1\)的子矩阵。
数据范围\(1\le n,m\le 1000\)

Solution

先想如何暴力。
枚举这个子矩阵的两长和两宽,在暴力\(check\)该子矩阵内的所有点是否为\(1\)
复杂度:\(O(n^6)\),期望得分:\(10\)分。
我们发现,这个\(check\)部分可以利用二维前缀和来优化,因此可以预处理\(cnt[i][j]=\sum_{p=1}^{i}\sum_{q=1}^{j}a_{i,j}\)
复杂度:\(O(n^4)\),期望得分:\(30\)分。
考虑框定了该子矩形的左端右端,那么我们可以扫,并进行简单计数即可。
复杂度:\(O(n^3)\),期望得分:\(30\)~\(100\)分。
接下来,我们用:
\(h_{i,j}\)表示\((i,j)\)这个点往上连续\(1\)的最长长度。
\(l_{i,j}\)表示从\((i,j)\)开始第一个满足\(h_{i,l_{i,j}}\le h(i,j)\)的点,如果不存在,令\(l_{i,j}=0\)
\(r_{i,j}\)表示从\((i,j)\)开始第一个满足\(h_{i,r_{i,j}}\le h(i,j)\)的点,如果不存在,令\(r_{i,j}=m+1\)
那么,对于\((i,j)\)为矩形底的贡献,就是\(val=(j-l_{i,j})*(r_{i,j}-j)*h_{i,j}\)
考虑一下,这样做如何保证答案不重不漏。
不重:当且仅当在同一行存在两个数\(l_{i,j_1}=l_{i,j_2}\)并且\(r_{i,j_1}=r_{i,j_2}\)的时候,才有可能算重矩形。
但是这种情况是不存在的,因为\(l_{i,j}\)满足了左边第一个小于等于的,右边第一个小于的,显然无法构造出这种情况。
不漏:对于一个矩形,总有一个\(l_{i,j},r_{i,j}\)能框住一个矩形的两边,故这个矩形一定能被计算到。
我们可以通过一个单调栈来计算\(l_{i,j}\)\(r_{i,j}\),复杂度\(O(n^2)\)

Code

#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;

#define rint register int
const int N = 1005;
bool a[N][N];
int h[N][N], l[N][N], r[N][N], n, m;

stack <int> st;

void push_l(int i, int j) {
  while (!st.empty() && h[i][st.top()] > h[i][j]) r[i][st.top()] = j, st.pop();
  st.push(j);
}
void push_r(int i, int j) {
  while (!st.empty() && h[i][st.top()] >= h[i][j]) l[i][st.top()] = j, st.pop();
  st.push(j);
}

int main() {
  scanf("%d%d", &n, &m);
  for (rint i = 1; i <= n; i++) {
    for (rint j = 1; j <= m; j++) {
      char x = getchar();
      while (x != '.' && x != '*') {
        x = getchar();
      }
      a[i][j] = x == '.';
    }
  }
  for (rint j = 1; j <= m; j++) {
    for (rint i = 1; i <= n; i++) {
      if (a[i][j]) h[i][j] = h[i - 1][j] + 1;
      else h[i][j] = 0;
    }
  }
  long long ans = 0ll;
  for (rint i = 1; i <= n; i++) {
    while (!st.empty()) st.pop();
    for (rint j = 1; j <= m; j++) {
      push_l(i, j);
    } 
    while (!st.empty()) r[i][st.top()] = m + 1, st.pop();
    for (rint j = m; j >= 1; j--) {
      push_r(i, j);
    }
    while (!st.empty()) l[i][st.top()] = 0, st.pop();
    for (rint j = 1; j <= m; j++) {
      ans += 1ll * (j - l[i][j]) * (r[i][j] - j) * h[i][j];
    }
  }
  printf("%lld\n", ans);
  return 0;
}

类型2

题目链接:ZJOI2007 棋盘制作

Description

给定你一个\(n\times m\)的矩阵,询问最大的全\(1\)矩形和正方形的面积。
数据范围\(1\le n,m\le 2000\)

Solution

跟上一题类似,我们用:
\(h_{i,j}\)表示\((i,j)\)这个点往上连续\(1\)的最长长度。
\(l_{i,j}\)表示从\((i,j)\)开始第一个满足\(h_{i,l_{i,j}}\le h(i,j)\)的点,如果不存在,令\(l_{i,j}=0\)
\(r_{i,j}\)表示从\((i,j)\)开始第一个满足\(h_{i,r_{i,j}}\le h(i,j)\)的点,如果不存在,令\(r_{i,j}=m+1\)
那么,对于\((i,j)\)为矩形底的贡献,我们需要求的是 \(max\{(r_{i,j}-l_{i,j}-1)\times h_{i,j}\}\)
正方形的话,只需要求\(max^2 \{min\{r_{i,j}-l_{i,j}-1,h_{i,j}\}\}\)

Code

#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;

#define rint register int
#define ll long long

const int N = 2105;
bool a[N][N];
int h[N][N], l[N][N], r[N][N], n, m;

stack <int> st;

void push_l(int i, int j) {
  while (!st.empty() && h[i][st.top()] > h[i][j]) r[i][st.top()] = j, st.pop();
  st.push(j);
}
void push_r(int i, int j) {
  while (!st.empty() && h[i][st.top()] >= h[i][j]) l[i][st.top()] = j, st.pop();
  st.push(j);
}

pair <ll, ll> solve() {
  for (rint j = 1; j <= m; j++) {
    for (rint i = 1; i <= n; i++) {
      if (a[i][j]) h[i][j] = h[i - 1][j] + 1;
      else h[i][j] = 0;
      l[i][j] = r[i][j] = 0;
    }
  }
  long long ans1 = 0ll, ans2 = 0ll;
  for (rint i = 1; i <= n; i++) {
    while (!st.empty()) st.pop();
    for (rint j = 1; j <= m; j++) {
      push_l(i, j);
    } 
    while (!st.empty()) r[i][st.top()] = m + 1, st.pop();
    for (rint j = m; j >= 1; j--) {
      push_r(i, j);
    }
    while (!st.empty()) l[i][st.top()] = 0, st.pop();
    for (rint j = 1; j <= m; j++) {
      ans1 = max(ans1, 1ll * (r[i][j] - l[i][j] - 1) * h[i][j]);
      ans2 = max(ans2, (long long)min(r[i][j] - l[i][j] - 1, h[i][j]));
    }
  }
  return make_pair(ans1, ans2 * ans2);
}

int main() {
  scanf("%d%d", &n, &m);
  for (rint i = 1; i <= n; i++) {
    for (rint j = 1; j <= m; j++) {
      scanf("%d", &a[i][j]);
      if ((i + j) & 1) {
        a[i][j] ^= 1;
      }
    }
  }
  pair <ll, ll> ans1 = solve();
  for (rint i = 1; i <= n; i++) {
    for (rint j = 1; j <= m; j++) {
      a[i][j] ^= 1;
    }
  }
  pair <ll, ll> ans2 = solve();
  
  printf("%lld\n%lld\n", max(ans1.second, ans2.second), max(ans1.first, ans2.first));
  return 0;
}

类型3

题目链接:区间max

Description

给定一个序列a,求\(max \{ \min(a[l],a[l+1],a[l+2]…a[r]) * (r-l+1) \}\)
数据范围\(1\le n\le 5\times 10^5\)

Solution

在暴力扫的过程中记录当前区间最小值。
复杂度:\(O(n^2)\),期望得分:\(80\)分。
考虑分治进行该过程,假设我们想要知道\([l,r]\)的答案,可以把它拆分成\([l,mid]\)\([mid+1,r]\)和两者合并的答案。
所以可以在分治后随便合并一下,即可通过此题。
复杂度:\(O(nlogn)\),期望得分:\(100\)分。
我们转换思路,考虑\(a_i\)成为\(min\)时,左右最远能拓展到的距离。
显然,这可以用两个单调栈解决。
复杂度:\(O(n)\),期望得分:\(100\)分。
话说这数据造不了太大,该怎么卡\(O(nlogn)\)啊。

Code1(分治)

const int N = 500005;
ll a[N];
int n;

ll dfs(int l, int r) {
  if (l == r) return a[l];
  int mid = (l + r) >> 1;
  ll ans = max(dfs(l, mid), dfs(mid + 1, r));
  ll x = mid, y = mid + 1, h = min(a[x], a[y]);
  ans = max(ans, h << 1);
  while (x > l && y < r) {
    if (a[x - 1] < a[y + 1]) h = min(h, a[++y]);
    else h = min(h, a[--x]);
    ans = max(ans, (ll)(y - x + 1) * h);
  }
  while (x > l) {
    h = min(h, a[--x]);
    ans = max(ans, (ll)(y - x + 1) * h);
  }
  while (y < r) {
    h = min(h, a[++y]);
    ans = max(ans, (ll)(y - x + 1) * h);
  }
  return ans;
}

int main() {
  read(n);
  rep(i, 1, n) scanf("%lld", &a[i]);
  print(dfs(1, n), '\n');
  return 0;
}

Code2(单调栈)

#include <bits/stdc++.h>
using namespace std;

const int N = 500005;
int a[N], L[N], R[N];
int st[N], tp = 0;
int n;

int main() {
  scanf("%d", &n);
  for (int i = 1; i <= n; i++) {
    scanf("%d", &a[i]);
  }
  for (int i = 1; i <= n; i++) {
    while (tp > 0 && a[st[tp]] >= a[i]) tp--;
    L[i] = st[tp] + 1;
    st[++tp] = i;
  }
  tp = 0; st[tp] = n + 1; 
  for (int i = n; i >= 1; i--) {
    while (tp > 0 && a[st[tp]] >= a[i]) tp--;
    R[i] = st[tp] - 1;
    st[++tp] = i;
  }
  
  long long res = 0;
  for (int i = 1; i <= n; i++) {
    res = max(res, (long long)a[i] * (R[i] - L[i] + 1));
  }
  printf("%lld\n", res);
  return 0;
}

单调栈优化dp

题目链接:JSOI2011 柠檬

Description

将一个数列分成若干段,从每一段中选定一个数\(s_0\),假设这个数在此段有\(t\)个,那么这一段价值为\(s_0t^2\),数列的总价值为每一段的价值和。
你需要最大化总价值。
数据范围\(1\le n\le 100000,1\le s_i\le 10000\)

posted @ 2020-03-27 08:18  wlzhouzhuan  阅读(188)  评论(0编辑  收藏  举报