【最大子矩阵和】二维前缀和的应用及矩阵的相关问题

详细讲解一系列矩阵问题的考察方式和解决问题思路

题目一: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;
    }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值