最大子矩阵 【Java实现】【深度优先搜索】【二分查找】
问题描述:
小明有一个大小为N × M 的矩阵,可以理解为一个 N 行 M 列的二维数组。我们定义一个矩阵 m的稳定度 f(m) 为 f(m) = max(m) − min(m),其中max(m)表示矩阵 m中的最大值,min(m) 表示矩阵 m 中的最小值。现在小明想要从这个矩阵中找到一个稳定度不大于limit 的子矩阵,同时他还希望这个子矩阵的面积越大越好(面积可以理解为矩阵中元素个数)。
子矩阵定义如下:从原矩阵中选择一组连续的行和一组连续的列,这些行列交点上的元素组成的矩阵即为一个子矩阵。
解题算法思路
- 假设一个矩阵的左上端点的坐标为(x1,y1),右下坐标的端点为(x2,y2)。我们可以暴力枚举两点的横坐标
x1和x2,这样做的复杂度为(n^2)。 - 接下来我们只需要找到y1和y2使得矩阵的稳定度不大于limit就可以了。如果同样暴力枚举的话复杂度是O(m^2)不可接受。所以考虑使用二分法优化。
- 在下面的代码中,二分法通过check方法实现,当二分的值为mid时,也就是需要去判断是否存在宽度为mid的矩阵符合要求,因为我们前面预处理已经将高度压缩为1,所以这其实就是一个一维数组滑动窗口求最值问题,属于是单调队列的模板使用,求出每个长度为mid的窗口的最大值max和最小值min,只要有一个窗口符合max−min<=limit我们都返回true,否则返回false。
- 接下来我们思考如何高效的求出一个矩阵的max和min,如果是一个一维数组,求区间的最值的方法就很多了,所以我们将二维矩阵预处理成一维。我们生成两个数组max[k][i][j]以及min[k][i][j],其中max[k][i][j]代表的含义是在第k列中,第i
个元素到第j 个的元素最大值是多少,min数组同理。我们预处理的转移式子应该为:
max[k][i][j]=max(max[k][i][j−1],max[k][j][j])
这个式子的含义也很简单,对于区间 [i,j]的最大值应该是 [i,j−1]的最大值和 j 位值取较大值。由于我们处理的是每一列,这样时间复杂度是 O(n^2),由于总共有 m,所以总时间复杂度应该是 O(n^2m)。
接下来是源代码
1.import java.io.*;
2.import java.util.ArrayDeque;
3.import java.util.Deque;
4.
5.public class MaximumSubmatrix {//最大子矩阵 深度优先遍历 二分查找优化
6. //max[k][i][j]表示第k列中[i,j]之间的最大值
7. static int[][][] max;
8. static int[][][] min;
9. static int n, m, limit,ans;
10. static BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
11. static PrintWriter out=new PrintWriter(new OutputStreamWriter(System.out));
12. public static void main() throws IOException {
13. String[] s=br.readLine().split(" ");
14. n = Integer.parseInt(s[0]);
15. m = Integer.parseInt(s[1]);
16. max=new int[m+1][n+1][n+1];
17. min=new int[m+1][n+1][n+1];
18. for (int i = 1; i <= n; i++) {
19. s=br.readLine().split(" ");
20. for (int j = 1; j <= m; j++) {
21. max[j][i][i] = min[j][i][i] = Integer.parseInt(s[j-1]);
22. }
23. }
24. limit = Integer.parseInt(br.readLine());
25. //预处理 复杂度 n^2*m
26. for (int k = 1; k <= m; ++k) {
27. for (int i = 1; i <= n; ++i) {
28. for (int j = i + 1; j <= n; ++j) {
29. max[k][i][j] = Math.max(max[k][i][j - 1], max[k][j][j]);
30. min[k][i][j] = Math.min(min[k][i][j - 1], min[k][j][j]);
31. }
32. }
33. }
34. for (int x1 = 1; x1 <= n; x1++) {
35. for (int x2 = x1; x2 <= n; x2++) {
36. int l = 1, r = m;
37. while (l < r) {
38. int mid = l + r + 1 >> 1;
39. if (check(x1, x2, mid)) l = mid;
40. else r = mid - 1;
41. }
42. if (check(x1,x2,r)) ans=Math.max(ans,(x2-x1+1)*r);
43. }
44. }
45. out.println("子矩阵的最大面积为 "+ans);
46. out.flush();
47. UI.main();
48. }
49.
50. //k是窗口大小
51. static boolean check(int x1, int x2, int k) {
52. Deque<Integer> qmax = new ArrayDeque<>();
53. Deque<Integer> qmin = new ArrayDeque<>();
54. for (int i = 1; i <= m; i++) {
55. //处理最小
56. if (!qmin.isEmpty() && qmin.peekFirst() < i - k + 1) qmin.pollFirst();
57. while (!qmin.isEmpty() && min[qmin.peekLast()][x1][x2] > min[i][x1][x2]) qmin.pollLast();
58. qmin.offerLast(i);
59. //处理最大
60. if (!qmax.isEmpty() && qmax.peekFirst() < i - k + 1) qmax.pollFirst();
61. while (!qmax.isEmpty() && max[qmax.peekLast()][x1][x2] < max[i][x1][x2]) qmax.pollLast();
62. qmax.offerLast(i);
63. //说明窗口为k
64. if (i >= k && max[qmax.peekFirst()][x1][x2] - min[qmin.peekFirst()][x1][x2] <= limit) return true;
65. }
66. return false;
67. }
68.}
运行样例截图
算法效率分析
- 一般来说,二分是个很有用的优化途径,因为这样会直接导致减半运算,而对于能否二分,有一个界定标准:状态的决策过程或者序列是否满足单调性或者可以局部舍弃性。对于这道题而言,因为本质上横坐标x确定的是矩阵高,而y确定的是矩阵的宽,所以我们去二分宽度——查找矩阵内是否存在宽度为L的矩阵稳定度不大于limit。如果存在一个宽度为L的矩阵符合要求,那么一定能找到一个宽度在区间 [L,L−1]的矩阵也符合要求。所以这道题可以使用二分。
- 当二分得到最长宽度为r 时,该矩阵的面积就为(x2−x1+1)∗r,每次用一个全局变量更新答案。考虑时间复杂度——枚举横坐标为 O(n^2),
- 二分的复杂度为O(logm),每次check判定的复杂度是O(m),所以整体时间复杂度为O(n^2mlogm)。