悬线法

什么是悬线法

oiwiki

1
2
3
4
5
6
悬线法的适用范围是单调栈的子集。具体来说,悬线法可以应用于满足以下条件的题目:
- 需要在扫描序列时维护单调的信息;
- 可以使用单调栈解决;
- 不需要在单调栈上二分。

看起来悬线法可以被替代,用处不大,但是悬线法概念比单调栈简单,更适合初学 OI 的选手理解并解决最大子矩阵等问题。
1
2
3
4
5
6
7
8
9
10
11
12
何为悬线法

悬线的定义是这样的:

**从每一个点向上走,知道遇到障碍点或顶边界。**

那么我们可以轻松地得到悬线的一些性质:

1. 每一个点对应一根悬线
2. 每一根悬线都对应了一个高度等于悬线高度,宽度大于0的矩形

所以悬线法的步骤就是:**找出每一个点对应的悬线的高度,然后向左右分别找出该悬线能拓展出的矩形的宽度。**

ps:悬线法本质上也是一种动态规划,因为悬线法的难点就在于对l数组和r数组的动态规划处理。

例题

HISTOGRA - Largest Rectangle in a Histogram

https://www.luogu.com.cn/problem/SP1805

解法一:悬线法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<bits/stdc++.h>
using namespace std;
using u32 = unsigned;
#define i128 __int128;
using ll = long long;
#define int ll
using u64 = unsigned long long;
const ll inf = 1e9;
const ll INF = 1e18;

signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
while(cin>>n,n)
{
vector<int>a(n+1),l(n+1),r(n+1);
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)l[i]=r[i]=i;
for(int i=1;i<=n;i++)while(l[i]>1&&a[i]<=a[l[i]-1])l[i]=l[l[i]-1];
for(int i=n;i>=1;i--)while(r[i]<n&&a[i]<=a[r[i]+1])r[i]=r[r[i]+1];
int maxn=-inf;
for(int i=1;i<=n;i++)
{
int shu=(long long)(r[i]-l[i]+1)*a[i];
maxn=max(maxn,shu);
}
cout<<maxn<<'\n';
}
return 0;
}

解法二: 单调栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include<bits/stdc++.h>
using namespace std;
using u32 = unsigned;
#define i128 __int128;
using ll = long long;
//#define int ll
using u64 = unsigned long long;
const ll inf = 1e9;
const ll INF = 1e18;

signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
while(cin>>n,n)
{
vector<int>l(n+1,0),r(n+1,n+1);
vector<int>a(n+1);
for(int i=1;i<=n;i++)cin>>a[i];
stack<int>stk;
for(int i=1;i<=n;i++)
{
while(stk.size()&&a[i]<a[stk.top()])
{
r[stk.top()]=i;
stk.pop();
}
stk.push(i);
}
stack<int>stk1;
for(int i=n;i>=1;i--)
{
while(stk1.size()&&a[i]<a[stk1.top()])
{
l[stk1.top()]=i;
stk1.pop();
}
stk1.push(i);
}
ll ans=0;
for(int i=1;i<=n;i++)
{
ll shu=1ll*(r[i]-l[i]-1)*a[i];
ans=max(ans,shu);
}
cout<<ans<<'\n';
}

return 0;
}

感觉不错 Feel Good

https://www.luogu.com.cn/problem/UVA1619

这一题不能用STL,不然会超时!!!

注意有多组数据

注意出了最后一组数据,其他数据中间都有一个空行。

数据中有一个数据是全0,注意答案初始要为:

1
2
3
ans = 0,

l = 1 r = 1

这一题的本质就是枚举最小值,将每一个a[i]当作最小值来处理,从i开始把区间向两边扩,使得a[i]是这个区间中的最小值。这样就可以用到悬线法来进行操作了。

解法一:悬线法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
constexpr int N = 100010;
int a[N],l[N],r[N];
ll pre[N];
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int vis=1;
int n;
while(cin>>n)
{
memset(a, -1, sizeof(a));
if(!vis)cout<<'\n';
else vis=0;
ll ans=0;//这个好像必须是0,但是我目前不知道为什么不能设的更小一点
int ansl=1,ansr=1;
for(int i=1;i<=n;i++){
cin>>a[i];
l[i]=r[i]=i;
pre[i]=pre[i-1]+a[i];
}
for(int i=1;i<=n;i++)while(a[l[i]-1]>=a[i])l[i]=l[l[i]-1];
for(int i=n;i>=1;i--)while(a[r[i]+1]>=a[i])r[i]=r[r[i]+1];
for(int i=1;i<=n;i++)
{
ll shu=(pre[r[i]]-pre[l[i]-1])*a[i];
if(shu>ans||(shu==ans&&r[i]-l[i]+1<ansr-ansl+1))
{
ans=shu;
ansr=r[i],ansl=l[i];
}
}
cout<<ans<<'\n'<<ansl<<' '<<ansr<<'\n';
}

