动态规划 | 悬线法
章零 · 序章
前天洛谷的睿智推荐终于不是天天给我推荐 [模板] 了,然后发现开始全部推荐 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\),向左扩展的时候会有以下情况:
-
\(l_i=1\):说明现在 \(i\) 最左能扩展到边界,那么就不能继续扩展了。
-
\(h_i < h_{i-1}\):说明此时左侧矩形的高度大于当前悬线所在矩形的高度,可以继续扩展。并且显然的是 \(l_i-1\) 能扩展到的最左的位置,\(i\) 也能扩展到,那么我们就直接使 \(l_i=l_{l_i-1}\) 即可。
-
\(h_i > h_{i-1}\):说明此时左侧矩形的高度小于当前矩形,因为要维持悬线高度,所以当前高度不能再继续扩展。
向右扩展同理也分为三种情况:
通过摊还分析,可以证明每个 \(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)\) 开始能到达的最远的左右位置和能向上扩展的最长长度。
那么和上一题相似,我们有:
因为题目是需要求矩形的最大和正方形的最大,于是我们记录两个 \(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;
}
章三 · 尾声
实际上是有很多题适合练习的:
-
洛谷 | P4147 玉蟾宫 [提高+/省选-]:和棋盘制作类似的思想。
-
洛谷 | P1578 奶牛浴场 [提高+/省选-]:悬线法典中典,但是我懒得在这里写了。
-
洛谷 | P2701 Big Barn [普及/提高-]:更为简单的 DP,不过大约有悬线法的部分思路?
-
洛谷 | UVA1619 Feel Good [提高+/省选-]:和 SP1805 相像。
以上题目都能在我的杂题记录中找到(编号从 #161 开始)
节一 · 参考资料
- [1] 悬线法 —— OI-Wiki