1.K倍区间:
这道题目很明显能看出来需要使用前缀和进行解答,但是由于最多偶1e6个数字所以 通过前缀和加上暴力的方法肯定是不可行的所以我们需要考虑怎样进行时间复杂度的优化,那么我们不妨进行分析: 对于任意一个子序列,用s数组来表示前缀和,那么从l到r的序列无非也就是s[r]-s[l-1],如果他是k的倍数那么它们的差模k必定为0那么再进行拆解,其实也就是s[r]%k == s[l-1]%k,所以我们不妨开一个数组进行存储每一个前缀和的模,这个数组里面就只会有0到k-1的元素,于是就能用较小的时间复杂度计算出来ans,代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+100;
int n,k,ans;
int a[N],s[N],res[N];
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
s[i]=s[i-1]+a[i];
ans += res[s[i]%k];
++res[s[i]%k];
}
cout<<ans+res[0]<<endl;
return 0;
}
2.统计子矩阵:
对于这一个题目,乍一看n,m都最大只有五百,但是如果要通过前缀加上暴力进行解答的话是O(n^4)的复杂度,所以需要考虑如何进行优化,这道题目需要使用二位前缀和,我们不妨考虑怎样能通过较小的时间复杂度去枚举出来所有的矩阵,那么我们不妨考虑使用双指针的方式进行枚举每一种可能:
这样就是O(n^3)的时间复杂度,我们就能够接受了,那么我们就要考虑代码如何写了,怎样来进行枚举可以考虑进去所有的情况,下面就是代码实现:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=5e2+10;
int n,m,k;
int a[N][N];
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>m>>k;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
a[i][j] += a[i][j-1]+a[i-1][j]-a[i-1][j-1];
}
}
ll ans=0;
for(int i=1;i<=m;i++){
for(int j=i;j<=m;j++){
for(int s=1,t=1;t<=n;t++){
while(s<=t && a[t][j]-a[s-1][j]-a[t][i-1]+a[s-1][i-1] >k ) ++s;
if(s<=t) ans += t-s+1;
}
}
}
cout<<ans<<endl;
return 0;
}
请注意一点,就是a数组是不能开成long long的会超时,ans需要开成ll,前者如果开成ll,会多一个常数的时间复杂度,也就是O(n)变成O(kn) k是一个大于1的常数。
3.最佳牛围栏:
这道题的题意还是比较容易理解的,我们需要找到连续长度大于等于f的平均值最大的子序列乘以一千的结果,首先容易观察到n的最大值是1e5,f的最小值为1,所以暴力肯定是不可以的,那么我们就要考虑一下怎么来优化时间复杂度了。
首先我们考虑,假设存在一个已知的平均值average,那么如果最优解为x,那么当average<=x的时候,必定存在一段序列其平均值是大于等于average的,基于此,考虑使用二分(对于二分,二分是二分性而不是单调性 只要满足可以找到一个值一半满足一半不满足即可 而不用满足单调性)。
知道了这道题的算法之后,我们就来分析这道题
首先我们二分针对的是平均数,那么根据我们就可以捏出以下主函数:
#include <cstdio>
#include <iostream>
using namespace std;
const int N = 100005;
int cows[N]; double sum[N];
int n, m;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>m;
double l = 0, r = 0;
for (int i = 1; i <= n; i++) {
cin>>cows[i];
r = max(r, (double)cows[i]);
}
while(r - l > 1e-5) {
double mid = (l + r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
cout<<(int)(r*1000);
return 0;
}
二分最难的地方就在于check函数的写法,我们先来捋一遍思路,防止写代码的时候思路混乱
①:我们要找的是 有没有一段不小于F的区间,使这段区间的平均数尽可能的大,如果我们找到了一段连续的区间且区间长度不小于F且平均数大于我们二分的平均数 那么大于这个数且区间也满足的一定满足了 我们直接判断正确即可
②:因为我们要找一段区间的平均数,根据平均数的一个基本应用,显而易见,对于一段序列,每个数减去我们所算的平均数,如果大于0 那么他本身就大于平均数,如果小于0 那么它本身就小于平均数 此时我们就能算出哪些数大于0 哪些数小于0 ,之后我们再使用前缀和,就能判断一个区间内的平均值是否大于或小于我们二分的平均数了。
③:据②我们还可以继续优化,因为我们不仅需要找F大小区间内,我们还要找>F大小区间内的,我们如果用二次for太费时间了,我们这里可以使用双指针的做法。
代码实现如下:
bool check(double avg) {
for (int i = 1; i <= n; i++) {
sum[i] = sum[i - 1] + (cows[i] - avg); //计算前缀和
}
double minv = 0; //设置最小值
for (int i = 0, j = m; j <= n; j++, i++) {
minv = std::min(minv, sum[i]); //找最优极小值
if(sum[j] - minv >= 0) return true; //进行判断
} return false; //如果所有的都不满足,那么这个平均数就一定不满足
}
完整代码如下:
#include <cstdio>
#include <iostream>
using namespace std;
const int N = 100005;
int cows[N]; double sum[N];
int n, m;
bool check(double avg) {
for (int i = 1; i <= n; i++) {
sum[i] = sum[i - 1] + cows[i] - avg;
}
double minv = 0;
for (int i = 0, j = m; j <= n; j++, i++) {
minv = std::min(minv, sum[i]);
if(sum[j] - minv >= 0) return true;
}
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>m;
double l = 0, r = 0;
for (int i = 1; i <= n; i++) {
cin>>cows[i];
r = max(r, (double)cows[i]);
}
while(r - l > 1e-5) {
double mid = (l + r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
cout<<(int)(r*1000);
return 0;
}