数据结构专题——树状数组、线段树练习题

牛客竞赛_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ (nowcoder.com)

A.[NOIP2012]借教室

题目大意:

一共有n天,每天学校有ri个教室可供租借,给你一系列租借订单,问你所有订单是否都可满足,若不满足,求出从第几个计划开始不满足。

每个计划的格式是:从第L天到第R天,租借x个房间

题解:

二分+树状数组

树状数组用来维护每一天的空余教室数,即差分用法

每次二分一个计划编号mid,将mid以前的所有订单都维护进树状数组,然后检查每一天的空余教室数是否存在小于0的情况,如存在,说明到这个订单已不能满足,向前二分,否则向后二分。

如果一直找不到不合法情况,则说明所有订单可满足,否则二分出来的ans即为第一个不满足的订单

Code:
#include 
using namespace std;
typedef long long ll;
inline int R(){int a=0,b=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-')b=-1;c=getchar();}while(c>='0'&&c<='9'){a=a*10+c-'0';c=getchar();}return a*b;}
int n,m;
int a[1000010],c[1000010];
struct query{
	int d,l,r;
}q[1000010];
int lb(int x){
	return x&(-x);
}
void Add(int x,int d){
	for(int i=x;i<=n;i+=lb(i)){
		c[i]+=d;
	}
}
int Sum(int x){
	int ans=0;
	for(int i=x;i>0;i-=lb(i)){
		ans+=c[i];
	}
	return ans;
}
void init(){
	for(int i=1;i<=n;++i){
		Add(i,a[i]); // 初始化,将a[i]转移至树状数组c[i] 
		Add(i+1,-a[i]);
	}
}
int last;
bool check(int now){
	if(now>last){
		for(int i=last+1;i<=now;++i){
			Add(q[i].l,-q[i].d);
			Add(q[i].r+1,q[i].d);
		}
	}else if(last>now){
		for(int i=last;i>=now+1;--i){
			Add(q[i].l,q[i].d);
			Add(q[i].r+1,-q[i].d);
		}
	}
	last=now;
	for(int i=1;i<=n;++i){
		if(Sum(i)<0)return false;
	}
	return true;
}
int main(){
	n=R();m=R();
	for(int i=1;i<=n;++i){
		a[i]=R();
	}
	init();
	for(int i=1;i<=m;++i){
		q[i].d=R();q[i].l=R();q[i].r=R();
	}
	int L=0,R=m,mid,ans=-1;
	while(L<=R){
		mid=L+R>>1;
		if(check(mid)){
			L=mid+1;
		}else{ // 不合法 
			R=mid-1;
			ans=mid;
		}
	}
	if(ans==-1)printf("0");
	else printf("%d\n%d",-1,ans);
	return 0;
}

B.[SDOI2009]HH的项链

题目大意:

给一个序列,m次询问,每次询问一个区间内不同数的个数

题解:

离线+树状数组

将询问存下来,按照右端点从小到大排序

维护树状数组:

第一次碰到一个数,直接在这个位置加1

若非第一次,将这个数上一次出现的位置减1,在当前位置加1,

如此一来,对于询问[L,R],ans=sum®-sum(L-1)

数据结构专题——树状数组、线段树练习题_第1张图片

Code:
#include 
using namespace std;
typedef long long ll;
inline int R(){int a=0,b=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-')b=-1;c=getchar();}while(c>='0'&&c<='9'){a=a*10+c-'0';c=getchar();}return a*b;}
int n,m,a[50010],la[1000010],ans[200010];
struct query{
	int l,r,id;
}q[200010];
int c[50010];
int lb(int x){
	return x&(-x);
}
void Add(int x,int d){
	for(int i=x;i<=n;i+=lb(i)){
		c[i]+=d;
	}
}
int Sum(int x){
	int ans=0;
	for(int i=x;i>0;i-=lb(i)){
		ans+=c[i];
	}
	return ans;
}
void init(){
	for(int i=1;i<=n;++i){
		Add(i,a[i]); // 初始化,将a[i]转移至树状数组c[i] 
	}
}
bool cmp(query a,query b){
	return a.r<b.r;
}
int main(){
	n=R();
	for(int i=1;i<=n;++i){
		a[i]=R();
	}
	m=R();
	for(int i=1;i<=m;++i){
		q[i].l=R();q[i].r=R();
		q[i].id=i;
	}
	sort(q+1,q+1+m,cmp);
	int j=1;
	for(int i=1;i<=n;++i){
		if(la[a[i]])Add(la[a[i]],-1);
		Add(i,1);
		la[a[i]]=i;
		while(i==q[j].r){
			ans[q[j].id]=Sum(i)-Sum(q[j].l-1);
			++j;
		}
	}
	for(int i=1;i<=m;++i){
		printf("%d\n",ans[i]);
	}
	return 0;
}

C.[HEOI2012]采花

题目大意:

与上题类似,给定一个数组,m次询问,每次询问区间内【数字个数】大于1的【数字】的个数

题解:

离线+树状数组

将询问按左端点从小到大排序

一开始初始化一个链表,每个位置的next指向和当前位置数字相同的下一个位置

然后把每一种数字第二次出现的地方在树状数组里加1

要处理[L,R]的询问时,将L之前的数字全部处理掉:

从前往后遍历,每遇到一个数字,如果这个数字在后面还出现过,那么next一定指向它,且去掉当前数后,next指向的数将成为第一次出现,而不是第二次,所以next指向的位置减1(树状数组中减1)

若next还存在next,则将其所在位置加1,因为它成了新的第二次出现

Code:
#include 
using namespace std;
inline int R(){int a=0,b=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-')b=-1;c=getchar();}while(c>='0'&&c<='9'){a=a*10+c-'0';c=getchar();}return a*b;}
const int MAXN=2000010;
int N,C,M;
int c[MAXN];
int a[MAXN],head[MAXN],Next[MAXN],ans[MAXN];
struct node{
    int l,r,id;
}q[MAXN];
bool cmp(node a,node b){
    return a.l<b.l;
}
void add(int x,int d){
    for(int i=x;i<=N;i+=i&(-i)){
        c[i]+=d;
    }
}
int sum(int x){
    int ans=0;
    for(int i=x;i>=1;i-=i&(-i)){
        ans+=c[i];
    }
    return ans;
}
int main(){
    N=R();C=R();M=R();
    for(int i=1;i<=N;++i){
        a[i]=R();
    }
    for(int i=N;i>=1;--i){
        Next[i]=head[a[i]];
        head[a[i]]=i;
    }
    for(int i=1;i<=M;++i){
        q[i].l=R();q[i].r=R();
        q[i].id=i;
    }
    for(int i=1;i<=C;++i){
        int t=Next[head[i]];
        if(t){
            add(t,1);
        }
    }
    sort(q+1,q+1+M,cmp);
    int now=1;
    for(int i=1;i<=M;++i){
        int l=q[i].l;
        while(now<l){
            int t=Next[now];
            if(t){
                add(t,-1);
                if(Next[t]){
                    add(Next[t],1);
                }
            }
            now++;
        }
        ans[q[i].id]=sum(q[i].r);
    }
    for(int i=1;i<=M;++i){
        printf("%d\n",ans[i]);
    }
    return 0;
}

D.数据结构

题目大意:

线段树板子题

​ 1 l r 询问区间[l,r]内的元素和

​ 2 l r 询问区间[l,r]内的元素的平方

​ 3 l r x 将区间[l,r]内的每一个元素都乘上x

​ 4 l r x 将区间[l,r]内的每一个元素都加上x

题解:

熟悉一下线段树的结构,模块化实现,细心

Code:
#include 
using namespace std;
#define ls i<<1
#define rs i<<1|1
typedef long long ll;
inline ll R(){ll a=0,b=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-')b=-1;c=getchar();}while(c>='0'&&c<='9'){a=a*10+c-'0';c=getchar();}return a*b;}
ll n,m,a[10010];
struct Tree{
	ll l,r,s1,s2;
	ll sum,mul;
}o[10010*4];
ll ans;
void pushup(ll i){
	o[i].s1=o[ls].s1+o[rs].s1;
	o[i].s2=o[ls].s2+o[rs].s2;
}
void pushdown(ll i){
	ll m=o[i].mul,s=o[i].sum;
	if(m!=1){
		o[ls].mul*=m;
		o[rs].mul*=m;
		o[ls].s2*=m*m;
		o[rs].s2*=m*m;
		o[ls].s1*=m;
		o[rs].s1*=m;
		o[i].mul=1;
	}
	ll ln=o[ls].r-o[ls].l+1;
	ll rn=o[rs].r-o[rs].l+1;
	if(o[i].sum){
		o[ls].sum+=s;
		o[rs].sum+=s;
		o[ls].s2+=2*o[ls].s1*s+ln*s*s;
		o[rs].s2+=2*o[rs].s1*s+rn*s*s;
		o[ls].s1+=ln*s;
		o[rs].s1+=rn*s;
		o[i].sum=0;
	}
}
void build(ll i,ll L,ll R){ //初始化
	o[i].l=L;o[i].r=R;
	o[i].sum=0;
	o[i].mul=1;
	if(L==R){
		o[i].s1=a[L];
		o[i].s2=a[L]*a[L];
		return;
	}
	ll mid=L+R>>1;
	build(ls,L,mid);
	build(rs,mid+1,R);
	pushup(i);
}
ll Ask(ll i,ll L,ll R,ll op){ //询问区间
	if(R<o[i].l||L>o[i].r)return 0;
	if(L<=o[i].l&&o[i].r<=R){
		if(op==1){
			return o[i].s1;
		}else{
			return o[i].s2;
		}
	}
	ll ans=0;
	pushdown(i);
	ans+=Ask(i<<1,L,R,op);
	ans+=Ask(i<<1|1,L,R,op);
	return ans;
}
void mul(ll i,ll L,ll R,ll d){ //修改区间
	ll l=o[i].l,r=o[i].r;
	if(R<l||L>r)return;
	if(L<=l&&r<=R){
		o[i].s1*=d;
		o[i].s2*=d*d;
		o[i].mul*=d;
		o[i].sum*=d;
		return;
	}
	pushdown(i);
	mul(ls,L,R,d);
	mul(rs,L,R,d);
	pushup(i);
}
void add(ll i,ll L,ll R,ll d){ //修改区间
	ll l=o[i].l,r=o[i].r;
	if(R<l||L>r)return;
	if(L<=l&&r<=R){
		o[i].s2+=2*o[i].s1*d+(r-l+1)*d*d;
		o[i].s1+=(r-l+1)*d;
		o[i].sum+=d;
		return;
	}
	pushdown(i);
	add(ls,L,R,d);
	add(rs,L,R,d);
	pushup(i);
}
int main(){
	n=R();m=R();
	for(ll i=1;i<=n;++i){
		a[i]=R();
	}
	build(1,1,n);
	while(m--){
		ll op=R();
		ll l,r,x;
		switch(op){
			case 1:{
				l=R();r=R();
				printf("%lld\n",Ask(1,l,r,1));
				break;
			}
			case 2:{
				l=R();r=R();
				printf("%lld\n",Ask(1,l,r,2));
				break;
			}
			case 3:{
				l=R();r=R();x=R();
				mul(1,l,r,x);
				break;
			}
			case 4:{
				l=R();r=R();x=R();
				add(1,l,r,x);
				break;
			}
		}
	}
	return 0;
}

E.线段树

题目大意:

板子题加一丢丢的答案处理

1 l r v,区间a[l]到a[r]的所有数字全部加v。

2 l r v,区间a[l]到a[r]的所有数字全部乘v。

3 l r,求区间a[l]到a[r]之间两两之间数字的乘积和(例如:2,3,4,5两两之间乘积和为 2*3+2*4+2*5+3*4+3*5+4*5)

题解:

只需要维护区间和sum以及平方和pf,即可

最后的答案为 (sum*sum-pf)/2

需要用到逆元,由于模数是质数,所以直接用费马小定理

Code:
#include 
using namespace std;
typedef long long ll;
inline ll R(){ll a=0,b=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-')b=-1;c=getchar();}while(c>='0'&&c<='9'){a=a*10+c-'0';c=getchar();}return a*b;}
const ll MAXN=100010;
#define ls i<<1
#define rs i<<1|1
ll a[MAXN],n,m,p;
struct Tree{
	ll l,r,sum,pf;
	ll add,mul; //懒标记 
}o[MAXN<<2];
ll KSM(ll a,ll b){ 		//快速幂,计算a^b 
    ll ans=1;
    while(b){
        if(b&1)ans=(__int128)ans*a%p; 	//if(b%2==1)
        a=(__int128)a*a%p;
        b>>=1;			//此处等价于power=power/2
    }
    return ans;
}
void pushup(ll i){
	o[i].sum=(o[ls].sum+o[rs].sum)%p;
    o[i].pf=(o[ls].pf+o[rs].pf)%p;
} 
void deal_add(ll i,ll d){ //懒标记处理
    ll r=o[i].r,l=o[i].l;
    o[i].pf=(o[i].pf+2*o[i].sum%p*d%p+d*d%p*(r-l+1)%p)%p;
    o[i].sum=(o[i].sum+(r-l+1)*d%p)%p;
    o[i].add=(d+o[i].add)%p;
}
void deal_mul(ll i,ll d){ //懒标记处理
    ll r=o[i].r,l=o[i].l;
    o[i].pf=o[i].pf*d%p*d%p;
    o[i].sum=o[i].sum*d%p;
    o[i].mul=o[i].mul*d%p;
    o[i].add=o[i].add*d%p;
}
void pushdown(ll i){ //标记下传 
	ll s=o[i].add,m=o[i].mul;
    ll ln=o[ls].r-o[ls].l+1;
    ll rn=o[rs].r-o[rs].l+1;
    if(m!=1){
        deal_mul(ls,m);
        deal_mul(rs,m);
        o[i].mul=1;
    }
	if(s){
        deal_add(ls,s);
        deal_add(rs,s);
		o[i].add=0;
	}
}
void build(ll i,ll L,ll R){ //初始化
	o[i].l=L;o[i].r=R;
	o[i].add=0;
    o[i].mul=1;
	if(L==R){
		o[i].sum=a[L]%p; //叶子节点信息 
        o[i].pf=a[L]*a[L]%p;
		return;
	}
	ll mid=L+R>>1;
	build(ls,L,mid);
	build(rs,mid+1,R);
	pushup(i);
}
ll Ask(ll i,ll L,ll R,ll op){ //询问区间
    ll l=o[i].l,r=o[i].r;
	if(R<l||L>r)return 0;
	if(L<=l&&r<=R){
		if(op==1)return o[i].sum;
        else return o[i].pf;
	}
	ll ans=0;
	pushdown(i);
	ans+=Ask(ls,L,R,op);
	ans+=Ask(rs,L,R,op);
	return ans%p;
}
void Add(ll i,ll L,ll R,ll d){ //修改区间
	ll l=o[i].l,r=o[i].r;
	if(R<l||L>r)return;
	if(L<=l&&r<=R){
        deal_add(i,d);
		return;
	}
	pushdown(i);
	Add(ls,L,R,d);
	Add(rs,L,R,d);
	pushup(i);
}
void Mul(ll i,ll L,ll R,ll d){ //修改区间
	ll l=o[i].l,r=o[i].r;
	if(R<l||L>r)return;
	if(L<=l&&r<=R){
        deal_mul(i,d);
		return;
	}
	pushdown(i);
	Mul(ls,L,R,d);
	Mul(rs,L,R,d);
	pushup(i);
}
int main(){
	// freopen("R.txt","r",stdin);
	ll T=R();
    while(T--){
        n=R();m=R();p=R();
        for(ll i=1;i<=n;++i) a[i]=R();
        build(1,1,n);
        ll l,r,v;
        for(ll i=1;i<=m;++i){
            ll op=R();
            switch(op){
                case 1:{
                    l=R();r=R();v=R();
                    Add(1,l,r,v);
                    break;
                }
                case 2:{
                    l=R();r=R();v=R();
                    Mul(1,l,r,v);
                    break;
                }
                case 3:{
                    l=R();r=R();
                    ll sum=Ask(1,l,r,1);
                    ll mull=Ask(1,l,r,2);
                    printf("%lld\n",((sum*sum%p-mull+p)%p*KSM(2,p-2)%p+p)%p);
                    break;
                }
            }
        }
    }
	return 0;
}

H.仓鼠的鸡蛋

题目大意:

有n堆鸡蛋,n个桶,每个桶的容量是m,每个桶最多装k堆鸡蛋,求每一堆鸡蛋装进的桶

每堆鸡蛋将放入编号最小的可放入的桶,若放不了输出-1

题解:

这道题只是用到了线段树的结构和pushup,没有修改和查值操作

维护已装鸡蛋的最小值,若已装了k堆,则直接将桶的已装鸡蛋数赋为m表示不能再装

想象线段树的结构,若左边有桶能装下我,则进入左子树,否则进入右子树

Code:
 #include 
using namespace std;
typedef long long ll;
inline ll R(){ll a=0,b=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-')b=-1;c=getchar();}while(c>='0'&&c<='9'){a=a*10+c-'0';c=getchar();}return a*b;}
#define ls i<<1
#define rs i<<1|1
const ll MAXN=300010;
ll n,m,k;
struct Tree{
	ll l,r,mi,cnt;
}o[MAXN<<2];
inline ll Min(ll a,ll b){
    return (a<b?a:b);
}
void pushup(ll i){
	o[i].mi=Min(o[ls].mi,o[rs].mi);
}
void build(ll i,ll L,ll R){ //初始化
	o[i].l=L;o[i].r=R;
	if(L==R){
		o[i].mi=0; //叶子节点信息 
		o[i].cnt=0;
		return;
	}
	ll mid=L+R>>1;
	build(ls,L,mid);
	build(rs,mid+1,R);
	pushup(i);
}
ll findpos(ll i,ll d){ //询问区间
	if(o[i].l==o[i].r){
		if(o[i].mi+d<=m&&o[i].cnt<k){
			o[i].mi+=d;
			o[i].cnt++;
			if(o[i].cnt==k)o[i].mi=m;
			return o[i].l;
		}else{
			return -1;
		}
	}
	ll ans=-1;
	bool bj=0;
    if(o[ls].mi+d<=m){
		ans=findpos(ls,d);
	}else{
		ans=findpos(rs,d);
	}
	pushup(i);
	return ans;
}
int main(){
	// freopen("R.txt","r",stdin);
	ll T=R();
    while(T--){
        n=R();m=R();k=R();
        build(1,1,n);
        for(ll i=1;i<=n;++i){
            ll t=R();
            ll pos=findpos(1,t);
            printf("%lld\n",pos);
        }
    }
	return 0;
}

你可能感兴趣的:(线段树&树状数组,数据结构,算法,c++)