【蓝桥杯】每日连续 Day9 前缀和
目录
前言
最佳牛围栏
分析
代码
壁画
分析
代码
K倍区间
分析
代码
统计子矩阵
分析
代码
递增三元组
分析
代码
激光炮弹
分析
代码
总结
前言
虽迟但到,主播今天去打了一下蓝桥杯的月赛,只ac
了三道,鉴定为省三QAQ
。
今天为大家带来了一道二分的题目和三道前缀和的题目,话不多说我们马上开始吧。
最佳牛围栏
分析
看见牛就知道来自USACO
,就知道题目不会很简单。
发现问题可以用遍历来解决,考虑一下能不能二分。
先来尝试二分围起来的田地数量,很显然不满足单调性。
随后我们来尝试二分答案,能否二分平均值然后判断能否满足条件呢?
我们假设存在最大平均值m
。
很明显对于平均值小于等于m
的情况皆可以满足,而对于平均值大于m
的情况则不可以满足(这道题因为max
是一种包含关系,所以可以用二分)。
所以我们通过二分来引入平均值m
。
接下来只需要判断是否存在平均值大于等于m
即可。
但若挨个计算区间的平均值的话很显然是会超时的,需要优化。
如何来优化呢?
第一步,我们先将平均值计算的一步优化:
对于计算平均值显然是不可以直接使用前缀和的,我们列出式子
S / n <= m
移项
S <= m * n
将S
拆分
a1 + a2 + ... + an <= m + m + ... + m
每一项减去m
a1 - m + a2 - m + ... + an - m <= 0
因为每次的m
都是确定的,属于离线问题,所以可以通过前缀和来优化。
第二步,对区间枚举进行优化:
假设我们选定区间s
,根据题目要求s
的长度是要大于等于k
的,我们将他转换成前缀减前缀
sj - si
,为方便大家理解我画了图出来:
因为max
是一种包含关系,所以我们不需要将每个区间的确切值算出来比较,只需要判断大小便好。
借助动态规划的思想,我们取min(si)
,即所有区间长度小于等于i
的所有前缀的最小值。随后判断sj - min(si)
的正负便好。
代码
// 二分答案
#include<iostream>
using namespace std;
const int N = 100010;
const double INF = 1e-6;
int n, f;
int nums[N];
double s[N];
bool func(double x)
{
//cout << x << endl;
for(int i = 1; i <= n; i++)
{
s[i] = s[i - 1] + nums[i] - x;
//cout << s[i] << ' ';
}
//puts("");
double l = 0;
for(int i = 1; i <= n - f + 1; i++)
{
//cout << l << ' ';
l = min(l, s[i - 1]); //寻找最小前缀
if(s[i + f - 1] - l >= 0) //存在至少一种可能可以使得区间大于0
return true;
}
//puts("");
return false;
}
int main()
{
scanf("%d%d", &n, &f);
for(int i = 1; i <= n; i++)
{
scanf("%d", nums + i);
}
double l = 0, r = 2000;
while(r - l > INF)
{
double x = (l + r) / 2;
if(func(x)) //可以满足
l = x;
else //不能满足
r = x;
}
printf("%d", (int)(r * 1000));
return 0;
}
壁画
分析
这其实是一道思维题。先来给大家叙述一遍题目
给n
段连续的墙体。
t
第一次可以任选一个地方画画,但是在之后只能选择与已经画了的地方的相邻位置画画(也就是一个连续的区间)。
每天结束后都会有一段墙面崩溃,且只会崩溃只与一段还未毁掉的墙面相邻的墙面(其实就是前缀和后缀)。
且崩溃无法蔓延到已经画过画的部分。
先来考虑极限情况,第一次在端点作画。
由于t
是先手,所以这种情况t
可以画画ceil(n/2)
次。
接下来我们来思考一般情况,来分析是不是一定可以画到ceil(n/2)次
(t
是绝顶聪明的,只判断可行性。)
我们假设t
画到了ceil(n/2)
次,如图:
对两边画出沿交界点对称的区间。
第一次选择两个区间的中间进行画画,随后我们可以发现只要我们每次选择摧毁的那个方向画画,最后就一定能画满ceil(n/2)
次。
所以我们只需要查找所有长度为ceil(n/2)的区间的最大值即可。
要进行离线的区间和操作,使用前缀和。
代码
// 毁坏程度与绘画天数相关联,枚举起始位置
#include<iostream>
using namespace std;
const int N = 5000010;
char a[N];
int s[N];
int t, n, l;
int func(int x) //根据初始位置计算可行天数
{
int l = (n + 1)/2;
return s[x + l - 1] - s[x - 1];
}
int main()
{
scanf("%d", &t);
for(int k = 1; k <= t; k++)
{
scanf("%d%s", &n, a + 1);
for(int i = 1; i <= n; i++)
s[i] = s[i - 1] + a[i] - '0'; //前缀和
int l = 0;
for(int i = 1; i <= n / 2 + 1; i++) //枚举起点,找最大值即可
l = max(l, func(i));
printf("Case #%d: %d\n",k, l);
}
return 0;
}
K倍区间
分析
要求区间和,我们使用前缀和,可得
(Sj - Si) mod K == 0
。
根据同余定理变形可得
Sj mod K == Si mod K
。
所以我们只需要枚举Sj
,每次记录与他同余的Si
的数量便好。(mod == 0
需要单独计算)
代码
// 暴力O(n ^ 2),
// S % k == 0, 将S拆分——(Sj - Si)% k == 0 <---> Sj % k = Si & k转化成同余问题
// 而题目要求连续的区间,转换成后缀减去前缀,所以直接打表即可
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N];
LL s[N];
int st[N];
int n, k;
int main()
{
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; i++)
{
scanf("%d", a + i);
s[i] = s[i - 1] + a[i];
}
LL l = 0;
for(int i = 1; i <= n; i++)
{
int m = s[i] % k;
if(m == 0) l++;
l += st[m]++;
}
printf("%lld", l);
return 0;
}
统计子矩阵
分析
直接枚举矩阵的话时间复杂度为O(n ^ 4)
,很显然会超时。
考虑优化,我们发现每个点的数值都是大于0
的,即矩阵和会随矩阵的增大而增大,具有单调性。
但是在二维上利用单调性显然是非常复杂的,如何简化问题呢?
降维!我们将枚举两个点拆分为上下边界和左右边界。
我们先对每一列进行前缀和操作,随后每次先枚举上下边界。
问题转换为了区间求和问题,显然暴力枚举区间的话还是会超时,如何优化呢?
前面说了区间具有单调性,我们需要的只是满足条件的那一部分,那么有没有一种算法可以完美的避开不能满足要求的那一部分呢?
想到这里,四个大字从主播的脑海里腾空而起——滑动窗口。
代码
// 暴力的话O(n ^ 4)超时
// 观察矩阵越大和越大,满足单调性,考虑双指针优化
#include<iostream>
using namespace std;
const int N = 510;
typedef long long LL;
int n, m, k;
int a[N][N], s[N][N];
LL cnt;
int main()
{
scanf("%d%d%d", &n, &m, &k);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
scanf("%d", &a[i][j]);
s[i][j] = s[i - 1][j] + s[i][j - 1] + a[i][j] - s[i - 1][j - 1]; // 合并两个区间,容斥原理
}
for(int i = 1; i <= n; i++)
for(int j = i; j <= n; j++)
{ //确定区间的上届和下届
// 滑动窗口
int l = 1, r = 0;
while(++r <= m)
{
while(l <= r && s[j][r] - s[j][l - 1] - s[i - 1][r] + s[i - 1][l - 1] > k)
l++;
if(l <= r) cnt += r - l + 1;
}
}
cout << cnt;
return 0;
}
递增三元组
分析
显然暴力枚举的话会超时,需要优化。
我们将不等式拆分:
Ai < Bj
Bj < Ck
先来算下面的不等式。
要计算满足Bj < Ck
,的二元组的数量可以先将B
和C
排序。
随后通过双指针计算出对于每一个Bj
,满足的Cj
的数量。
我们便得出了下面的不等式的数量,那么如何代入上面的不等式呢?
由于<
的包含性,所以对于每个A
,我们计算的是满足Bj < Cj
二元组上的一个后缀。
所以我们先对计算出来的二元组进行前缀和操作。
随后排序A
,利用双指针计算即可。
代码
// 暴力的话O(n ^ 3),会超时,考虑优化
// 随后我们发现可以排序,通过双指针逐层计算。想不出来和前缀和有什么关联
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int a[N], b[N], c[N];
int st[N];
LL s[N];
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++)
scanf("%d", a + i);
for(int i = 1; i <= n; i++)
scanf("%d", b + i);
for(int i = 1; i <= n; i++)
scanf("%d", c + i);
sort(a + 1, a + n + 1); sort(b + 1, b + n + 1); sort(c + 1, c + n + 1);
for(int i = 1, j = 1; i <= n && j <= n; i++)
{
while(j <= n && c[j] <= b[i]) j++;
st[i] = n - j + 1; //计算出b上每个点满足要求的数量
} //前缀和
for(int i = 1; i <= n; i++)
s[i] = s[i - 1] + st[i];
LL l = 0;
for(int i = 1, j = 1; i <= n && j <= n; i++)
{
while(j <= n && b[j] <= a[i]) j++;
l += s[n] - s[j - 1];
}
printf("%lld", l);
return 0;
}
激光炮弹
分析
要计算矩阵和,使用二维前缀和。(这道题主要是考察细节,空间上比较接近上限,要严格判断每个数据会不会溢出)
代码
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 5010;
int n, r;
int s[N][N], l;
int main()
{
scanf("%d%d", &n, &r);
for(int i = 0; i < n; i++)
{
int x, y, w;
scanf("%d%d%d", &x, &y, &w);
s[x + 1][y + 1] += w;
}
for(int i = 1; i < N; i++)
for(int j = 1; j < N; j++)
s[i][j] += s[i - 1][j] + s[i][j - 1] -s[i - 1][j - 1];
if(r >= N)
{
printf("%lld", s[N - 1][N - 1]);
return 0;
}
for(int i = 1; i + r - 1 < N; i++)
for(int j = 1; j + r - 1 < N; j++) //枚举起点
l = max(l, s[i + r - 1][j + r - 1] - s[i + r - 1][j - 1] -
s[i - 1][j + r - 1] + s[i - 1][j - 1]);
printf("%lld", l);
return 0;
}
总结
困QAQ。主播要休息了,明天是差分和双指针。