HDU3473+CSU1080-划分树求静态区间前k大数的和-主席树求动态区间前k的和

(有任何问题欢迎留言或私聊


这两天看了一下划分树的博客,推荐一个**大神博客,一看就懂。**


下面讲一下这两题题划分树的应用。

首先是HDU3473:

 题目意思很裸,读完应该就知道xi应该就是中位数

 划分树建树复杂度时nlogn,m次查询,划分树每次查询是log复杂度。

 所以要处理出来的就是怎么O(1)求出表达式的值

 解决方法就是把绝对值去掉,xi左边的比xi小,xi右边的比xi大,所以:

化简公式

 sum = numLxi-sumL+0+sumR-numRxi

numL是在[LR]区间中小于xi的个数=mid-L
numR是在[LR]区间中大于xi的个数=R-mid
sumL是[LR]区间中小于xi的数的和
sumR是[LR]区间中大于xi的数的和

 numL和numR很好求出来,问题在于如果枚举sumL的和复杂度是O(n),肯定吃不消。
 为了把复杂度降低到O(log)可以用划分树求解。

 在普通划分树中再加一个sum数组,sum[i]记录的是本段从开头带i进入左子树的权值之和。
 具体看代码解释,十分详细。

AC代码:
#include<cstdio>  
#include<algorithm>
#include<cstring>
#include<cassert>  
using namespace std;
const int N = 100005;  
typedef long long LL;
/*
首先思考一下可只xi应该是中位数
我们要O1算出答案,算的时候xi在左边还是右边都行
把绝对值去掉就是: (numL*xi-sumL)+(sumR-numR*xi)
在划分树中加一个sum数组可以求出numL和sumL,再处理一个前缀和数组
sumL=sum;
sumR=pre[R]-pre[L-1]-sum;
numL=num;
numR=R-L+1-num;
mid=(R-L+2)>>1;
tmp=xi=query(mid);
sum=pre[y]-pre[x-1]-sum*2-(y-x+1-num-num)*tmp;
*/
struct lp{
	int cnt[N];//本层中从本段左端点到i有多少个进入左子树
	int num[N];//本层中从本段的数字排列
	LL sum[N];本层中从1到i进入左子树的权值和
}cw[22];
LL pre[N],sum;
int n,m,num;
int sor[N];
int build(int l,int r,int d){
	if(l==r)return cw[d].num[l];
	cw[d].sum[0]=cw[d].cnt[0]=0;
	int same=0,mid=(l+r)>>1;
	for(int i=mid;i>=l;--i){//左边有多少的和mid值相等
		if(sor[i]==sor[mid])same++;
		else break;
	}
	int cnt=0;
	int li=l,ri=mid+1;//下一层分段的起点,反正左右两边肯定是均分的(可能差1)
	for(int i=l;i<=r;++i){
		cw[d].sum[i]=cw[d].sum[i-1];//前缀进入左子树权值和
		if(cw[d].num[i]<sor[mid]){//小于mid一律入左子树
			cw[d+1].num[li++]=cw[d].num[i];
			cw[d].sum[i]+=cw[d].num[i];
			cnt++;
		}else if(cw[d].num[i]==sor[mid]&&same){
			same--;
			cnt++;
			cw[d+1].num[li++]=cw[d].num[i];
			cw[d].sum[i]+=cw[d].num[i];
		}else{
			cw[d+1].num[ri++]=cw[d].num[i];
		}
		cw[d].cnt[i]=cnt;//统计本段到i进入左子树数量
	}
	build(l,mid,d+1);//递归
	build(mid+1,r,d+1);
}
int query(int l,int r,int L,int R,int k,int d){
	if(l==r)return cw[d].num[l];//如果只有一个数,直接返回
	int left=0,sum_in_left=0,mid=(l+r)>>1;
	if(L==l){
		sum_in_left=cw[d].cnt[R];
	}else{
		sum_in_left=cw[d].cnt[R]-cw[d].cnt[L-1];//这是LR区间中进入左子树的数量
		left=cw[d].cnt[L-1];//这是本段[l,L)区间中进入左子树的数量
	}
	int newl,newr;
	if(sum_in_left>=k){//第k大数在左子树
		newl=l+left;//L左边有left个进入左子树,过滤掉那些(不理解的话,取left为0理解
		newr=l+left+sum_in_left-1;//下一段此区间有效的右边界
		return query(l,mid,newl,newr,k,d+1);
	}else{//在右子树
		newl=mid+L-l+1-left;//过滤掉[l,L)中在右子树的数量
		newr=mid+R-l+1-left-sum_in_left;//右边界(我把它化简了,不过很好推得
		num+=sum_in_left;//把LR区间在左子树的数量累加
		sum+=cw[d].sum[R]-cw[d].sum[L-1];//把LR区间在左子树权值累加
		return query(mid+1,r,newl,newr,k-sum_in_left,d+1);
	}
}
int main(int argc, char const *argv[]){
	int tim;
	scanf("%d",&tim);
	for(int T=1;T<=tim;++T){
		scanf("%d",&n);
		pre[0]=0;
		for(int i=1;i<=n;++i){
			scanf("%d",&sor[i]);
			cw[0].num[i]=sor[i];
			pre[i]=pre[i-1]+sor[i];//区里出前缀和,方便O1求值
		}
		sort(sor+1,sor+1+n);//记得排序呀兄dei
		build(1,n,0);
		scanf("%d",&m);
		printf("Case #%d:\n",T);
		while(m--){
			int x,y,z;
			scanf("%d%d",&x,&y);
			x++;y++;//要自加一下,因为我的下标从1开始
			sum=0;num=0;//初始化
			z=(y-x+2)>>1;//取中间一位
			int mid=(x+y)>>1;
			LL tmp=query(1,n,x,y,z,0);
			assert(num==mid-x);
			num=mid-x;
			sum=pre[y]-pre[x-1]-sum*2-(y-x+1-num-num)*tmp;//推出来的公式
			printf("%lld\n",sum );
		}
		printf("\n");//PE一发你就知道咯
	}
	return 0;
}

第二题CSU1080:

 这题稍微难一点,但是把上面的代码改一下就可以ac了。本质是一样的,都是要log时间内求区间前k大的数和。

题意及处理:

 中文题面没啥好说的,题目说的是每个学生最多解决k个任务。

 毫无疑问,贡献为负数的任务不选

 骚操作来了,负数贡献赋值为0,这样在用划分树处理前k大的数之和就十分方便了。

 还要注意一点的是,题目给出的是难度值和贡献值对应一个下标,所以要离散化一下。离散化处理出每个学生能解决的难度对应的下标。

直接套用上面的代码,稍作修改就ac了。

AC代码:
//872ms
#include<cstdio>  
#include<algorithm>
#include<cstring>
#include<cassert>  
using namespace std;
const int N = 100005;  
typedef long long LL;
struct lh{
	int id,val;
}ar[N];
bool cmp1(lh &a,lh &b){
	if(a.id!=b.id)return a.id<b.id;
	return a.val<b.val;
}
bool cmp2(int i,int j){
	return i<j;
}
struct lp{
	int cnt[N];//本层中从本段左端点到i有多少个进入左子树
	int num[N];//本层中从本段的数字排列
	LL sum[N];本层中从1到i进入左子树的权值和
}cw[22];
int id[N],val[N];
int pre[N],sum;
int n,m,num;
int sor[N];
int build(int l,int r,int d){
	if(l==r)return cw[d].num[l];
	cw[d].sum[0]=cw[d].cnt[0]=0;
	int same=0,mid=(l+r)>>1;
	for(int i=mid;i>=l;--i){//左边有多少的和mid值相等
		if(sor[i]==sor[mid])same++;
		else break;
	}
	int cnt=0;
	int li=l,ri=mid+1;//下一层分段的起点,反正左右两边肯定是均分的(可能差1)
	for(int i=l;i<=r;++i){
		cw[d].sum[i]=cw[d].sum[i-1];//前缀进入左子树权值和
		if(cw[d].num[i]<sor[mid]){//小于mid一律入左子树
			cw[d+1].num[li++]=cw[d].num[i];
			cw[d].sum[i]+=cw[d].num[i];
			cnt++;
		}else if(cw[d].num[i]==sor[mid]&&same){
			same--;
			cnt++;
			cw[d+1].num[li++]=cw[d].num[i];
			cw[d].sum[i]+=cw[d].num[i];
		}else{
			cw[d+1].num[ri++]=cw[d].num[i];
		}
		cw[d].cnt[i]=cnt;//统计本段到i进入左子树数量
	}
	build(l,mid,d+1);//递归
	build(mid+1,r,d+1);
}
int query(int l,int r,int L,int R,int k,int d){
	if(l==r)return cw[d].num[l];//如果只有一个数,直接返回
	int left=0,sum_in_left=0,mid=(l+r)>>1;
	if(L==l){
		sum_in_left=cw[d].cnt[R];
	}else{
		sum_in_left=cw[d].cnt[R]-cw[d].cnt[L-1];//这是LR区间中进入左子树的数量
		left=cw[d].cnt[L-1];//这是本段[l,L)区间中进入左子树的数量
	}
	int newl,newr;
	//printf("sum_in_left=%d\n",sum_in_left );
	if(sum_in_left>=k){//第k大数在左子树
		newl=l+left;//L左边有left个进入左子树,过滤掉那些(不理解的话,取left为0理解
		newr=l+left+sum_in_left-1;//下一段此区间有效的右边界
		return query(l,mid,newl,newr,k,d+1);
	}else{//在右子树
		newl=mid+L-l+1-left;//过滤掉[l,L)中在右子树的数量
		newr=mid+R-l+1-left-sum_in_left;//右边界(我把它化简了,不过很好推得
		//num+=sum_in_left;//把LR区间在左子树的数量累加
		sum+=cw[d].sum[R]-cw[d].sum[L-1];//把LR区间在左子树权值累加
		//printf("**%d\n",sum);
		return query(mid+1,r,newl,newr,k-sum_in_left,d+1);
	}
}
int main(int argc, char const *argv[]){
	while(~scanf("%d",&n)){
		for(int i=1;i<=n;++i){
			scanf("%d%d",&ar[i].id,&ar[i].val);
			if(ar[i].val<0)ar[i].val=0;
		}
		sort(ar+1,ar+1+n,cmp1);
		pre[0]=0;
		for(int i=1;i<=n;++i){
			pre[i]=pre[i-1]+ar[i].val;//前缀和
			id[i]=ar[i].id;//把难度值提出来,方便后面查询时得到下标
			val[i]=ar[i].val;
			sor[i]=val[i];
			cw[0].num[i]=val[i];
		}
		sort(sor+1,sor+1+n);//记得排序呀兄dei
		build(1,n,0);
		scanf("%d",&m);
		while(m--){
			int x,y,z,s;
			scanf("%d%d%d",&x,&y,&z);
			x=lower_bound(id+1,id+1+n,x)-id;//得到下标
			y=upper_bound(id+1,id+1+n,y)-id-1;
			s=y-x+1-z;
			//printf("*%d %d %d\n",x,y,s);
			if(z>y-x+1){
				printf("%d\n",pre[y]-pre[x-1]);
			}else{
				sum=0;
				int tmp=query(1,n,x,y,s,0);
				if(s)sum+=tmp;
				tmp=pre[y]-pre[x-1]-sum;
				printf("%d\n",tmp);
			}
		}
		printf("\n");//PE一发你就知道咯
	}
	return 0;
}

主席树求动态区间前k大数的和

CCPC-Wannafly Winter Camp Day5 Div1 C题 Division 主席树

#include<bits/stdc++.h>
#define fi first
#define se second
#define iis std::ios::sync_with_stdio(false)
#define pb push_back
#define o2(x) (x)*(x)
using namespace std;
typedef long long LL;
typedef pair<int, LL> pii;

const int MXN = 1e6 + 6;
const int INF = 0x3f3f3f3f;
const int mod = 1e9 + 7;
int n, q;
int ar[MXN], num[MXN];
LL sum[MXN];
struct QUERY {
    int l, r, k;
    LL ans;
}cw[MXN];
struct lp {
    int l, r, cnt;
    LL sum;
}node[MXN*16];
int inde, Root[MXN];
void z_update(int old, int &cur, int val, LL L, LL R) {
    cur = ++ inde;
    node[cur] = node[old];
    if(L == R) {
        node[cur].sum += val - val/2;
        ++ node[cur].cnt;
        return;
    }
    LL mid = (L + R) / 2;
    if(val <= mid) z_update(node[old].l, node[cur].l, val, L, mid);
    else z_update(node[old].r, node[cur].r, val, mid+1, R);
    node[cur].sum = node[node[cur].l].sum + node[node[cur].r].sum;
    node[cur].cnt = node[node[cur].l].cnt + node[node[cur].r].cnt;
}
LL z_query(int k, int old, int cur, LL L, LL R) {
    if(L == R) {
        return (LL)k*(L-L/2);//权值为L产生的贡献是k*(L-L/2)
    }
    LL mid = (L + R) / 2;
    int tmp = node[node[cur].r].cnt - node[node[old].r].cnt;
    if(k <= tmp) {
        return z_query(k, node[old].r, node[cur].r, mid + 1, R);
    }else {
        return node[node[cur].r].sum - node[node[old].r].sum
        + z_query(k-tmp, node[old].l, node[cur].l, L, mid);
    }
}
/*LL query(int k, int old, int cur, int L, LL R) {
    printf("%d %lld %lld\n", k, node[cur].sum, node[old].sum);
    if(L == R) {
        printf("*%d %d\n", L,k*(L-L/2));
        return (LL)k*(L-L/2);
    }
    LL mid = (L + R) / 2;
    int tmp = node[node[cur].r].cnt - node[node[old].r].cnt;
    printf("%d %d %d %d %lld\n", k, tmp, node[node[old].r].cnt,node[node[cur].r].cnt,node[node[cur].r].sum - node[node[old].r].sum);
    if(k <= tmp) {
        return query(k, node[old].r, node[cur].r, mid + 1, R);
    }else {
        return node[node[cur].r].sum - node[node[old].r].sum
        + query(k-tmp, node[old].l, node[cur].l, L, mid);
    }
}*/
int main(){
    scanf("%d%d", &n, &q);
    for(int i = 1; i <= n; ++i) scanf("%d", ar+i), sum[i]=sum[i-1]+ar[i];
    for(int i = 1; i <= q; ++i) {
        scanf("%d%d%d", &cw[i].l, &cw[i].r, &cw[i].k);
        cw[i].ans = sum[cw[i].r] - sum[cw[i].l-1];
    }
    LL L, R;
    for(int T = 30; T >= 0; --T) {
        L = 1LL<<T, R = 2LL<<T, inde = 0;
        node[0].l = node[0].r = node[0].sum = node[0].cnt = 0;
        for(int i = 1; i <= n; ++i) if(ar[i]>>T&1) {
            z_update(Root[i-1], Root[i], ar[i], L, R);
            //if(T == 2) printf("[%d %d]\n", i, ar[i]);
            sum[i] = sum[i-1] + ar[i] - ar[i]/2; num[i] = num[i-1] + 1;
        }else Root[i] = Root[i-1], sum[i] = sum[i-1], num[i] = num[i-1];
        //printf("T = %d\n", T);
        for(int i = 1, tmp; i <= q; ++i) {
            if(cw[i].k == 0) continue;
            if(cw[i].k >= num[cw[i].r] - num[cw[i].l-1]) {
                cw[i].k -= num[cw[i].r] - num[cw[i].l-1];
                cw[i].ans -= sum[cw[i].r] - sum[cw[i].l-1];
            }else {
                //printf("*%lld %d ", cw[i].ans, cw[i].k);
                cw[i].ans -= z_query(cw[i].k, Root[cw[i].l-1], Root[cw[i].r], L, R);
                //printf("%lld %d\n", cw[i].ans, T);
                cw[i].k = 0;
            }
            //printf("%d %d %d\n", cw[i].l, cw[i].r, cw[i].k);
        }
        for(int i = 1; i <= n; ++i) if(ar[i]>>T&1) ar[i] >>= 1;
        //printf("***\n");
    }
    for(int i = 1; i <= q; ++i) printf("%lld\n", cw[i].ans);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值