动态规划 | 悬线法

章零 · 序章

前天洛谷的睿智推荐终于不是天天给我推荐 [模板] 了,然后发现开始全部推荐 DP 了,我谢谢你

很高兴,于是就点开了一道,发现是一道悬线法可做的 DP,于是就顺着水了做了一些相关的题目。

做了一些题了,我想也对此大约有了一定的理解了,于是就写一写罢。

章一 · 悬线法

先来参考 OI-Wiki 来介绍一下悬线法。

节一 · 简介

简单来说,悬线法能解决的问题单调栈都能解决,能被单调栈替代,但是她的思想更加简单。

具体来说,悬线法可以满足以下条件的题目:[1]

  • 需要在扫描序列时维护单调的信息;

  • 可以使用单调栈解决;

  • 不需要在单调栈上二分。

节二 · 思想

介绍完了什么是悬线法,下面来简单说一下悬线法到底是用一个何种的思想来解题。

悬线还有一个叫法是垂线,那么这个垂线顾名思义就是一条竖直的线。(废话

一般来说我们需要枚举线的位置 \(i\),在一定的判定下合理向其他方向(一般是上、左、右)扩展信息。

而为了解题,一条线一般带有长度或高度等信息。

可能这里还是比较空泛,那么下面来结合几道简单的例题来看看悬线法是如何实现的。

章二 · 例题与实现

说实话垂线法的经典例题好像是奶牛浴场来着,但是我不会。

其实是不想写题解了

例一 · SP1805

洛谷 | SP1805 HISTOGRA - Largest Rectangle in a Histogram [普及+/提高]

在坐标轴上有 \(n\) 个宽为 \(1\) 的矩形,求包含于这些矩形最大子矩形\(n \le 10^5\),矩形的高度 \(h_i \le 10^9\)

思路简述

用悬线法的话,显然的是我们的线需要记录信息为高度。

同时因为是计算矩形的面积,所以我们左右扩展的目的是最大化矩形的面积,下面来考虑如何扩展一个位置能到达的最左和最右位置。

先来考虑向左扩展,设 \(l_i\) 表示位置 \(i\) 的悬线能扩展到的最左边的位置。那么 \(l_i\) 初始值即为 \(i\),向左扩展的时候会有以下情况:

  1. \(l_i=1\):说明现在 \(i\) 最左能扩展到边界,那么就不能继续扩展了。

  2. \(h_i < h_{i-1}\):说明此时左侧矩形的高度大于当前悬线所在矩形的高度,可以继续扩展。并且显然的是 \(l_i-1\) 能扩展到的最左的位置,\(i\) 也能扩展到,那么我们就直接使 \(l_i=l_{l_i-1}\) 即可。

  3. \(h_i > h_{i-1}\):说明此时左侧矩形的高度小于当前矩形,因为要维持悬线高度,所以当前高度不能再继续扩展。

向右扩展同理也分为三种情况:

\[\begin{cases} r_i=n &, \texttt{Stop} \\ h_i > h_{i+1} &, r_i=r_{r_i+1} \\ h_i < h_{i+1} &, \texttt{Stop} \\ \end{cases} \]

通过摊还分析,可以证明每个 \(l_i\) 最多会被其他的 \(i\) 遍历到一次,因此时间复杂度为 \(O(n)\)[1]

虽然我只会口胡,不知道是具体如何证明的就逝了

Code

先是缺省源,下面的代码就再不放了。

#include <iostream>
#include <stdio.h>
#include <cmath>
#include <algorithm>
#include <cstring>
#define Heriko return
#define Deltana 0
#define Romanno 1
#define S signed
#define LL long long
#define R register
#define I inline
#define CI const int
#define mst(a, b) memset(a, b, sizeof(a))
#define ON std::ios::sync_with_stdio(false);cin.tie(0)
using namespace std;

template<typename J>
I void fr(J &x)
{
    short f(1);
    char c=getchar();
    x=0;
    while(c<'0' or c>'9')
    {
        if(c=='-') f=-1;
        c=getchar();
    }
    while (c>='0' and c<='9') 
    {
        x=(x<<3)+(x<<1)+(c^=48);
        c=getchar();
    }
    x*=f;
}

template<typename J>
I void fw(J x,bool k)
{
    x=(x<0?putchar('-'),-x:x);
    static short stak[35];
    short top(0);
    do
    {
        stak[top++]=x%10;
        x/=10;
    }
    while(x);
    while(top) putchar(stak[--top]+'0');
    if(k) putchar('\n');
    else putchar(' ');
}

template<typename J>
I J Hmax(const J &x,const J &y) {Heriko x>y?x:y;}

CI MXX=1e5+5;

int n,a[MXX],l[MXX],r[MXX];

LL ans;

S main()
{
    fr(n);
    while(n)
    {
        ans=0;
        for(R int i(1);i<=n;++i) fr(a[i]),l[i]=r[i]=i;
        for(R int i(1);i<=n;++i) while(l[i]>1 and a[i]<=a[l[i]-1]) l[i]=l[l[i]-1];
        for(R int i(n);i>=1;--i) while(r[i]<n and a[i]<=a[r[i]+1]) r[i]=r[r[i]+1];
        for(R int i(1);i<=n;++i) ans=Hmax(ans,(LL)(r[i]-l[i]+1)*a[i]);
        fr(n);
    }
    Heriko Deltana;
}

例二 · [ZJOI2007] 棋盘制作

洛谷 | P1169 棋盘制作 [ZJOI2007] [提高+/省选-]

在大小为 \(N \times M\)\(01\) 矩阵中找到最大的 \(01\) 子正方形和最大的 \(01\) 子矩形。\(N,M \le 2000\)

思路简述

还是考虑用悬线去维护并扩展信息。

定义三个扩展信息用的二维数组:l,r,u,分别表示从 \((i,j)\) 开始能到达的最远的左右位置和能向上扩展的最长长度。

那么和上一题相似,我们有:

\[\begin{aligned} l(i,j)&=\max\{l(i,j),l(i-1,j)\} \\ r(i,j)&=\min\{r(i,j),r(i-1,j)\} \\ u(i,j)&=u(i-1,j)+1 \\ \end{aligned} \]

因为题目是需要求矩形的最大和正方形的最大,于是我们记录两个 \(ans\) 即可。

Code

template<typename J>
I J Hmax(const J &x,const J &y) {Heriko x>y?x:y;}

template<typename J>
I J Hmin(const J &x,const J &y) {Heriko x<y?x:y;}

CI MXX=2006;

int n,m,l[MXX][MXX],r[MXX][MXX],u[MXX][MXX],f[MXX][MXX];

int ans[2];

S main()
{
    fr(n),fr(m);
    for(R int i(1);i<=n;++i)
        for(R int j(1);j<=m;++j)
            fr(f[i][j]);
    for(R int i(1);i<=n;++i)
        for(R int j(1);j<=m;++j)
            l[i][j]=r[i][j]=j,
            u[i][j]=1;
    for(R int i(1);i<=n;++i)
        for(R int j(m-1);j>=1;--j)
            if(f[i][j]!=f[i][j+1])
                r[i][j]=r[i][j+1];
    for(R int i(1);i<=n;++i)
        for(R int j(2);j<=m;++j)
            if(f[i][j]!=f[i][j-1])
                l[i][j]=l[i][j-1];
    for(R int i(1);i<=n;++i)
        for(R int j(1);j<=m;++j)
        {
            if(f[i][j]!=f[i-1][j] and i>=2)
            {
                l[i][j]=Hmax(l[i][j],l[i-1][j]);
                r[i][j]=Hmin(r[i][j],r[i-1][j]);
                u[i][j]=u[i-1][j]+1;
            }
            int x(r[i][j]-l[i][j]+1);int y(Hmin(x,u[i][j]));
            ans[0]=Hmax(ans[0],y*y);ans[1]=Hmax(ans[1],x*u[i][j]);
        }
    fw(ans[0],1),fw(ans[1],1);
    Heriko Deltana;
}

章三 · 尾声

实际上是有很多题适合练习的:

以上题目都能在我的杂题记录中找到(编号从 #161 开始)

节一 · 参考资料

posted @ 2021-08-22 10:39  HerikoDeltana  阅读(179)  评论(4编辑  收藏  举报