DP专题-学习笔记:悬线法 DP
1. 前言
悬线法 DP,是一种 DP,用来处理矩阵类问题。
这种 DP 一般处理的问题长这样:
给出一个 \(n \times m\) 的矩阵,问满足条件的最大子矩阵的面积是多少?
当然也可以问边长之类的。
这类问题通常有非悬线法 DP 的解法,但是悬线法 DP 往往能够减小思维量,减小出错率。
2. 详解
例题:P1387 最大正方形
这道题有两种方法:普通 DP 与 悬线法 DP。
普通 DP?
设 \(f_{i,j}\) 表示处理到 \(a_{i,j}\) 时的最大值,那么有转移方程:
转移方程应该还是好想的吧。
那么普通 DP 以优秀的表现通过了这道题。
那么悬线法 DP 呢?
悬线法 DP 的一般思路就是:处理出每一个点向左(\(l_{i,j}\)),向右(\(r_{i,j}\))能够扩展的 位置,以及向上(\(Up_{i,j}\))能够扩展的 距离。
什么意思呢?看下面这张图。
这样做有什么用处吗?
好处就是:如果我们以 \((i,j)\) 为矩形的底边,那么这个最大子矩形实际上就已经确定了!
因为我们知道往左边能最多扩展多少,往右边最多扩展多少,往上面还能够扩展多少,如图:
那么首先我们要预处理一下 \(l,r,Up\),递推式如下:
转移条件:相邻两个全部都符合题意,也就是可以作为一个子矩阵。
初始值:如果 \(a_{i,j}=1\),\(l_{i,j}=r_{i,j}=j,Up_{i,j}=1\)。
这部分的代码如下:
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if (a[i][j] == 1) l[i][j] = r[i][j] = j, Up[i][j] = 1;//初始化
for (int i = 1; i <= n; ++i)
for (int j = 2; j <= m; ++j)
if (a[i][j] && a[i][j - 1]) l[i][j] = l[i][j - 1];//l
for (int i = 1; i <= n; ++i)
for (int j = m - 1; j >= 1; --j)
if (a[i][j] && a[i][j + 1]) r[i][j] = r[i][j + 1];//r
for (int i = 2; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if (a[i][j] && a[i - 1][j]) Up[i][j] = Up[i - 1][j] + 1;//Up
然后如何确定最大子矩阵呢?
如果你的想法是直接用 \(l_{i,j},r_{i,j},Up_{i,j}\) 推答案,那么考虑一下下面这张图:
在上图中,真正的矩形是橙色的矩形,但是如果你直接推答案就会变成黑色的矩形,显然答案是错的。
因此我们要对 \(l,r\) 做一点修改。
考虑一下就会发现,\(l,r\) 就是从能够到达最上面的点,到这个点中原先 \(l,r\) 中的最大/最小值。
那么递推式就是这样:
需要注意第一行不能递推。
答案:
如果是求矩形的面积,就是 \(\max\{(r_{i,j}-l_{i,j}+1) \times Up_{i,j}|i \in [1,n],j \in [1,m]\}\)
但是因为这道题球的是正方形的边长,\(r_{i,j}-l_{i,j}+1\) 和 \(Up_{i,j}\) 取最小值即可。
这一部分的代码:
for (int i = 1; i <= n; ++i)
for(int j = 1; j <= m; ++j)
{
if ((i ^ 1) && a[i][j] != 0 && a[i - 1][j] != 0)//特别注意第一行不能转移
{
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, Min(Up[i][j], r[i][j] - l[i][j] + 1));
}
总代码:
/*
========= Plozia =========
Author:Plozia
Problem:P1387 最大正方形
Date:2021/3/13
========= Plozia =========
*/
#include <bits/stdc++.h>
typedef long long LL;
const int MAXN = 100 + 10;
int n, m, a[MAXN][MAXN], l[MAXN][MAXN], r[MAXN][MAXN], Up[MAXN][MAXN], ans;
int read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return (fh == 1) ? sum : -sum;
}
int Max(int fir, int sec) {return (fir > sec) ? fir : sec;}
int Min(int fir, int sec) {return (fir < sec) ? fir : sec;}
int main()
{
n = read(), m = read();
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
a[i][j] = read();
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if (a[i][j] == 1) l[i][j] = r[i][j] = j, Up[i][j] = 1;//初始化
for (int i = 1; i <= n; ++i)
for (int j = 2; j <= m; ++j)
if (a[i][j] && a[i][j - 1]) l[i][j] = l[i][j - 1];//l
for (int i = 1; i <= n; ++i)
for (int j = m - 1; j >= 1; --j)
if (a[i][j] && a[i][j + 1]) r[i][j] = r[i][j + 1];//r
for (int i = 2; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if (a[i][j] && a[i - 1][j]) Up[i][j] = Up[i - 1][j] + 1;//Up
for (int i = 1; i <= n; ++i)
for(int j = 1; j <= m; ++j)
{
if ((i ^ 1) && a[i][j] != 0 && a[i - 1][j] != 0)//特别注意第一行不能转移
{
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, Min(Up[i][j], r[i][j] - l[i][j] + 1));
}
printf("%d\n", ans);
return 0;
}
3. 练习题
练习题传送门:DP专题-专项训练:悬线法 DP