[Codeforce526F]:Pudding Monsters(分治)
题目传送门
题目描述
由于各种原因,桐人现在被困在Under World(以下简称UW)中,而UW马上要迎来最终的压力测试——魔界入侵。
唯一一个神一般存在的Administrator被消灭了,靠原本的整合骑士的力量 是远远不够的。所以爱丽丝动员了UW全体人民,与整合骑士一起抗击魔族。
在UW的驻地可以隐约看见魔族军队的大本营。整合骑士们打算在魔族入侵前发动一次奇袭,袭击魔族大本营!
为了降低风险,爱丽丝找到了你,一名优秀斥候,希望你能在奇袭前对魔族 大本营进行侦查,并计算出袭击的难度。
经过侦查,你绘制出了魔族大本营的地图,然后发现,魔族大本营是一个$N \times N$的网格图,一共有N支军队驻扎在一些网格中(不会有两只军队驻扎在一起)。
在大本营中,每有一个$k \times k$(1≤k≤N)的子网格图包含恰好k支军队,我们袭 击的难度就会增加1点。
现在请你根据绘制出的地图,告诉爱丽丝这次的袭击行动难度有多大。
输入格式:
第一行,一个正整数N,表示网格图的大小以及军队数量。
接下来N行,每行两个整数,$X_i$,$Y_i$,表示第i支军队的坐标。
保证每一行和每一列都恰有一只军队,即每一个$X_i$和每一个$Y_i$都是不一样的。
输出格式:
一行,一个整数表示袭击的难度。
样例:
样例输入:
5
1 1
3 2
2 4
5 5
4 3
样例输出:
10
数据范围与提示:
样例解释:
显然,分别以(2,2)和(4,4)为左上,右下顶点的一个子网格图中有3支军队,这为我们的难度贡献了1点。类似的子网格图在原图中能找出10个。
数据范围:
对于30%的数据,N≤100。
对于60%的数据,N≤5000。
对于100%的数据,N≤50000。
注意题中一句话,得语文者得天下:
保证每一行和每一列都恰有一只军队,即每一个$X_i$和每一个$Y_i$都是不一样的。
(然而当时的我并没有看见……)
题解:
$O( N^5 )$:
考虑暴力枚举,$N^2$枚举左下角位置,$O(N)$枚举正方形边长,$N^2$暴力求和,与边长比较,统计答案。
期望得分:27分。
$O( N^4 )$:
考虑上面红色字的含义,即可把这道题转化为:给定N个书的一个排列,问这个序列中有多少个子区间的数恰好是连续的。
那么,我们就可以将$O( N^5 )$算法压一维,同样是$N^2$枚举左右端点位置,$O(N)$枚举正方形边长,但是暴力求和的时候只需要$O(N)$把这一段区间扫一遍即可。
期望得分:27分。
$O(N^3)$:
又不用考虑上面红色字的含义了,维护一下$O( N^5 )$算法的前缀和,就能将$N^2$暴力求和压掉,就做到了$O(N^3)$。
期望得分:27分。
$O(N^2)$:
又得考虑上面红色字的含义了,认真想一想,题目就转化成了:有多少种情况使得相邻的k个数中最大值和最小值的差为k-1。
那么,我们可以维护区间的最大值和最小值,然后进行处理,还是$N^2$枚举左右端点位置,但是直接用这段区间的最大值减去最小值,将它与k做比较,相同则ans++。
至于最大值最小值,可以在枚举左端点的时候清空,然后在枚举右端点的时候暴力更新;也可以使用ST算法,$N \log N$预处理,$O(1)$查询,其实没必要,主要是说一下这种思维。
期望得分:
暴力更新:64分。
ST算法:55分。
$O(N^2)Pro$:
考虑对$O(N^2)$算法进行优化,如果要满足当前长度为k的区间里所有的数都是连续的k个数,那么如果当前枚举右端点的时候扫到的点不是当前区间的最小值,而比它大1的数却在左端点左边,那么以后的一定都不能满足了,直接break掉,枚举下一个左端点就好了,这个点不是当前区间的最大值时同理。
期望得分:91分。
$O(N^2)Pro+$:
考虑卡常,使用register,fread快读。
期望得分:91分。
$O(N \log N)||O(N \log^2 N)$:
两种解法:分治和线段树。
在这里主要讲一下分治:
对于当前分治的区间[l,r],设其中点为mid。
当前区间的答案即为:
$ans[l,r]=ans[l,mid]+ans[mid+1,r]+$跨过中点的合法方案数。
计算跨过中点的合法方案数时,分一下四种情况:
1.最大值和最小值都在左侧。
2.最大值和最小值都在右侧。
3.最小值在左侧,最大值在右侧。
4.最小值在右侧,最大值在左侧。
然后我们发现,情况1和情况3对称,情况2和情况4对称,所以下面我们只考虑情况1和情况3。
对于情况1,我们枚举左边界,然后可以计算出右边界的位置,再判断是否合法,统计答案,时间复杂度$O(N)$。
对与情况3,如果一个区间合法的话就一定满足:
$\max (a[mid+1]...a[r])- \min (a[l]...a[mid])=r-l$
移项得:
$\max (a[mid+1]...a[r]-r= \min (a[l]...a[mid])-l$
然后利用单调栈和桶来完成这些操作,代码实现较为复杂,时间复杂度$O(N)$。
考虑上二分区间的时候的$\log N$,总的时间复杂度即为$O(N \log N)$。
因为情况1和情况3对称,情况2和情况4对称,为了降低码长我们可以使用algorithm库里的reverse函数,在$\log N$的时间内翻转区间,时间复杂度为$O(N \log^2 N)$。
线段树的思路也是利用单调栈,用线段书进行区间修改和查询,统计答案,时间复杂度$O(N \log N)$。
期望得分:100分。
代码时刻:
$O( N^5 )$:
#include<bits/stdc++.h> using namespace std; int n,a[5001][5001]; int ans; int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { int x,y; scanf("%d%d",&x,&y); a[x][y]=1; } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)//枚举端点 for(int len=1;len<=min(n-i+1,n-j+1);len++)//枚举长度 { int sum=0; for(int k=0;k<len;k++) for(int l=0;l<len;l++)//暴力统计答案 if(a[i+k][j+l])sum++; if(sum==len)ans++; } printf("%d",ans); return 0; }
$O( N^4 )$:
#include<bits/stdc++.h> using namespace std; int n,a[50001],ans; int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { int x,y; scanf("%d%d",&x,&y); a[x]=y; } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)//枚举端点 for(int len=1;len<=min(n-i+1,n-j+1);len++)//枚举长度 { int sum=0; for(int pos=0;pos<len;pos++)//暴力统计答案 { if(a[i+pos]<=j+len-1&&a[i+pos]>=j)sum++; if(sum>len)break; } if(sum==len)ans++; } printf("%d",ans); return 0; }
$O(N^3)$:
#include<bits/stdc++.h> using namespace std; int ans; int Map[5001][5001]; int main() { int n; scanf("%d",&n); for(int i=1;i<=n;i++) { int x,y; scanf("%d%d",&x,&y); Map[x][y]=1; } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) Map[i][j]=Map[i][j]+Map[i-1][j]+Map[i][j-1]-Map[i-1][j-1];//计算前缀和 ans=n; for(int k=2;k<=n;k++)//枚举长度 for(int i=0;i<=n-k;i++) for(int j=0;j<=n-k;j++)//枚举左右端点 if(Map[i+k][j+k]-Map[i+k][j]-Map[i][j+k]+Map[i][j]==k)ans++;//统计答案 printf("%d",ans); return 0; }
$O(N^2)$:
暴力求最大值和最小值:
#include<bits/stdc++.h> using namespace std; int n; int a[50001]; int ans; int maxn,minn; int main() { int n; scanf("%d",&n); for(int i=1;i<=n;i++) { int x,y; scanf("%d%d",&x,&y); a[x]=y; } for(int i=1;i<=n;i++)//枚举左端点 { maxn=0; minn=1<<30;//初始值 for(int j=i;j<=n;j++)//枚举右端点 { maxn=max(maxn,a[j]); minn=min(minn,a[j]);//更新最大值和最小值 if(maxn-minn==j-i)ans++;//统计答案 } } printf("%d",ans); return 0; }
利用ST算法最大值和最小值:
#include<bits/stdc++.h> using namespace std; int n; int maxn[50001][20],minn[50001][20]; int ans; void st(int x) { for(int i=1;i<=16;i++) for(int j=1;j+(1<<i)<=x+1;j++) { maxn[j][i]=max(maxn[j][i-1],maxn[j+(1<<(i-1))][i-1]); minn[j][i]=min(minn[j][i-1],minn[j+(1<<(i-1))][i-1]); } } pair<int,int> query(int l,int r) { int k=log2(r-l+1); return make_pair(max(maxn[l][k],maxn[r-(1<<k)+1][k]),min(minn[l][k],minn[r-(1<<k)+1][k])); } int main() { memset(minn,0x3f,sizeof(minn)); int n; scanf("%d",&n); for(int i=1;i<=n;i++) { int x,y; scanf("%d%d",&x,&y); maxn[x][0]=minn[x][0]=y;//直接存入ST表 } st(n);//ST表初始化 for(int i=1;i<=n;i++) for(int j=i;j<=n;j++) { pair<int,int> flag=query(i,j);//取出当前区间的最大值和最小值 if(flag.first-flag.second==j-i)ans++;//统计答案 } printf("%d",ans); return 0; }
$O(N^2)Pro$:
#include<bits/stdc++.h> using namespace std; int n; int a[50001],flag[50001];//flag存储对应位置 int ans; int maxn,minn; int main() { int n; scanf("%d",&n); for(int i=1;i<=n;i++) { int x,y; scanf("%d%d",&x,&y); a[x]=y; flag[y]=x; } for(int i=1;i<=n;i++) { maxn=0; minn=1<<30; for(int j=i;j<=n;j++) { maxn=max(maxn,a[j]); minn=min(minn,a[j]); if((a[j]!=minn&&flag[a[j]-1]<i)||(a[j]!=maxn&&flag[a[j]+1]<i))break;//剪枝 if(maxn-minn==j-i)ans++; } } printf("%d",ans); return 0; }
$O(N^2)Pro+$:
#include<bits/stdc++.h> using namespace std; const int L(1<<20|1); char buffer[L],*S,*T; #define getchar() ((S==T&&(T=(S=buffer)+fread(buffer,1,L,stdin),S==T))?EOF:*S++)//调试时记得注释 int read() { register int a=0,b=1;register char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')b=-1;ch=getchar();} while(ch>='0'&&ch<='9'){a=(a<<3)+(a<<1)+(ch-'0');ch=getchar();} return a*b; } int n; int a[50001],flag[50001]; int ans; int maxn,minn; int main() { register int n=read(); for(register int i=1;i<=n;i++) { register int x=read(),y=read(); a[x]=y; flag[y]=x; } for(register int i=1;i<=n;i++) { maxn=0; minn=1<<30; for(register int j=i;j<=n;j++) { maxn=max(maxn,a[j]); minn=min(minn,a[j]); if((a[j]!=minn&&flag[a[j]-1]<i)||(a[j]!=maxn&&flag[a[j]+1]<i))break; if(maxn-minn==j-i)ans++; } } printf("%d",ans); return 0; }
$O(N \log^2 N)$:
#include<bits/stdc++.h> using namespace std; int n; int a[50001]; int lmin[50001],lmax[50001],rmin[50001],rmax[50001],barrel[100001];//分别存储左区间最小值,左区间最大值,右区间最小值,右区间最大值,桶。 int ans; int wzc(int l,int r,int mid) { lmin[mid ]=lmax[mid ]=a[mid ]; rmin[mid+1]=rmax[mid+1]=a[mid+1]; for(int i=mid-1;i>=l;i--) { lmin[i]=min(lmin[i+1],a[i]); lmax[i]=max(lmax[i+1],a[i]); } for(int i=mid+2;i<=r;i++) { rmin[i]=min(rmin[i-1],a[i]); rmax[i]=max(rmax[i-1],a[i]); } int flag=0,flag1=mid+1,flag2=mid+1; for(int i=l;i<=mid;i++) { int miao=lmax[i]-lmin[i]+i; if(miao<=r&&mid<miao&&rmax[miao]<lmax[i]&&lmin[i]<rmin[miao])flag++;//最大值和最小值在同侧 } while(flag1<=r&&lmin[l]<rmin[flag1]) barrel[rmax[flag1]-flag1+50000]++,flag1++;//最小值在左侧 while(flag2<=r&&rmax[flag2]<lmax[l]) barrel[rmax[flag2]-flag2+50000]--,flag2++;//最大值在右侧 for(int i=l;i<=mid;i++) { while(flag1>mid+1&&rmin[flag1-1]<lmin[i]) flag1--,barrel[rmax[flag1]-flag1+50000]--; while(flag2>mid+1&&lmax[i]<rmax[flag2-1]) flag2--,barrel[rmax[flag2]-flag2+50000]++; flag+=barrel[lmin[i]-i+50000]>0?barrel[lmin[i]-i+50000]:0; } for(int i=mid+1;i<=r;i++)barrel[rmax[i]-i+50000]=0; return flag; } void dfs(int l,int r)//二分区间 { if(l==r)return; int mid=(l+r)>>1; dfs(l,mid); dfs(mid+1,r); ans+=wzc(l,r,mid); reverse(a+l,a+r+1);//翻转 ans+=wzc(l,r,mid-((r-l+1)&1));//注意mid位置在反转之后会发生改变 reverse(a+l,a+r+1);//记得翻回来 return; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { int x,y; scanf("%d%d",&x,&y); a[x]=y; } dfs(1,n); cout<<ans+n<<endl;//最后记得加n return 0; }
$O(N \log N)$:
#include<bits/stdc++.h> using namespace std; int n; int a[50001]; int lmin[50001],lmax[50001],rmin[50001],rmax[50001],barrel[200001];//桶要稍微开打点 int ans; int wzc(int l,int r,int mid) { lmin[mid ]=lmax[mid ]=a[mid ]; rmin[mid+1]=rmax[mid+1]=a[mid+1]; for(int i=mid-1;i>=l;i--) { lmin[i]=min(lmin[i+1],a[i]); lmax[i]=max(lmax[i+1],a[i]); } for(int i=mid+2;i<=r;i++) { rmin[i]=min(rmin[i-1],a[i]); rmax[i]=max(rmax[i-1],a[i]); } int flag=0,flag1=mid+1,flag2=mid+1; for(int i=l;i<=mid;i++) { int miao=lmax[i]-lmin[i]+i; if(miao<=r&&mid<miao&&rmax[miao]<lmax[i]&&lmin[i]<rmin[miao])flag++; } for(int i=mid+1;i<=r;i++) { int miao=rmin[i]-rmax[i]+i; if(miao>=l&&mid>=miao&&rmax[i]>lmax[miao]&&lmin[miao]>rmin[i])flag++; } while(flag1<=r&&lmin[l]<rmin[flag1]) barrel[rmax[flag1]-flag1+50000]++,flag1++; while(flag2<=r&&rmax[flag2]<lmax[l]) barrel[rmax[flag2]-flag2+50000]--,flag2++; for(int i=l;i<=mid;i++) { while(flag1>mid+1&&rmin[flag1-1]<lmin[i]) flag1--,barrel[rmax[flag1]-flag1+50000]--; while(flag2>mid+1&&lmax[i]<rmax[flag2-1]) flag2--,barrel[rmax[flag2]-flag2+50000]++; flag+=barrel[lmin[i]-i+50000]>0?barrel[lmin[i]-i+50000]:0; } for(int i=mid+1;i<=r;i++)barrel[rmax[i]-i+50000]=0; flag1=flag2=mid; while(flag1>=l&&lmin[flag1]>rmin[r]) barrel[lmax[flag1]+flag1+50000]++,flag1--;//最小值在右侧 while(flag2>=l&&rmax[r]>lmax[flag2]) barrel[lmax[flag2]+flag2+50000]--,flag2--;//最大值在左侧 for(int i=r;i>mid;i--) { while(flag1<mid&&rmin[i]>lmin[flag1+1]) flag1++,barrel[lmax[flag1]+flag1+50000]--; while(flag2<mid&&lmax[flag2+1]>rmax[i]) flag2++,barrel[lmax[flag2]+flag2+50000]++; flag+=barrel[rmin[i]+i+50000]>0?barrel[rmin[i]+i+50000]:0; } for(int i=l;i<=mid;i++)barrel[lmax[i]+i+50000]=0; return flag; } void dfs(int l,int r) { if(l==r)return; int mid=(l+r)>>1; dfs(l,mid); dfs(mid+1,r); ans+=wzc(l,r,mid); return; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { int x,y; scanf("%d%d",&x,&y); a[x]=y; } dfs(1,n); printf("%d",ans+n); return 0; }
rp++