[NEFU ACM大一暑假集训 解题报告]前缀和与差分
题量略大,所以解题报告和fjy大佬分了一下工
由我负责A-K部分题解(不是AK部分题解啊,哈哈)
后半部分题解(LM+R~V+XYZ)由fjy大佬发布
剩下的题目烦请有余力的同学自行解决
题目
A - Molly’s Chemicals
题目求区间和为m的幂次的子区间个数,也就是要满足下面这个公式。
幂次的话想不到啥特性,但是这题m是固定的那么直接把幂次暴力预处理出来就完事了。大概为1e19级别,题目弄到1e14就够了,所以弄个几十次就能结束。
然后对于这种找满足条件的等式,我们可以很快想到用map来查询有没有配对的值。但是这里的map需要采用类似前缀和的方式来维护查询,而不是直接在1~n里面搜。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int>PII;
#define endl '\n'
int T;
int n,m;
const int N=1e5+10;
LL sum[N],mpow[N];
map<LL,LL>mp;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>sum[i];
sum[i]+=sum[i-1];
}
int cnt=0;
LL mk=1;
if(m==1)mpow[++cnt]=1;
else if(m==-1){mpow[++cnt]=1;mpow[++cnt]=-1;}
else{
while(abs(mk)<1e14+10){
mpow[++cnt]=mk;
mk*=m;
}
}
LL ans=0;
mp[0]++;
for(int i=1;i<=n;i++){
for(int j=1;j<=cnt;j++){
if(mp.find(sum[i]-mpow[j])!=mp.end())ans+=mp[sum[i]-mpow[j]];
}
mp[sum[i]]++;
}
cout<<ans<<endl;
return 0;
}
B - Balanced Substring
找一个最长的连续子串要求0和1的数量相同。
对于数量相同而且只有01两种,我们一般把0和1做差,预处理前缀和。题目要求0和1数量相同那么就是满足条件:
而我们要求的最长的连续字串,考虑区间对答案的贡献为我们固定右端点i,然后找最小的满足条件的j,使得长度最大。
类似A题,我们用map维护i前面,满足条件的j的最小值。维护满足条件的最小值,只需要从前往后遍历一边,把第一个满足条件的标记即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int>PII;
#define endl '\n'
const int N=100005;
int T;
int t[N];
map<int,int>mp;
int main(){
int n;cin>>n;
string s;cin>>s;
if(s[0]=='1')t[0]=1;
else t[0]=-1;
for(int i=1;s[i];i++){
if(s[i]=='1')t[i]=t[i-1]+1;
else t[i]=t[i-1]-1;
}
for(int i=0;i<n;i++){
if(mp.find(t[i])==mp.end()){
mp[t[i]]=i;
}
}
int ans=0;
for(int i=0;i<n;i++){
if(mp[t[i]]!=i)ans=max(ans,i-mp[t[i]]);
if(t[i]==0)ans=max(ans,i+1);
}
cout<<ans;
return 0;
}
C - White Lines
行和列没分开处理,写公式写吐了,后来看别人的发现分开处理会好很多
参考这篇大小的网格,有黑白两种颜色,要求使用的橡皮擦擦一次,求最多的白线(全为白的行和列)数目。
首先读入地图,黑色为1,白色为0。
最暴力的方法是枚举橡皮擦起点,然后判是否构成白线算贡献,复杂度为
考虑优化算贡献的部分:
我们可以预处理两个数组row[i][j],col[i][j]表示i行j列左边和上边分别有多少黑块
我们先把本来就是白线的部分算出来
条件为:
if(row[i][n]==0)cnt++;
if(col[i][n]==0)cnt++;
然后前缀和维护覆盖后的白线数列
维护的是前i行第j个字符到第个字符被覆盖后,组成的白线数量
维护的是前j列第i个字符到第个字符被覆盖后,组成的白线数量
如何判断一段被覆盖后这行/列为白线?这一段区间内黑块数量==整行/列黑块数列
最后枚举橡皮擦左上角起点就可以了
区域的贡献为
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int>PII;
#define endl '\n'
int T;
const int N=2005;
char a[N][N];
int g[N][N],row[N][N],col[N][N];
int rsum[N][N],csum[N][N];
int main(){
int n,k;cin>>n>>k;
for(int i=1;i<=n;i++)scanf("%s",a[i]+1);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
row[i][j]+=row[i][j-1]+(a[i][j]=='B');
col[j][i]+=col[j][i-1]+(a[i][j]=='B');
}
LL cnt=0;
for(int i=1;i<=n;i++){
if(row[i][n]==0)cnt++;
if(col[i][n]==0)cnt++;
}
for(int i=1;i<=n;i++){
for(int j=1;j+k-1<=n;j++){
rsum[i][j]=rsum[i-1][j]+(row[i][j+k-1]-row[i][j-1]==row[i][n]&&row[i][n]!=0);
csum[i][j]=csum[i-1][j]+(col[i][j+k-1]-col[i][j-1]==col[i][n]&&col[i][n]!=0);
}
}
LL ans=0;
for(int i=1;i+k-1<=n;i++){
for(int j=1;j+k-1<=n;j++){
ans=max(ans,cnt+rsum[i+k-1][j]-rsum[i-1][j]+csum[j+k-1][i]-csum[j-1][i]);
}
}
cout<<ans<<endl;
return 0;
}
D - Monitor
n×m 区域,有p个长方形,q次询问,每次询问一个区域能否完全地被p个长方形覆盖。
本题卡空间,所以使用vector来写,当然也可以采用坐标hash+map的方式压缩空间。
实际上vector目前在大部分评测平台上,性能和数组基本差不了太多,开完优化后基本一致
思路比较简单,先用二维差分标记p个长方形,然后通过一次前缀和预处理出来。
判断一个区域是否被完全覆盖,我们可以把被覆盖的点都赋值为1,若这个区域被全部覆盖,那么这个区域的区间和等于这个区域的面积。
因此在第一步前缀和预处理后,我们还需要把被覆盖的点值处理为1
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int>PII;
#define endl '\n'
int T;
int main(){
int n,m,p;
while(cin>>n>>m>>p){
vector<vector<LL>> a(n + 5, vector<LL>(m + 5, 0));
while(p--){
int x1,x2,y1,y2;scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
a[x1][y1]++;a[x2+1][y1]--;a[x1][y2+1]--;a[x2+1][y2+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 i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(a[i][j]>0)a[i][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];
int q;scanf("%d",&q);
while(q--){
int x1,x2,y1,y2;scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
int sum=a[x2][y2]-a[x1-1][y2]-a[x2][y1-1]+a[x1-1][y1-1];
if(sum>=(x2-x1+1)*(y2-y1+1))puts("YES");
else puts("NO");
}
}
return 0;
}
E - Finding Seats
求一个最小的子矩阵,使得区间和>=k
对于满足条件的最小子矩阵和最大子矩阵,我们的一个思路是把二维问题转化为一维度问题。
我们可以通过合并连续的几行,把他转为一维问题:求最大短连续子序列使得区间和>=k。
对于一维问题,我们容易想到用双指针来解决(尺取法训练A题)
我们枚举合并哪几行,然后维护更新答案即可
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int>PII;
#define endl '\n'
int T;
const int N=305;
int n,m,k;
LL s[N][N],g[N][N],merge_mat[N],merge_sum[N];
char a[N][N];
void get_merge(int st,int ed){
memset(merge_mat,0,sizeof merge_mat);
memset(merge_sum,0,sizeof merge_sum);
for(int i=1;i<=m;i++){
merge_mat[i]=s[ed][i]-s[ed][i-1]-s[st-1][i]+s[st-1][i-1];
merge_sum[i]=merge_sum[i-1]+merge_mat[i];
}
}
LL get_len(){//求和>=k的最短连续序列
LL ans=1e9+7;
for(int i=1,j=1;i<=m;i++){
while(j<=i&&merge_sum[i]-merge_sum[j-1]>=k)j++;
if(merge_sum[i]-merge_sum[j-2]>=k)ans=min(ans,(LL)(i-(j-1)+1));
}
if(ans==1e9+7)return -1;
return ans;
}
int main(){
while(~scanf("%d%d%d",&n,&m,&k)){
if(n==0&&m==0&&k==0)break;
for(int i=1;i<=n;i++)scanf("%s ",a[i]+1);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
if(a[i][j]=='.')g[i][j]=1;
else g[i][j]=0;
s[i][j]=s[i][j-1]+s[i-1][j]+g[i][j]-s[i-1][j-1];
}
LL ans=n*m;
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++){
get_merge(i,j);//合并i和j列
LL len=get_len();
if(len!=-1)ans=min(ans,(j-i+1)*len);
}
cout<<ans<<endl;
}
return 0;
}
F - The Number of Products
给你一个非零序列,求满足区间积为正以及区间积为负的区间数量。
前缀积和前缀和有着相似的性质。问题涉及乘积的正负,我们可以联想到这与区间中负数的奇偶性有关。
负数为奇数,区间为负数,负数为偶数,区间为偶数。因此我们首先预处理负数数量的前缀和。
也就是要满足
sum[r]-sum[l-1]=奇数
sum[r]-sum[l-1]=偶数
对于奇偶性,我们考虑加减法和乘法两种形式。
在这里乘法比加减法容易得多,因为只需要奇偶相异就是负数。
也就是说下面两个情况是等效的:
sum[r]奇数,sum[l]偶数
sum[r]偶数,sum[l]奇数
我们只关心l和r配对是不是一奇一偶,不用受到前缀这一定义的限制,我们直接统计sum[0]~sum[n]有多少奇数多少偶数即可。(如果加减法就需要考虑前缀的限制,就变得很麻烦)
负数的答案为
正数只需用把所有配对方式减去负数的答案即可,答案为
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int>PII;
#define endl '\n'
const int N=2e5+10;
int T;
int sum[N],odd[N],even[N];
int main(){
LL n;cin>>n;
for(int i=1,x;i<=n;i++){
cin>>x;
if(x<0)sum[i]=sum[i-1]+1;
else sum[i]=sum[i-1];
}
LL odd=0,even=0;
for(int i=0;i<=n;i++){
if(sum[i]%2)odd++;
else even++;
}
cout<<odd*even<<" "<<(n+1)*n/2-odd*even<<endl;
return 0;
}
G - 最大子矩阵
求区间和最大的子矩阵。
板子题,预处理前缀和后枚举更新就完事了。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int>PII;
#define endl '\n'
int T;
const int N=1005;
LL s[N][N],g[N][N];
int main(){
scanf("%d",&T);
while(T--){
int n,m,x,y;scanf("%d%d%d%d",&n,&m,&x,&y);
memset(g,0,sizeof g);
memset(s,0,sizeof s);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%lld",&g[i][j]);
s[i][j]=s[i-1][j]+s[i][j-1]+g[i][j]-s[i-1][j-1];
}
}
LL ans=0;
for(int i=1;i+x-1<=n;i++){
for(int j=1;j+y-1<=m;j++){
ans=max(ans,s[i+x-1][j+x-1]-s[i+x-1][j-1]-s[i-1][j+y-1]+s[i-1][j-1]);
}
}
printf("%lld\n",ans);
}
return 0;
}
H - Color the ball
每次给一个区间的数加一,查询一个数的值。
差分板子题,差分数组预处理标记,前缀和还原即可
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int>PII;
#define endl '\n'
int T;
const int N=1e6+10;
int b[N];
int main(){
int n;
while(~scanf("%d",&n),n){
memset(b,0,sizeof b);
int sum=0;
for(int i=1;i<=n;i++){
int l,r;scanf("%d%d",&l,&r);
b[l]++,b[r+1]--;
}
for(int i=1;i<n;i++){
sum+=b[i];
printf("%d ",sum);
}
printf("%d\n",sum+b[n]);
}
return 0;
}
I - Subsequence
区间和>=k的最短连续子序列,尺取法训练A题,不讲了
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
typedef long long LL;
typedef pair<int,int>PII;
#define endl '\n'
int T;
const int N=1e5+10;
LL sum[N],a[N];
int main(){
scanf("%d",&T);
while(T--){
LL n,k;scanf("%lld%lld",&n,&k);
memset(sum,0,sizeof sum);
for(int i=1;i<=n;i++){
scanf("%lld",&sum[i]);
sum[i]+=sum[i-1];
}
LL ans=1e9+7;
for(int i=1,j=1;i<=n;i++){
while(j<=i&&sum[i]-sum[j-1]>=k)j++;
if(sum[i]-sum[j-2]>=k)ans=min(ans,(LL)i-j+2);
}
if(ans!=1e9+7)cout<<ans<<endl;
else cout<<0<<endl;
}
return 0;
}
J - Best Cow Fences
建议看蓝书(算法竞赛进阶指南),二分答案求最值转为存在性问题。
给一个正整数序列A,求平均值最大的,长度不小于L的连续子段。
即:在长度不小L这一限制下,让连续子段平均值最大。
最值问题转化为存在性问题:最大,就是不存在某一个操作比当前操作获得答案更大了。
我们二分答案,对平均值做二分。
接下里的问题是如何快速判断是否有满足条件的解。
我们可以数列中每个数都减去二分的之,就转化为判定是否存在一个长度不小于L的子段和非负。
存在非负也就是说,长度不小于L的最大子段和>=0。
在sum[0]~sum[n]找 令最大的并且满足的i和j
类似B题,我们枚举i,在的范围内找最小的,更新答案
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
typedef long long LL;
typedef pair<int,int>PII;
#define endl '\n'
int T;
const int N=1e6+10;
const double eps=1e-5;
double a[N],b[N],sum[N];
int main(){
int n,len;scanf("%d%d",&n,&len);
for(int i=1;i<=n;i++)scanf("%lf",&a[i]);
double l=-1e6,r=1e6;
while(r-l>eps){
double mid=(l+r)/2;
for(int i=1;i<=n;i++)b[i]=a[i]-mid;
for(int i=1;i<=n;i++)sum[i]=sum[i-1]+b[i];
double ans=-1e10;
double min_val=1e10;
for(int i=len;i<=n;i++){
min_val=min(min_val,sum[i-len]);
ans=max(ans,sum[i]-min_val);
}
if(ans>=0)l=mid;
else r=mid;
}
cout<<int(r*1000)<<endl;
return 0;
}
K - Cornfields
感觉不应出现在这个专题里
要快速查询k*k区域内最大值和最小值差值。
多次区间最值查询我们可以想到RMQ,一般有下面这几种
我们选用单调队列(滑动窗口)
一维度滑动窗口解决这一类问题:预处理出不同起点,长度固定的区间内的最值。
这道题是二维的,那么二维度滑动窗口怎么写?
我们只需要先对行做滑动窗口,然后把行合并,再对列求滑动窗口即可。具体原理和实现各位可以参考这道题目的题解(理想的正方形)
fjy大佬用优先队列写的,你们可以问问他。
搞笑的是ph大佬写了个朴素暴力过去了hh,数据太弱了。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 250;
int row_min[N][N],row_max[N][N],res[N][N];
//row_min[i][j]表示当前第i行,第[j - m + 1,j]这个区间的最小值,row_max同理
int n,k,Q; //表示n * m的矩阵,选出k * k矩阵的最大整数和最小整数的差值”的最小值
int w[N][N];
int q[N]; //优先队列
void get_min(int a[], int b[], int tot){
int hh = 0, tt = -1;
for (int i = 1; i <= tot; i ++ )
{
if (hh <= tt && q[hh] < i - k + 1) hh ++ ;
while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;
q[++tt] = i;
b[i] = a[q[hh]];
}
}
void get_max(int a[], int b[], int tot)
{
int hh = 0, tt = -1;
for (int i = 1; i <= tot; i ++ )
{
if (hh <= tt && q[hh] <= i - k) hh ++ ;
while (hh <= tt && a[q[tt]] <= a[i]) tt -- ;
q[ ++ tt] = i;
b[i] = a[q[hh]];
}
}
int main(){
cin >> n >> k >> Q;
for(int i = 1;i <= n;++i){
for(int j = 1;j <= n;++j){
scanf("%d",&w[i][j]);
}
}
for(int i = 1;i <= n;++i){
get_min(w[i],row_min[i],n);
get_max(w[i],row_max[i],n);
}
int ans[N],col_min[N],col_max[N];
for(int j = k;j <= n;++j){ //枚举列
for(int i = 1;i <= n;++i) ans[i] = row_min[i][j];
get_min(ans,col_min,n);
for(int i = 1;i <= n;++i) ans[i] = row_max[i][j];
get_max(ans,col_max,n);
for(int i = k;i <= n;++i){
res[i][j]=col_max[i]-col_min[i];//以i,j为右下角的长度为k的矩形
}
}
while(Q--){
int x,y;cin>>x>>y;
cout<<res[x+k-1][y+k-1]<<endl;
}
return 0;
}