return 0;
}

解法二:单调栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include<bits/stdc++.h>
using namespace std;
using u32 = unsigned;
#define i128 __int128;
using ll = long long;
//#define int ll
using u64 = unsigned long long;
const ll inf = 1e9;
const ll INF = 1e18;
const int N=1e5+10;
int a[N],l[N],r[N];
ll pre[N];
int vis;
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
while(cin>>n)
{
if(vis)cout<<'\n';
else vis++;
for(int i=1;i<=n;i++)
{
cin>>a[i];
pre[i]=pre[i-1]+a[i];
}
stack<int>stk,stk1;
for(int i=1;i<=n;i++)
{
l[i]=0,r[i]=n+1;
}
for(int i=1;i<=n;i++)
{
while(stk.size()&&a[i]<a[stk.top()])
{
r[stk.top()]=i;
stk.pop();
}
stk.push(i);
}
for(int i=n;i>=1;i--)
{
while(stk1.size()&&a[i]<a[stk1.top()])
{
l[stk1.top()]=i;
stk1.pop();
}
stk1.push(i);
}
ll maxn=0;
int ansl=1,ansr=1;
for(int i=1;i<=n;i++)
{
ll shu=1ll*(pre[r[i]-1]-pre[l[i]])*a[i];
if(shu>maxn||(shu==maxn&&((r[i]-l[i]-1<ansr-ansl+1)||(r[i]-l[i]-1==ansr-ansl+1&&l[i]+1<ansl))))
{
maxn=shu;
ansl=l[i]+1,ansr=r[i]-1;
}
}
cout<<maxn<<'\n';
cout<<ansl<<" "<<ansr<<'\n';
}
return 0;
}

P4147 玉蟾宫

第一种是悬线法的做法,第二种是单调栈的做法。

这一题的悬线法跟https://www.luogu.com.cn/problem/SP1805的找最大矩形的思路几乎一模一样,只不过这个是对每层分别进行相似的步骤罢了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<bits/stdc++.h>
using namespace std;
using u32 = unsigned;
#define i128 __int128;
using ll = long long;
#define int ll
using u64 = unsigned long long;
const ll inf = 1e9;
const ll INF = 1e18;

signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m;
cin>>n>>m;
vector dp(n+1,vector<int>(m+1));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
char ch;
cin>>ch;
if(ch=='F')dp[i][j]=dp[i-1][j]+1;
}
}
vector<int>l(m+1),r(m+1);
ll maxn=0;
auto ask=[&](int x){
for(int i=1;i<=m;i++)l[i]=r[i]=i;
auto &a=dp[x];
for(int j=1;j<=m;j++)while(l[j]>1&&a[j]<=a[l[j]-1])l[j]=l[l[j]-1];
for(int j=m;j>=1;j--)while(r[j]<m&&a[j]<=a[r[j]+1])r[j]=r[r[j]+1];
for(int i=1;i<=m;i++)
{
int shu=(r[i]-l[i]+1)*a[i];
maxn=max(maxn,1ll*shu);
}
};
for(int i=1;i<=n;i++)ask(i);
cout<<maxn*3<<'\n';
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<bits/stdc++.h>
using namespace std;
using u32 = unsigned;
#define i128 __int128;
using ll = long long;
//#define int ll
using u64 = unsigned long long;
const ll inf = 1e9;
const ll INF = 1e18;
ll maxn;
struct node
{
int len,h;
}a[1010];
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m;
cin>>n>>m;
vector dp(n+1,vector<int>(m+1));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
char ch;
cin>>ch;
if(ch=='F')dp[i][j]=dp[i-1][j]+1;
}
}
auto ask=[&](int x){
memset(a,0,sizeof(a));
stack<node>stk;
for(int j=1;j<=m;j++)
{
int w=0;
while(stk.size()&&stk.top().h>=dp[x][j])
{
w+=stk.top().len;
maxn=max(maxn,1ll*w*stk.top().h);
stk.pop();
}
a[j].h=dp[x][j];a[j].len=w+1;
stk.push(a[j]);
}
int w=0;
while(stk.size())
{
w+=stk.top().len;
maxn=max(maxn,1ll*w*stk.top().h);
stk.pop();
}
};
for(int i=1;i<=n;i++)ask(i);
cout<<maxn*3<<'\n';
return 0;
}

P1169 [ZJOI2007] 棋盘制作

https://www.luogu.com.cn/problem/P1169

