【题解】洛谷 P1169 [ZJOI2007] 棋盘制作 [思维 + dp]
P1169 [ZJOI2007] 棋盘制作 - 洛谷
我不会告诉你这道题我做了多久。
注意到数据范围,正解应该是 的时间复杂度。
单纯枚举长方形是 ,肯定是要做些什么前缀和 / 预处理来降低下的。
二维预处理后转移考虑 dp,整体要满足 “局部最优达成整体最优 / 整体最优能从局部最优继承”。
那应该怎么定义?
我就是在这一步走歪的,定义了 :以
为右下角的最大合法矩阵的行范围和列范围。
但其实有一个很明显的问题:如果有面积相等的两个矩阵,应该选哪个?
由于后面的状态是否合法还要看当前状态的边界,所以说这样做是有后效性的。
所以这种方法从一开始就是错误的,这警示我们打一切 dp 之前都要仔细证明有无后效性。
(我就这样调了好久找不出错,,小数据都对 70 分。)
不管怎样,不能再以点作为结束点 / 边界了。
但是肯定是要有边界的,比如说以某一行 or 某一列。
这样转移下一行 / 列时边界对接都是固定的,不会产生后效性。
考虑以行作为边界,预处理 up 数组作为 往上(更小的行)可合法的格子数。
但这还不够,我们需要知道当前行合法的格子范围,于是再定义两个数组记录该行合法范围。
注意到一行的合法范围不止一段,那么再细节点,定义:
int lef[N][N], rig[N][N];
// lef[i][j]:第 i 行从 (i,j) 向左能延伸到的合法最左列(满足黑白交替)
// rig[i][j]:第 i 行从 (i,j) 向右能延伸到的合法最右列(满足黑白交替)
状态转移时,我们有:
// 如果不是第一行,且当前格与上方格颜色不同if(i > 1 && mat[i][j] != mat[i - 1][j]) {up[i][j] = up[i - 1][j] + 1;// 当前悬线可以向上延伸:高度 + 1lef[i][j] = max(lef[i][j], lef[i - 1][j]);// 左边界取当前行与上一行的更靠右者(取 max,因为 left 值越小越靠左)rig[i][j] = min(rig[i][j], rig[i - 1][j]);// 右边界取当前行与上一行的更靠左者(取 min,因为 right 值越大越靠右)}// 当前悬线能覆盖的横向宽度int width = rig[i][j] - lef[i][j] + 1;// 正方形边长受限于宽度和高度中的较小值int side = min(width, up[i][j]);// 更新最大正方形面积(边长平方)s_ans = max(s_ans, side * side);// 更新最大矩形面积(宽 × 高)r_ans = max(r_ans, width * up[i][j]);
发现这样转移出来的是个倒着的 “T” 字形,想要变成矩阵,还要满足:
// 预处理每行的 left:若当前格与左边格颜色不同,则可继承左边的左边界for (int i = 1; i <= n; i ++) {for (int j = 2; j <= m; j ++) if (mat[i][j] != mat[i][j - 1]) {lef[i][j] = lef[i][j - 1]; // 向左扩展}}// 预处理每行的 right:若当前格与右边格颜色不同,则可继承右边的右边界for (int i = 1; i <= n; i ++) {for (int j = m - 1; j >= 1; j --) if (mat[i][j] != mat[i][j + 1]) {rig[i][j] = rig[i][j + 1]; // 修正:应为 j+1,不是 j-1}}
即预处理时就要定义 lef 和 rig 是可以和上一行贴合的,保证后面每一次转移都合法。
就这样做完了,完整代码请翻到最后面。
关于这种状态转移比较诡异的 dp,我们其实有一个专门的算法:悬线法。
注释里也写了,up 数组就是 “悬线”。顾名思义,是从上面往下悬的一整列合法格子。
然后标准过程就如本题,预处理俩数组然后美美转移。
只要满足以下几点,就是悬线法题目:
(1)求最大子矩形
(2)合法性具有“垂直可继承性”(有悬线可处理)
(3)行内合法性可预处理(可降低时间复杂度)
(4)约束是局部的(相邻之间)
再使用点,以下三个问题都满足,即可用悬线:
- 是不是在 0 / 1(或有限颜色)矩阵中找最大矩形?
- “合法”是否只依赖相邻格子(上下左右)?
- 数据范围是否允许
?
完整代码:
// 代码来自本人,注释由千问 AI 生成 + 人工修改
#include<bits/stdc++.h>
using namespace std;const int N = 2010;int mat[N][N];
// mat[i][j]:原始棋盘颜色(0/1)int lef[N][N], rig[N][N];
// lef[i][j]:第 i 行从 (i,j) 向左能延伸到的合法最左列(满足黑白交替)
// rig[i][j]:第 i 行从 (i,j) 向右能延伸到的合法最右列(满足黑白交替)int up[N][N];
// up[i][j]:从 (i,j) 向上能延伸的连续黑白交替格子数(即 "悬线" 高度)int main() {ios::sync_with_stdio(false);cin.tie(0);int n, m;cin >> n >> m; // 读入棋盘,并初始化 left / right / upfor (int i = 1; i <= n; i ++) {for (int j = 1; j <= m; j ++) {cin >> mat[i][j];lef[i][j] = rig[i][j] = j; // 初始时左右边界都是自己up[i][j] = 1; // 初始高度为 1(仅当前格)}}// 预处理每行的 left:若当前格与左边格颜色不同,则可继承左边的左边界for (int i = 1; i <= n; i ++) {for (int j = 2; j <= m; j ++) if (mat[i][j] != mat[i][j - 1]) {lef[i][j] = lef[i][j - 1]; // 向左扩展}}// 预处理每行的 right:若当前格与右边格颜色不同,则可继承右边的右边界for (int i = 1; i <= n; i ++) {for (int j = m - 1; j >= 1; j --) if (mat[i][j] != mat[i][j + 1]) {rig[i][j] = rig[i][j + 1]; // 修正:应为 j+1,不是 j-1}}int s_ans = 0, r_ans = 0; // 正方形面积和长方形面积 for (int i = 1; i <= n; i ++) {for (int j = 1; j <= m; j ++) {// 如果不是第一行,且当前格与上方格颜色不同if(i > 1 && mat[i][j] != mat[i - 1][j]) {up[i][j] = up[i - 1][j] + 1;// 当前悬线可以向上延伸:高度 + 1lef[i][j] = max(lef[i][j], lef[i - 1][j]);// 左边界取当前行与上一行的更靠右者(取 max,因为 left 值越小越靠左)rig[i][j] = min(rig[i][j], rig[i - 1][j]);// 右边界取当前行与上一行的更靠左者(取 min,因为 right 值越大越靠右)}// 当前悬线能覆盖的横向宽度int width = rig[i][j] - lef[i][j] + 1;// 正方形边长受限于宽度和高度中的较小值int side = min(width, up[i][j]);// 更新最大正方形面积(边长平方)s_ans = max(s_ans, side * side);// 更新最大矩形面积(宽 × 高)r_ans = max(r_ans, width * up[i][j]);}}cout << s_ans << "\n" << r_ans << "\n";return 0;
}
