P2216 [HAOI2007] 理想的正方形 题解
user100566 · · 题解
前言
这道题是一道很好的深入理解 ST 算法的例题,通过优化,ST 算法成功地取得了这道题的总 222ms,最大 32ms,8.04MB 的最优解,与次优解拉开断层差距。
本题解讲解如何从基础的 ST 算法开始优化,在这之前你需要掌握基本的 ST 算法。
题目分析
题目描述很直接,考虑用 ST 算法实现,初始化 ST 表后,暴力枚举正方形区域(共
基础的 ST 做法
二维 ST 表的定义
用
ST 的原理是用两个可重叠的区间合并为要查询的区间,只要两个最长区间长度的和不小于最大查询长度即可,不妨设最大查询长度为
接下来考虑二维 ST 表,参考一维下
因为查询长度
求解 ST 表
求解这个 ST 表比较简单,设现在是在求最大值 ST 表,参考一维 ST 表的倍增转移方程,得到下面的初始化方法:
注意,并不需要同时通过两个方程转移,实现时可以先从小到大枚举
处理查询
接下来考虑查询操作,设要查询的矩形为
可以看出查询被划分为
复杂度分析
ST 表的大小即空间复杂度为
然而我们计算程序需要的空间大小,最大最小 ST 表总计需要内存
正方形优化
ST 表压维
注意到查询的矩形始终是正方形的,所以查询时的子块也是正方形的,这意味着我们没有必要定义两个
定义新的压维 ST 表
压维 ST 表的求解
一个大正方形可以由
处理查询
同基础 ST 表的查询,由于查询的是正方形,一定能找到
复杂度分析
容易知道时空复杂度均为
自我滚动优化
ST 算法的本质是倍增 DP,既然是 DP,那么就可以使用滚动数组优化空间复杂度,倍增长度这一维是可以滚掉的,不过为什么普通的 ST 算法不使用滚动优化呢?
原因显然:对于不同的查询长度,需要不同长度的块来合并为查询区间,例如查询
然而本题询问的正方形边长
滚动 ST 表
我们把
滚动 ST 表的求解
同压维 ST 表的求解,需要注意的是外层
同时滚动时注意
处理查询
查询正方形的大小是固定的,对于给定的正方形左上角坐标,要查询的
复杂度分析
容易知道这一步优化后空间复杂度变为
递推优化
设想如果要求查询的矩形不是正方形,那么无法使用正方形优化,但是依然可以使用自我滚动优化:先滚动
-
B[i][j]=\max\{B[i][j], B[i][j+2^{p-1}], B[i+2^{p-1}][j], B[i+2^{p-1}][j+2^{p-1}]\} -
B[i][j]=\max\{B[i][j], B[i+2^{p-1}][j]\}, B[i][j]=\max\{B[i][j], B[i][j+2^{q-1}]\}
将 1. 中的
实际上并不一定真的先滚动
当然,查询过程中也可以使用这样的优化。
图形解释
观察下面一张格点有向图:
注意,这张图省略了主对角线外的斜有向边,例如
其中,每个节点都可视为 ST 表的状态(仅含
查询时,需要根据查询长度在
回顾基础 ST 表
这张图共
对于正方形优化中的压维 ST 表
对角线上的节点数共
滚动 ST 表利用了询问只用到了右下角的节点这一特性,只存储一个节点的信息,然后通过自我滚动转移到新的节点上。
边上的数值表示了转移所需的时间,前面已经提到了,先向下再向右由于直接向右下,递推优化的转移图如下:
上面演示的是先滚动
由于询问是正方形的,到达最终节点向下和向右的边数相等,所以可以同步滚动,转移图如下:
这仅仅是为了将转移写在一个循环内,两者没有性能上的明显差距。
请读者借助以以上图片思考以下问题:
- 如果需要找出的是
n\times m 的矩形,如何实现? - 如果正方形的边长可以是给定的
k 个\le n 的值(假设题目为此缩小了数据范围或者延长了时间限制),能否用O(ab) 的空间复杂度实现? - 如果找出的矩形的大小可以是给定的
k 个(n, m) ,并且满足n_i\le n_{i+1}, m_i\le m_{i+1} ,能否用O(ab) 的空间复杂度实现? - 跳出本题,考虑二维数组 RMQ,每次查询一个
n\times m 的矩阵,其中n 是一个在所有询问前给出的定值,m 与查询有关,那么最好的时空复杂度分别为多少?如果强制在线呢?
代码
关于取值细节,代码中的注释有详细解释,其它优化如快读,预处理等请参见代码。
云剪贴板存档
#include <cstdio>
#include <algorithm>
using namespace std;
// 快读
#ifdef ONLINE_JUDGE
#define getchar() getchar_unlocked()
#endif
inline int read(){
int c = getchar();
while(c<48||c>57) c = getchar();
int x = 0;
while(48<=c&&c<=57){
x = (x<<3)+(x<<1)+(c^48);
c = getchar();
}
return x;
}
int a, b, n;
// dp1 维护最大值 dp2 维护最小值
int dp1[1000][1000], dp2[1000][1000];
int main(){
a = read(); b = read(); n = read();
// 即使去掉这个特判也是正确的
if(n==1){
putchar('0');
return 0;
}
for(int i=0; i<a; ++i){
for(int j=0; j<b; ++j){
// 不需要存原矩阵,直接作为 dp 初值
dp2[i][j] = dp1[i][j] = read();
}
}
int maxT = 0;
while((2<<maxT)<n) ++maxT;
int offset, maxi, maxj;
for(int t=0; t<maxT; ++t){
offset = 1<<t;
// 在 i 方向上倍增
// i 方向上区间长是 2*offset,区间为 [i, i+2*offset-1]
// i+2*offset-1<a => i<a-2*offset+1 => i<=a-2*offset
maxi = a-(offset<<1);
// j 方向上区间长是 offset,区间为 [j, j+offset-1]
// j+offset-1<b => j<b-offset+1 => j<=b-offset
maxj = b-offset;
for(int i=0; i<=maxi; ++i){
for(int j=0; j<=maxj; ++j){
// std::max, std::min 快得不止一点
//if(dp1[i+offset][j]>dp1[i][j]) dp1[i][j] = dp1[i+offset][j];
//if(dp2[i+offset][j]<dp2[i][j]) dp2[i][j] = dp2[i+offset][j];
dp1[i][j] = max(dp1[i][j], dp1[i+offset][j]);
dp2[i][j] = min(dp2[i][j], dp2[i+offset][j]);
}
}
// 在 j 方向上倍增
// maxi = a-(offset<<1);
maxj = b-(offset<<1);
for(int i=0; i<=maxi; ++i){
for(int j=0; j<=maxj; ++j){
dp1[i][j] = max(dp1[i][j], dp1[i][j+offset]);
dp2[i][j] = min(dp2[i][j], dp2[i][j+offset]);
}
}
}
offset = n-(1<<maxT);
// 在 i 方向上倍增
// 区间长是 n,区间为 [i, i+n-1]
// i+n-1<a => i<a-n+1 => i<=a-n
maxi = a-n;
// 区间长是 2^maxT,区间为 [j, j+2^maxT-1]
// j+2^maxT-1<b => j<b-2^maxT+1 => j<=b-2^maxT
maxj = b-(1<<maxT);
for(int i=0; i<=maxi; ++i){
for(int j=0; j<=maxj; ++j){
dp1[i][j] = max(dp1[i][j], dp1[i+offset][j]);
dp2[i][j] = min(dp2[i][j], dp2[i+offset][j]);
}
}
// 在 j 方向上倍增
// maxi = a-n;
maxj = b-n;
int ans = 1000000000;
for(int i=0; i<=maxi; ++i){
for(int j=0; j<=maxj; ++j){
// 直接更新 ans,不用更新 dp
ans = min(ans, max(dp1[i][j], dp1[i][j+offset])-min(dp2[i][j], dp2[i][j+offset]));
}
}
printf("%d", ans);
return 0;
}