zoukankan      html  css  js  c++  java
  • [ZJOI2013]K大数查询 浅谈整体二分

    题目大意

    题面链接:
    bzoj3110
    洛谷P3332

    重新讲一下含糊不清的题意:

    有n个可重集合,有m个操作,操作分为两种:

    • 1 l r c 给第l到第r个可重集合都加入一个数c。
    • 2 l r c 询问第l到第r个可重集合第c大的数是多少。

    (nle 50000,mle 50000,1le lle rle n),1中的(|c|le n),2中的(cle longspace long)

    sol

    暴力

    给每一个点开一个vector暴力插入,询问时将l到r的数暴力排一个序。时间复杂度:(Theta(n^3log n))

    基于暴力,我们可以用树套树来做。

    二分

    对与每个询问,我们可以在-n到n之间二分,二分出一个mid后,我们暴力扫一遍所有在它之前的插入,统计出l到r中有多少个数大于mid,如果大于mid的数大于c,说明我们二分小了,反之说明我们二分大了。

    时间复杂度:(Theta(n^2log n))

    整体二分

    整体二分顾名思义就是所有的询问一起二分。其实在单次二分时,我们得出了很多有用信息,对之后的询问依然有用,所以我们可把所有询问一起二分。

    二分思路就在上面。下面讲一些不一样的地方。

    我们先新开出一个区间和两个数组,第一个数组存要向小二分的操作,第二个数组存要向大二分的操作。

    在整体二分时,我们二分出了一个mid,依次扫过每一个操作:

    1. 插入,讨论它的c值,如果大于mid,就在新区间的l到r加一,把它扔到第二个数组,否则扔到第一个数组。
    2. 查询,令num等于新区间l到r的值之和,则num就等于l到r大于mid的数的个数。如果num小于c,则c-=num,把操作扔到第一个数组里,否则扔到第二个数组。

    最后,递归处理两个数组。

    证明一下这样做的正确性:

    查询时,如果c大于num,那么对num产生贡献的插入都会和它到不同的数组中,以后不会再产生贡献。但由于我们往小二分,那些插入实际上是会对它产生贡献的,所以我们现在就要把贡献记下来,实际操作就是减去num。如果c小于num,那么对num产生贡献的插入都会和它到相同的数组中,没有问题。

    插入操作其实是随着查询操作来的,查询没错它就没错。

    对于新区间的区间修改,区间查询,树状数组或线段树即可。

    时间复杂度:(Theta(nlog^2 n))

    还有一些细节和优化都在代码注释里。
    code:

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int maxn=50010;
    int n,m;
    ll ans[maxn];
    struct BIT {//树状数组
    	ll c[maxn];
    	inline void add(int x,int y) {
    		for(int i=x; i<=n; i+=i&-i)c[i]+=y;
    	}
    	inline ll sum(int x) {
    		ll ans=0;
    		for(int i=x; i; i-=i&-i)ans+=c[i];
    		return ans;
    	}
    } a,b,c;
    struct node {//操作的结构体
    	int tp,l,r,id;
    	ll c;
    } q[maxn],tem1[maxn],tem2[maxn];//tem为两新数组
    void solve(int x,int y,int l,int r) {//整体二分
    	if(l==r) {//找到答案
    		for(int i=x; i<=y; i++)
    			ans[q[i].id]=l;
    		return;
    	}
    	int mid=(l+r)>>1,cnt1=0,cnt2=0;
    	for(int i=x; i<=y; i++)
    		if(q[i].tp==2) {//查询
    			ll num=q[i].r*a.sum(q[i].r)-b.sum(q[i].r)-(q[i].l-1)*a.sum(q[i].l-1)+b.sum(q[i].l-1);
    			if(num<q[i].c) {
    				q[i].c-=num;
    				tem1[++cnt1]=q[i];
    			} else tem2[++cnt2]=q[i];
    		} else {//插入
    			if(q[i].c>mid) {
    				a.add(q[i].l,1),a.add(q[i].r+1,-1);
    				b.add(q[i].l,q[i].l-1),b.add(q[i].r+1,-q[i].r);
    				tem2[++cnt2]=q[i];
    			} else tem1[++cnt1]=q[i];
    		}
    	for(int i=x; i<=y; i++)//清空树状数组,不能用memset,否则时间会爆炸
    		if(q[i].tp==1&&q[i].c>mid) {
    			a.add(q[i].l,-1),a.add(q[i].r+1,1);//照样减回去
    			b.add(q[i].l,-q[i].l+1),b.add(q[i].r+1,q[i].r);
    		}
    	for(int i=1; i<=cnt1; i++)q[x+i-1]=tem1[i];//滚动一下优化空间
    	for(int i=1; i<=cnt2; i++)q[x+cnt1+i-1]=tem2[i];
    	if(cnt1)solve(x,x+cnt1-1,l,mid);//分别去二分
    	if(cnt2)solve(x+cnt1,y,mid+1,r);
    }
    int main() {
    	scanf("%d%d",&n,&m);
    	int Q=0;
    	for(int i=1; i<=m; i++) {
    		scanf("%d%d%d%lld",&q[i].tp,&q[i].l,&q[i].r,&q[i].c);
    		if(q[i].tp==2)q[i].id=++Q;
    	}
    	solve(1,m,-n,n);
    	for(int i=1; i<=Q; i++)
    		printf("%lld
    ",ans[i]);
    	return 0;
    }
    

    总结

    整体二分不能在线,时间复杂度其实和树套树一样。但它代码短,常数小,还是挺有用的一个算法。

  • 相关阅读:
    利用栈计算后缀式
    中缀式转换为后缀式(逆波兰式)方法
    JAVA字符串首字母大小写转换(截取转换+移动ASCII编码)
    VSCode格式化代码后,设置代码不自动换行
    oracle连接不上,提示监听服务不可用的解决办法
    壹周立波秀
    欣慰,举手之力,帮助一个老同学恢复了博士论文文档
    开发中的思维转换也是一种创新-思维创新
    电脑借液晶电视显示器出现雪花点的另类解决办法
    邮箱已满解决办法
  • 原文地址:https://www.cnblogs.com/hht2005/p/11421235.html
Copyright © 2011-2022 走看看