详细讲解一系列矩阵问题的考察方式和解决问题思路
题目一:1282 最大子矩阵
1282:最大子矩阵 信奥
【题目描述】
已知矩阵的大小定义为矩阵中所有元素的和。给定一个矩阵,你的任务是找到最大的非空(大小至少是1 × 1)子矩阵。
比如,如下4 × 4的矩阵
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2
的最大子矩阵是
9 2
-4 1
-1 8
这个子矩阵的大小是15。
【输入】
输入是一个N×N的矩阵。输入的第一行给出N(0<N≤100)
。再后面的若干行中,依次(首先从左到右给出第一行的N
个整数,再从左到右给出第二行的N个整数……)给出矩阵中的N2个整数,整数之间由空白字符分隔(空格或者空行)。已知矩阵中整数的范围都在[−127,127]。
【输出】
输出最大子矩阵的大小。
【输入样例】
4
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2
【输出样例】
15
知识点:前缀和,即s[i]=a[0]+a[1]+…+a[i-1]+a[i]。二维前缀和有两种写法,第一种是只求一整列或者一整行的前缀和:a[i][j]+=a[i-1][j]或a[i][j]+=a[i][j-1];
第二种是行列整体一起求。a[i][j]=a[i-1][j]+a[i][j-1]-a[i-1][j-1]。
两种方法分别有不同的用法。当数据量小时可以行列一起运用,一般是四重循环。数据量大时单用行或列,再结合滑动窗口降低时间复杂度。
#include<bits/stdc++.h>
using namespace std;
int n;
int a[1001][1001];
int res=-9999;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
cin>>a[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
a[i][j]+=a[i-1][j]+a[i][j-1]-a[i-1][j-1];
}
}
for(int k=1;k<=n;k++){//控制矩形的长度
for(int l=1;l<=n;l++){//控制矩形的宽度
//以下两个循环开始逐一查找符合边长为k*l的每个子矩阵的前缀和,筛选出最大值
for(int i=k;i<=n;i++){
for(int j=l;j<=n;j++){
res=max(res,a[i][j]-a[i-k][j]-a[i][j-l]+a[i-k][j-l]);
}
}
}
}
//错误解法:已经是前缀和,相当于动态规划,只能从后往前推当前的结果,结果跟着下标改动,
//不能先定左上角再定右下角,相加的值以及超过数组范围,不可取。
// for(int i=1;i<=n;i++){
// for(int j=1;j<=n;j++){
// for(int k=1;k<=n-i;k++){
// for(int l=1;l<=n-j;l++){
// res=max(res,a[i+k][j+l]-a[i-k][j]-a[i][j-l]+a[i-k][j-l]);
// }
// }
// }
// }
cout<<res<<endl;
}
//属于单行单列的前缀和解法,只取列的前缀和,但在测试平台有数据不通过,不清楚什么原因。
// for(int i=1;i<=n;i++){
// for(int j=1;j<=n;j++){
// cin>>a[i][j];
// a[i][j]+=a[i][j-1];
// }
// }
// int ans=0;
// for(int l=0;l<=n-1;l++){
// for(int r=1;r<=n;r++){
// ans=0;
// for(int k=1;k<=n;k++){
// ans+=a[k][r]-a[k][l];
// if(ans>maxx)maxx=ans;
// //if(ans<0)ans=0;
// }
// }
// }
相关知识串讲:求一个矩阵可以分割成多少个任意边长的子矩阵问题。学好二维矩阵的前提。
题目二:P2241 统计方形(数据加强版)
若用暴力枚举思想,需要很多重循环,十分复杂。但若假设有一个子矩形包含在大矩形中不断移动,就能覆盖所有种可能。当边长刚好是m,n时,一共有(m-m+1)×(n-n+1)种可能,边长为i,j时就有(m-i+1)×(n-j+1)种可能,。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n=sc.nextInt();
int m=sc.nextInt();
long a=0,b=0;
//int x[][]=new int[n+1][m+1];
for (int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(i==j)a+=(n-i)*(m-j);//i=j说明是正方形
else b+=(n-i)*(m-j);
}
}
System.out.println(a+" "+b);
}
}
当划分边长不能重复的情况时,又有另一种考查方式和解法。
题目三:分巧克力
题目描述
儿童节那天有 K 位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。
小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。切出的巧克力需要满足:
形状是正方形,边长是整数;
大小相同;
例如一块 6x5 的巧克力可以切出 6 块 2x2 的巧克力或者 2 块 3x3 的巧克力。
当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?
输入描述
第一行包含两个整数
输入保证每位小朋友至少能获得一块 1x1 的巧克力。
输出描述
输出切出的正方形巧克力最大可能的边长。
输入输出样例
示例
输入
2 10
6 5
5 6
copy
输出
2
普通用法测试数据不能全部通过,二分法才可以。
普通解法:
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class 分巧克力 {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int k=sc.nextInt();
List<Integer[][]> list=new ArrayList<>();
for (int i=0;i<n;i++){
list.add(new Integer[sc.nextInt()][sc.nextInt()]);//不必要开数组,直接创对象再加进去,用时取
}
int cnt[]=new int[100001];
int len=0;
int max=0;
for (Integer[][] t:list){
int a=t.length;
int b=t[0].length;
for (int i=1;i<a;i++){
for (int j=1;j<b;j++){
if(i==j) {
cnt[i]+=(a/i)*(b/j);//不重复的话要用除法,可以重复用减号
//len=i;
max=Math.max(i,max);//每个巧克力边长不一样,所以在循环内部标记有局限性
break;
}
}
}
}
// for (int i=1;i<=len;i++){
// System.out.print(cnt[i]+" ");
// }
for (int i=max;i>0;i--){
if(cnt[i]>=k)
{
int t=i;
System.out.println(t);
break;
}
}
}
}
二分解法:
import java.util.Scanner;
public class 分巧克力二分法 {
static int w[]=new int[100006];
static int h[]=new int[100006];
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int k=sc.nextInt();
for (int i=0;i<n;i++){
w[i]=sc.nextInt();
h[i]=sc.nextInt();
}
int r=100006;
int l=1;
int ans=0;
while(l<=r){
int mid=(r+l)/2;
int cnt=0;
for (int i=0;i<n;i++){
cnt+=(w[i]/mid)*(h[i]/mid);
}
if(cnt>=k)
{
l=mid+1;//并不是没有用,因为要求尽可能最大,后来要向上区间查找,知道循环结束才能求出最大结果
ans=mid;
}else
{
r=mid-1;
}
}
System.out.println(ans);
}
}
然后接着回到二维前缀和问题上。二维前缀和也属于动态规划思想,后边子矩阵的前缀和取决于前边的记录结果。
题目四:统计子矩阵
问题描述
给定一个 N×M 的矩阵 A, 请你统计有多少个子矩阵 满足子矩阵中所有数的和不超过给定的整数 K?
输入格式
第一行包含三个整数 N,M 和 K
输出格式
一个整数代表答案。
样例输入
3 4 10
1 2 3 4
5 6 7 8
9 10 11 12
样例输出
19
如果用普通的行列一起算的前缀和,需要四重循环,数据量过大,只能通过70%,该方法参考题目一的解法,下边将提供第二种解法,求列行的前缀和,再结合滑动窗口降低时间复杂度。
import java.io.*;
public class 统计子矩阵 {
// static int N=510,M=510;
static int n,m;
static long k;
static long a[][]=new long[510][510];
static long res;
static BufferedReader sc=new BufferedReader(new InputStreamReader(System.in));
static PrintWriter out=new PrintWriter(new OutputStreamWriter(System.out));
public static void main(String[] args) throws IOException {
String st=sc.readLine();
String s[]=st.split(" ");
n=Integer.parseInt(s[0]);
m=Integer.parseInt(s[1]);
k=Integer.parseInt(s[2]);
for (int i=1;i<=n;i++){
st=sc.readLine();
s=st.split(" ");
for (int j=1;j<=m;j++){
a[i][j]=Integer.parseInt(s[j-1]);
}
}
for (int i=1;i<=n;i++){//行列前缀和
for (int j=1;j<=m;j++){
a[i][j]+=a[i-1][j]+a[i][j-1]-a[i-1][j-1];
}
}
// //四重循环,前两个循环控制矩形的边长,后两个循环开始进行二维循环遍历
// for (int h=1;h<=n;h++){//高
// for (int l=h;l<=m;l++){//宽
// long sum=0;
// for (int i=h;i<=n;i++){
// for (int j=l;j<=m;j++){
// sum=a[i][j]-a[i-h][j]-a[i][j-l]+a[i-h][j-l];
// if(sum<=k)
// res++;
// }
// }
// }
// }
// System.out.println(res);
//滑动窗口计算,降低时间复杂度,前缀和只是单列前缀和
for (int i=1;i<=n;i++){
for (int j=1;j<=m;j++){
a[i][j]+=a[i-1][j];
}
}
int res2=0;
for (int i=1;i<=n;i++){//控制横向边长上界
for (int i2=i;i2<=n;i2++){//控制横向边长下界
int l = 1, r = 1;//滑动窗口的左右端点
int sum = 0;//区间前缀和:[l,r]区间的累计和
for(r = 1; r <= m; r++)//遍历右端点,根据区间和调整左端点
{
sum += a[i2][r] - a[i-1][r];//加上右端点处的和
while(sum > k)//区间和了,左端点右移,区间变小
{
sum -= a[i2][l] - a[i-1][l];//减去移出去的左端点处的和
l++;
}
res2 += r - l + 1;//方法数就是找到的区间大小累加!!关键思考点在这里,
}
}
}
System.out.println(res2);
}
}
题目五:激光炸弹
地图上有 N个目标,用整数 Xi,Yi表示目标在地图上的位置,每个目标都有一个价值 Wi。
注意:不同目标可能在同一位置。
现在有一种新型的激光炸弹,可以摧毁一个包含 R×R个位置的正方形内的所有目标。
激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆炸范围,即那个正方形的边必须和 x,y轴平行。
求一颗炸弹最多能炸掉地图上总价值为多少的目标。
输入格式
第一行输入正整数 N 和 R,分别代表地图上的目标数目和正方形包含的横纵位置数量,数据用空格隔开。
接下来 N行,每行输入一组数据,每组数据包括三个整数 Xi,Yi,Wi,分别代表目标的 x坐标,y坐标和价值,数据用空格隔开。
输出格式
输出一个正整数,代表一颗炸弹最多能炸掉地图上目标的总价值数目。
数据范围
0≤R≤109
0<N≤10000
,
0≤Xi,Yi≤5000
0≤Wi≤1000
输入样例:
2 1
0 0 1
1 1 1
输出样例:
1
#include<bits/stdc++.h>
using namespace std;
const int N=5010;
int g[N][N];
int main(){
int N,R;
cin>>N>>R;
int n=R,m=R;
for(int i=0;i<N;i++){
int x,y,w;
cin>>x>>y>>w;
x++;y++;
n=max(n,x);y=max(m,y);
g[x][y]+=w;
}
//求前缀和
for(int i=1;i<=n;i++) {
for(int j=1;j<=m;j++) {
g[i][j]+=g[i-1][j]+g[i][j-1]-g[i-1][j-1];
}
}
int res=0;
for(int i=R;i<=n;i++) {//以边长为R的正方形为基础。
for(int j=R;j<=m;j++) {
res=max(res, g[i][j]-g[i-R][j]-g[i][j-R]+g[i-R][j-R]);
}
}
cout<<res;
return 0;
}
题目六:最大子矩阵(蓝桥)
只会暴力解法,全通的解法还结合状态压缩和二分,很复杂,想不动,沾上源码未来再看。
import java.io.*;
import java.util.*;
public class Main {
//max[k][i][j]表示第k列中[i,j]之间的最大值
static int[][][] max;
static int[][][] min;
static int n, m, limit,ans;
static BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
static PrintWriter out=new PrintWriter(new OutputStreamWriter(System.out));
public static void main(String[] args) throws IOException {
String[] s=br.readLine().split(" ");
n = Integer.parseInt(s[0]);
m = Integer.parseInt(s[1]);
max=new int[m+1][n+1][n+1];
min=new int[m+1][n+1][n+1];
for (int i = 1; i <= n; i++) {
s=br.readLine().split(" ");
for (int j = 1; j <= m; j++) {
max[j][i][i] = min[j][i][i] = Integer.parseInt(s[j-1]);
}
}
limit = Integer.parseInt(br.readLine());
//预处理 复杂度 n^2*m
for (int k = 1; k <= m; ++k) {
for (int i = 1; i <= n; ++i) {
for (int j = i + 1; j <= n; ++j) {
max[k][i][j] = Math.max(max[k][i][j - 1], max[k][j][j]);
min[k][i][j] = Math.min(min[k][i][j - 1], min[k][j][j]);
}
}
}
for (int x1 = 1; x1 <= n; x1++) {
for (int x2 = x1; x2 <= n; x2++) {
int l = 1, r = m;
while (l < r) {
int mid = l + r + 1 >> 1;
if (check(x1, x2, mid)) l = mid;
else r = mid - 1;
}
if (check(x1,x2,r)) ans=Math.max(ans,(x2-x1+1)*r);
}
}
out.println(ans);
out.flush();
}
//k是窗口大小
static boolean check(int x1, int x2, int k) {
Deque<Integer> qmax = new ArrayDeque<>();
Deque<Integer> qmin = new ArrayDeque<>();
for (int i = 1; i <= m; i++) {
//处理最小
if (!qmin.isEmpty() && qmin.peekFirst() < i - k + 1) qmin.pollFirst();
while (!qmin.isEmpty() && min[qmin.peekLast()][x1][x2] > min[i][x1][x2]) qmin.pollLast();
qmin.offerLast(i);
//处理最大
if (!qmax.isEmpty() && qmax.peekFirst() < i - k + 1) qmax.pollFirst();
while (!qmax.isEmpty() && max[qmax.peekLast()][x1][x2] < max[i][x1][x2]) qmax.pollLast();
qmax.offerLast(i);
//说明窗口为k
if (i >= k && max[qmax.peekFirst()][x1][x2] - min[qmin.peekFirst()][x1][x2] <= limit) return true;
}
return false;
}
}