这一题还是很正常的悬线法,只不过l和r数组向两边扩的时候加了个限制条件(只有a[x][l[i]-1]!=a[x][l[i]]才能往左扩,向右扩同理。

这一题求最大矩形的面积就正常做就行了,求最大正方形的面积就是在每个矩形里面找到个最大正方形就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include<bits/stdc++.h>
using namespace std;
using u32 = unsigned;
#define i128 __int128;
using ll = long long;
//#define int ll
using u64 = unsigned long long;
const ll inf = 1e9;
const ll INF = 1e18;

signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m;
cin>>n>>m;
vector dp(n+1,vector<int>(m+1,1)),a(n+1,vector<int>(m+1));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>a[i][j];
}
}
for(int i=2;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(a[i-1][j]!=a[i][j])dp[i][j]+=dp[i-1][j];
}
}
ll maxn=0;
ll ret=0;
ll ansl,ansr,ceng,lie;
vector<int>l(m+1),r(m+1);
auto ask=[&](int x){
for(int i=1;i<=m;i++)l[i]=r[i]=i;
auto &h=dp[x];
for(int i=1;i<=m;i++)while(l[i]>1&&a[x][l[i]-1]!=a[x][l[i]]&&h[i]<=h[l[i]-1])l[i]=l[l[i]-1];
for(int i=m;i>=1;i--)while(r[i]<m&&a[x][r[i]+1]!=a[x][r[i]]&&h[i]<=h[r[i]+1])r[i]=r[r[i]+1];
for(int i=1;i<=m;i++)
{
ll shu=(r[i]-l[i]+1)*h[i];
maxn=max(maxn,shu);
ret=max(ret,(ll)pow(min(r[i]-l[i]+1,h[i]),2));
}
};
for(int i=1;i<=n;i++)
{
ask(i);
}
cout<<ret<<'\n'<<maxn<<'\n';

return 0;
}

P2701 [USACO5.3] 巨大的牛棚Big Barn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include<bits/stdc++.h>
using namespace std;
using u32 = unsigned;
#define i128 __int128;
using ll = long long;
//#define int ll
using u64 = unsigned long long;
const ll inf = 1e9;
const ll INF = 1e18;

signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,t;
cin>>n>>t;
vector<vector<int>>a(n+1,vector<int>(n+1)),dp(n+1,vector<int>(n+1));
for(int i=1;i<=t;i++)
{
int x,y;
cin>>x>>y;
a[x][y]=1;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(a[i][j]!=1)dp[i][j]=dp[i-1][j]+1;
}
}
int ret=0;
vector<int>l(n+1),r(n+1);
for(int i=1;i<=n;i++)
{
auto &h=dp[i];
for(int j=1;j<=n;j++)l[j]=r[j]=j;
for(int j=1;j<=n;j++)while(l[j]>1&&h[j]<=h[l[j]-1])l[j]=l[l[j]-1];
for(int j=n;j>=1;j--)while(r[j]<n&&h[j]<=h[r[j]+1])r[j]=r[r[j]+1];
for(int i=1;i<=n;i++)
{
ret=max(ret,min(h[i],r[i]-l[i]+1));
}
}
cout<<ret<<'\n';
return 0;
}

P1387 最大正方形

第一种做法是悬线法,第二种做法是普通的动态规划,虽然悬线法也是动态规划。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<bits/stdc++.h>
using namespace std;
using u32 = unsigned;
#define i128 __int128;
using ll = long long;
//#define int ll
using u64 = unsigned long long;
const ll inf = 1e9;
const ll INF = 1e18;

signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m;
cin>>n>>m;
vector<vector<int>>v(n+1,vector<int>(m+1)),dp(n+1,vector<int>(m+1));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)cin>>v[i][j];
}
vector<int>l(m+1),r(m+1);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(v[i][j]==1)dp[i][j]=dp[i-1][j]+1;
}
}
int ret=0;
for(int i=1;i<=n;i++)
{
auto &a=dp[i];
for(int j=1;j<=m;j++)l[j]=r[j]=j;
for(int j=1;j<=m;j++)while(l[j]>1&&a[j]<=a[l[j]-1])l[j]=l[l[j]-1];
for(int j=m;j>=1;j--)while(r[j]<m&&a[j]<=a[r[j]+1])r[j]=r[r[j]+1];
for(int j=1;j<=m;j++)ret=max(ret,min(a[j],r[j]-l[j]+1));
}
cout<<ret<<'\n';
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<bits/stdc++.h>
using namespace std;
using u32 = unsigned;
#define i128 __int128;
using ll = long long;
//#define int ll
using u64 = unsigned long long;
const ll inf = 1e9;
const ll INF = 1e18;

signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,m;
cin>>n>>m;
vector<vector<int>>v(n+1,vector<int>(m+1)),dp(n+1,vector<int>(m+1));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)cin>>v[i][j];
}
int maxn=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(v[i][j]==1){
dp[i][j]=min({dp[i-1][j],dp[i][j-1],dp[i-1][j-1]})+1;
maxn=max(maxn,dp[i][j]);
}
}
}
cout<<maxn<<'\n';
}