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;
}

posted @ 2021-02-19 08:56  fallingdust  阅读(107)  评论(0编辑  收藏  举报