样例输入
3 4 10
1 2 3 4
5 6 7 8
9 10 11 12
样例输出
19
分析:赛前没见过这样的做题方法,赛后才知道原来具有某种单调性质的子矩阵问题还可以通过双指针来进行优化,下面来对这种方法进行一下介绍:
我们先枚举矩阵的左右边界,这个复杂度是n^2的,如图所示:
确定L和R后,我们来枚举上下边界,在上下左右边界内的数都是我们所需要选择的数,因为我们能保证枚举完所有的可能成为答案的上下左右区间,所以这样枚举也是正确的。那是不是枚举上下边界也是n^2的复杂度呢?其实不是,题目中说了方格中的每一个数都是正数,也就是说在原来面积的基础上扩大面积,则和是单调不降的,换句话说在原来的基础上把下边界下移不会导致我们所选择的数的和变小,只会变大或者保持不变。所以我们就可以利用这个单调性来进行优化,由于左右边界都已经确定了,我们先预处理出来每一行第i列到第j列的元素和,这个可以通过前缀和相减得到,记录s[i][j]表示第i行前j列元素的和,也就是a[i][1]+……+a[i][j],那么我们在枚举L~R之间的元素时就可以o(1)处理出来任意一行第L列到第R列的数,那么就相当于在枚举一个个数,第一个数为s[1][L~R](s[R]-s[L-1]),第二个数是s[2][L~R],第i个数是s[i][L~R],那么就相当于我们有n个数(共有n行),然后求有多少种情况满足连续的若干个数的和小于等于K,这个显然可以用双指针来做,也就是先固定上边界,向下移动下边界,直到所选区域内的数的和大于k为止,不妨设这个时候的上边界为u,下边界为d,那么只要下边界位于u~d-1之间都是满足区域和小于等于k的(等价于下边界上移),也就是说以L和R作为左右边界且以u做为上边界的可能下边界有d-u种情况,我们只需要把这个记录到答案内即可。需要注意的是下边界不能超过n,当下边界无法移动时,这个时候再移动上边界,直到上边界也移动到最下方为止,这就是一个基本的双指针求连续区间和的问题。这一部分复杂度为o(n),所以这道题目总的复杂度就是o(n^3)。
这道题给我们的启发就是对于答案具有某种单调性的子矩阵问题(例如随着选取面积的增大而增大或者减少的某种性质),或许我们并不需要利用二维前缀和进行o(n^4)进行枚举,我们可以通过先枚举左右边界,然后利用单调性来进行双指针枚举上下边界求取满足某种性质的方案数,思想还是挺巧妙的。
下面是代码:
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC optimize(fast)
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<set>
#include<vector>
#include<map>
#include<queue>
using namespace std;
const int N=503;
long long s[N][N];//s[i][j]表示第i行前j列的和,也就是a[i][1]+……+a[i][j]
int main()
{
int n,m,K;
cin>>n>>m>>K;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
scanf("%lld",&s[i][j]);
s[i][j]+=s[i][j-1];
}
long long ans=0;
for(int l=1;l<=m;l++)
for(int r=l;r<=m;r++)
{
int d=1;
long long sum=0;
for(int u=1;u<=n;u++)
{
while(sum+s[d][r]-s[d][l-1]<=K&&d<=n)
{
sum+=s[d][r]-s[d][l-1];
d++;
}
sum-=s[u][r]-s[u][l-1];
ans+=d-u;//在u到d-1这段区间内的和都是小于等于K的
}
}
printf("%lld",ans);
return 0;
}