P2468 [SDOI2010]粟粟的书架 题解
题目描述&数据范围
你有一个长r,宽c的矩阵,矩阵上每个格子有权值,有m次询问,每次给出一个矩形左上角\(x1,y1\)和右下角\(x2 ,y2\),以及一个值h,求:在该矩形覆盖的权值和是否大于h,最小需要几个权值就可以大于h。
【数据规模和约定】
对于10%的数据,满足R, C≤10;
对于20%的数据,满足R, C≤40;
对于50%的数据,满足R, C≤200,M≤200,000;
另有50%的数据,满足R=1,C≤500,000,M≤20,000;
对于100%的数据,满足1≤Pi,j≤1,000,1≤Hi≤2,000,000,000。
题目分析
很显然,这道题目是让我们用两种方式去做,也就是:这题是二合一的题目,考虑前50%:
\(r,c\)很小,所以:该矩形很小,我们思考:如何去统计答案?由于要求最少数目,所以会想到二分答案,但是二分答案什么值呢?
考虑后50%,一条链的情况,那么肯定会想到是数据结构,那么用什么数据结构可以轻易从大到小取数且不超时呢?
Part 1 二分
第一种:二分个数
具体:我们二分取的数的个数,然后在该区间从大到小取(保证了选的数目最小),那么我们又会发现:这根本不需要二分,只需要暴力地排序就可以了,很显然,你绝对会T到飞起。那么就必须找一些很神奇的做法。
在启发之下,我也明白了下面这个很巧妙的方法。
第二种:二分k值
由于权值\(p\in[1,1000]\),我们的k的含义就是:小于等于p的值。
那么我们如何用k去统计我们最后的答案呢?
首先明白一件事:我们要做的是从大到小取数,使和大于等于h,那么,k的真正含义就是:我们取的一系列数的下界!
所以我们可以用二维前缀和来维护信息:用\(sum_{i,j,k}\)表示从\((1,1)\)到\((i,j)\)小于等于k的数的和,\(num_{i,j,k}\)表示,小于等于k的数的个数。那么很显然,我们去二分这个K,找到最大的可以恰好满足的,(恰好满足表示:当前这个数,+1就无法满足,-1也可以满足)但是请注意,显然,我们当前这个k的val可能回比要求的h大很多,所以我们需要用\(num_{i,j,k}\)减去不需要的个数.
代码1
void main1(){
for (int i=1;i<=r;i++)
for (int j=1;j<=c;j++)
scanf ("%d",a1[i]+j);
for (int i=1;i<=r;i++)
for (int j=1;j<=c;j++)
for (int k=1;k<=1000;k++){
sum[i][j][k]=sum[i-1][j][k]+sum[i][j-1][k]-sum[i-1][j-1][k]+(a1[i][j]>=k?a1[i][j]:0);
tot[i][j][k]=tot[i-1][j][k]+tot[i][j-1][k]-tot[i-1][j-1][k]+(a1[i][j]>=k?1:0);
//利用前缀和思想,统计当前的sum和num
}
int x1,x2,y1,y2,h;
while(m--){
scanf ("%d%d%d%d%d",&x1,&y1,&x2,&y2,&h);
if (sum[x2][y2][1]+sum[x1-1][y1-1][1]-sum[x2][y1-1][1]-sum[x1-1][y2][1]<h)
//特判,判断所有数加起来是否可以,若仍小于,则无解
printf ("Poor QLW\n");
else{
int l=1,r=1000,ans=0;
while(l<=r){
//二分
int mid=l+r>>1;
if(value(x1,y1,x2,y2,mid)>=h)
l=mid+1,
ans=mid;
//注意!!!这里的ans记录的是查询到的最优的k
else
r=mid-1;
}
//这里就是上文说的:减去多余的
printf("%d\n",number(x1,y1,x2,y2,ans)-(value(x1,y1,x2,y2,ans)-h)/ans);
//考虑为什么可以这样写:我们二分出来的数,一定是和大于等于h中最大且可以取到的数,所以多出来的值一定就是当前这个数,那么我们只需要做差,然后除这个数下取整就可以完美解决。
}
}
return;
}
part 2 主席数维护
考虑我们本题的后50%:
一条链,取区间,查询和,而且:每个点的值非常小,最大为1000,那么显然我们可以想到一个暴力的做法:
优先队列
每次都把区间扔到优先队列里,从大到小,从头到尾,取数。很显然T到飞起,那么我们就需要一个更加优秀的数据结构。经分析当然是主席树更合适(当然,splay完全可以写。实在懒得打代码。。。。。。)
主席树的实现
首先,我们,每个叶子节点存储:
1.值为当前点的数的个数(size)
2.这个数的和(sum)
而非叶子节点要记录:
1.左右儿子的下标(注意:主席树为了节省空间,并不是像线段树那么直接存储,而是类似动态开点的思想)
2.它的左右边界内数的个数。(size)
3.左右边界内数的和。(sum)
显然:我们的叶子节点从小到大,可以满足从大到小选的性质。
具体解决:
-
先离散化(由于本题最大为1000,也可以不离散化,但是我离散化了......)
-
建树,一个点一个点插入,记录每次插入的根的下标(历史版本)
-
用r和l-1来做处理(每个r版本上的点的信息,减去l-1这个历史版本上的信息,进行查询)
-
如果根节点的值小于h,无解,反之:
-
查询右子树,如果可以,继续向下,反之,将右子树取了,走左子树,重复上述步骤,很显然,我们最后有2种情况
-
1.我们拿了某一个右节点刚好等于h。
2.我们拿到了最后一个右叶子节点不够,那么用左叶子节点来补。
代码2
int dfncnt;
inline int build(int l,int r){
int rt=++dfncnt;
int mid=l+r>>1;
if (l<r){
t[rt].lson=build(l,mid);
t[rt].rson=build(mid+1,r);
t[rt].siz+=t[t[rt].lson].siz+t[t[rt].rson].siz;
}
return rt;
}
inline int update(int pre,int l,int r,int x){
int rt=++dfncnt;
t[rt].lson=t[pre].lson;
t[rt].rson=t[pre].rson;
t[rt].siz=t[pre].siz+1;
t[rt].sum=t[pre].sum+b[x];
if (l<r){
int mid=l+r>>1;
if (x<=mid)
t[rt].lson=update(t[pre].lson,l,mid,x);
else
t[rt].rson=update(t[pre].rson,mid+1,r,x);
}
return rt;
}
inline int query(int u,int v,int l,int r,int h){
int ans=0;
while(l<r){
int mid=l+r>>1;
long long lcd=t[t[v].rson].sum-t[t[u].rson].sum;
if (lcd<h) ans+=t[t[v].rson].siz-t[t[u].rson].siz,h-=lcd,r=mid,v=t[v].lson,u=t[u].lson;
else l=mid+1,v=t[v].rson,u=t[u].rson;
}
ans+=(h+b[l]-1)/b[l];
return ans;
}
int his[10000005];
void main2(){
for (int i=1;i<=c;i++)
scanf ("%d",a2+i),b[i]=a2[i];
sort(b+1,b+c+1);
int n1=unique(b+1,b+c+1)-b-1;
his[0]=build(1,n1);
for (int i=1;i<=c;i++){
a2[i]=lower_bound(b+1, b+1+n1, a2[i]) - b;
his[i]=update(his[i-1],1,n1,a2[i]);
}
while(m--){
int x1,y1,x2,y2,h;
scanf ("%d%d%d%d%d",&x1,&y1,&x2,&y2,&h);
if (t[his[y2]].sum-t[his[y1-1]].sum<h){
printf("Poor QLW\n");
continue;
}
int ans=query(his[y1-1],his[y2],1,n1,h);
cout<<ans<<endl;
}
return;
}
完整版:
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
struct Tree{
int lson,rson,siz;
long long sum;
}t[20000005];
int r,c,m;
long long a2[500005];
long long b[500005];
long long a1[205][205];
long long sum[205][205][1005];
int tot[205][205][1005];
inline long long value(int x1,int y1,int x2,int y2,int val){
return sum[x2][y2][val]+sum[x1-1][y1-1][val]-sum[x2][y1-1][val]-sum[x1-1][y2][val];
}
inline int number(int x1,int y1,int x2,int y2,int val){
return tot[x2][y2][val]+tot[x1-1][y1-1][val]-tot[x2][y1-1][val]-tot[x1-1][y2][val];
}
void main1(){
for (int i=1;i<=r;i++)
for (int j=1;j<=c;j++)
scanf ("%d",a1[i]+j);
for (int i=1;i<=r;i++)
for (int j=1;j<=c;j++)
for (int k=1;k<=1000;k++){
sum[i][j][k]=sum[i-1][j][k]+sum[i][j-1][k]-sum[i-1][j-1][k]+(a1[i][j]>=k?a1[i][j]:0);
tot[i][j][k]=tot[i-1][j][k]+tot[i][j-1][k]-tot[i-1][j-1][k]+(a1[i][j]>=k?1:0);
}
int x1,x2,y1,y2,h;
while(m--){
scanf ("%d%d%d%d%d",&x1,&y1,&x2,&y2,&h);
if (sum[x2][y2][1]+sum[x1-1][y1-1][1]-sum[x2][y1-1][1]-sum[x1-1][y2][1]<h)
printf ("Poor QLW\n");
else{
int l=1,r=1000,ans=0;
while(l<=r){
int mid=l+r>>1;
if(value(x1,y1,x2,y2,mid)>=h)
l=mid+1,
ans=mid;
else
r=mid-1;
}
printf("%d\n",number(x1,y1,x2,y2,ans)-(value(x1,y1,x2,y2,ans)-h)/ans);
}
}
return;
}
int dfncnt;
//建树,注意:建树建的是空树
inline int build(int l,int r){
int rt=++dfncnt;
int mid=l+r>>1;
if (l<r){
t[rt].lson=build(l,mid);
t[rt].rson=build(mid+1,r);
t[rt].siz+=t[t[rt].lson].siz+t[t[rt].rson].siz;
}
return rt;
}
//更新,也就是insert
inline int update(int pre,int l,int r,int x){
int rt=++dfncnt;
t[rt].lson=t[pre].lson;
t[rt].rson=t[pre].rson;
t[rt].siz=t[pre].siz+1;
t[rt].sum=t[pre].sum+b[x];
if (l<r){
int mid=l+r>>1;
if (x<=mid)
t[rt].lson=update(t[pre].lson,l,mid,x);
else
t[rt].rson=update(t[pre].rson,mid+1,r,x);
}
return rt;
}
//查询,可以理解为二分思想
inline int query(int u,int v,int l,int r,int h){
int ans=0;
while(l<r){
int mid=l+r>>1;
long long lcd=t[t[v].rson].sum-t[t[u].rson].sum;
//判断右子树够不够大
if (lcd<h) ans+=t[t[v].rson].siz-t[t[u].rson].siz,h-=lcd,r=mid,v=t[v].lson,u=t[u].lson;
//不够则加上右子树大小,向左走
else l=mid+1,v=t[v].rson,u=t[u].rson;
}
ans+=(h+b[l]-1)/b[l];
//用左子树来补
//注意!!!我用了离散化,所以这里我用的是b[l],如果不离散化,直接用l就可以了
return ans;
}
//历史版本的根的编号
int his[10000005];
void main2(){
for (int i=1;i<=c;i++)
scanf ("%d",a2+i),b[i]=a2[i];
sort(b+1,b+c+1);
int n1=unique(b+1,b+c+1)-b-1;
his[0]=build(1,n1);
for (int i=1;i<=c;i++){
a2[i]=lower_bound(b+1, b+1+n1, a2[i]) - b;
his[i]=update(his[i-1],1,n1,a2[i]);
}
while(m--){
int x1,y1,x2,y2,h;
scanf ("%d%d%d%d%d",&x1,&y1,&x2,&y2,&h);
if (t[his[y2]].sum-t[his[y1-1]].sum<h){
printf("Poor QLW\n");
continue;
}
int ans=query(his[y1-1],his[y2],1,n1,h);
cout<<ans<<endl;
}
return;
}
int main(){
//freopen (".in","r",stdin);
//freopen (".out","w",stdout);
scanf ("%d%d%d",&r,&c,&m);
if(r!=1)
main1();
else
main2();
return 0;
}