[学习笔记]一些数据结构题(阶段一)

是之前存在本地的笔记,想起来就传上来了

数据结构

队列/单调队列

1886滑动串口

题意:长度为\(n\)的序列,大小为\(k\)的窗口,从左向右滑动,每次滑动输出窗口内元素最大/最小值

题解

以最大值为例,单调数据结构的一般思路:考虑到如果有两个元素\(a_i,a_j\),不妨假设\(i<j\),如果\(a_i<a_j\),则\(a_i\)可以直接丢掉,于是我们用一个队列\(Q\),每次加入元素前分别从队头和队尾检查有没有元素比他小,有的话就直接弹出,队头元素就是答案。

rep(i,1,n){
    scanf("%d",&a);

    while(!q1.empty()&&q1.back().second<a)q1.pop_back();
    while(!q2.empty()&&q2.back().second>a)q2.pop_back();

    q1.push_back((pii){i,a});q2.push_back((pii){i,a});
    while(!q1.empty()&&(q1.front().first+k-1<i||q1.front().second<a))
        q1.pop_front();
    while(!q2.empty()&&(q2.front().first+k-1<i||q2.front().second>a))
        q2.pop_front();
    if(i>=k){
        r2[i-k+1]=q2.front().second;
        r1[i-k+1]=q1.front().second;
    }
}

分块

CF617E

https://www.luogu.com.cn/problem/CF617E

题意:每次询问一个区间\([l,r]\),问有多少个子区间的异或和为\(k\)\(k\)开始给定

题解:

子区间、异或和、统计个数、可以离线

惯性思维,预处理前缀异或和,一个区间\(l,r\)的异或和为\(k\)则有\(s_r \oplus s_{l-1}=k\),一种转化是\(s_{r}=s_{l-1}\oplus k\),这个不好统计,不过还是可以写成\(s_{l-1}=s_{r} \oplus k\),变成统计有多少个\(s_{l-1}\)的值是\(s_{r}\oplus k\)

子区间,统计有多少值为\(s_{r}\oplus k\),听着就很莫队x

(注意这边是\(s_{l-1}\),每次询问相当于问\([l-1,r]\)内有多少个\(s_{l-1}=s_x\oplus k\),所以在处理询问的时候不妨直接把询问区间改成\([l-1,r]\)

拿个东西存\(s_{i}\),对当前区间\([l,r]\),往大的扩展就对新增的\(s_{x}\)统计\(tot[s_x\oplus k]\)的个数,往小的扣掉就扣去\(tot[s_x \oplus k]\)这个贡献

大爷的字符串题

题意:对不起没看懂…上网查了一下题意,大概就是给你一个序列,每次询问区间众数的出现次数

题解

维护每个数的出现次数\(cnt\)和一个\(cnt\)的出现次数\(s\)

inline void add(int x){
	s[cnt[a[x]]]--;
	cnt[a[x]]++;
	s[cnt[a[x]]]++;
	res=max(res,cnt[a[x]]);
}
inline void del(int x){
	s[cnt[a[x]]]--;
	if(res==cnt[a[x]]&&s[cnt[a[x]]]==0)res--;
	cnt[a[x]]--;
	s[cnt[a[x]]]++;
}

线段树/树状数组

AHOI2009 维护序列(区间加/乘)

题意:长度为\(n\)的序列,区间乘\(c\),区间加\(c\),询问区间和。(\(n\leq 10^5\))

题解

  • 每次操作只有加法或者乘法,不妨把在处理区间和的时候全部当成先乘再加

  • 如果每次做完都下放标记就很简单,要是父亲节点积累了好几个标记怎么办?

  • 维护父亲节点懒标记的时候稍微改一下策略,我们考虑原来的\(w\)经过若干次操作变成\(((w+a)*b)+c)\),它可以化为\((wb+ab)+c=wb+ab+c\),容易发现对于乘法标记的更新直接乘上去就行了,而加法标记稍微改一改,记原本父亲节点有一个加法标记\(pt[node]\),现在传下来一个乘法\(m\)和加法\(p\),那样就用\(pt[node]*m+p\)来更新原来的标记

inline void eveal(int node,int p,int m,int l,int r){
	tr[node]=((ll)tr[node]*m+(ll)(r-l+1)*p)%MOD;
	mt[node]=((ll)mt[node]*m)%MOD;
	pt[node]=((ll)pt[node]*m+p)%MOD;
}
inline void push_down(int node,int l,int r){
	int mid=(l+r)>>1;
	eveal(lson,pt[node],mt[node],l,mid);
	eveal(rson,pt[node],mt[node],mid+1,r);
	pt[node]=0;mt[node]=1;
}
inline void modify(int node,int l,int r,int ql,int qr,int p,int m){
	if(ql<=l&&r<=qr){
		eveal(node,p,m,l,r);
		return ;
	}
	push_down(node,l,r);int mid=(l+r)>>1;
	if(mid>=ql)modify(lson,l,mid,ql,qr,p,m);
	if(mid+1<=qr)modify(rson,mid+1,r,ql,qr,p,m);
	push_up(node);
}

TJOI2018数学计算

题意:开始有一个数\(x=1\)\(Q\)次操作:

  • \(x=x\times m\)
  • \(x=x/m_i\),其中\(m_i\)为第\(i\)次操作乘上的\(m\),保证每个数最多被除一次

每次操作结束输出\(x \% MOD\)的结果,\(Q\leq 10^5\)

题解

想了好久…我好傻逼…看了题解“单点修改”才反应过来…

一开始想着去维护每次操作结束之后的结果…不太行

注意到每个数字最多被除一次,我们把操作次数作为下标,储存每次操作的数字,如果是除法就做单点修改,询问区间乘积

做完了…

Luogu1637三元上升子序列

题意:\(n\)个整数序列,值域\(2^{63}\),求满足\(i<j<k \and a_i<a_j<a_k\)\((i,j,k)\)对数

题解:

老经典题了,回忆求逆序对数的时候我们以值域为下标建树状数组查询在\(i\)之后且比\(a_i\)小的数的个数

这里我们不难想到,对于一个位置\(j\),算出前面比他小的数的个数\(pre[j]\)和在\(j\)之后比\(a_j\)大的数的个数\(nxt[j]\),那样以\(j\)为中间元素的三元上升子序列的个数是\(pre[j]\times nxt[j]\),而整个\(pre[]\)\(nxt[]\)数组可以用类似于逆序对数的方法来做。

值域的话离散化一下…话说之前一直只会写最一般的离散化(就算只能对整数离散化,还不好去重…)

这里就顺便学一下最一般的离散化:

rep(i,1,n)scanf("%lld",&a[i]),f[i]=a[i];
sort(f+1,f+n+1);
cnt=unique(f+1,f+n+1)-f-1;
rep(i,1,n)a[i]=lower_bound(f+1,f+cnt+1,a[i])-f;

离散化完了主代码也很好写:

rep(i,1,n){
    modify(a[i],1);
    pre[i]=query(a[i]-1);
}
rep(i,1,n)tr[i]=0;
per(i,n,1){
    modify(a[i],1);
    nxt[i]=query(n)-query(a[i]);
}
rep(i,1,n)ans+=pre[i]*nxt[i];
printf("%lld\n",ans);

修改和查询随便拿个树状数组或者线段树维护一下

COCI2010-2011#6 STEP-最长交替序列

题意:一个长度为\(n(n\leq 2\times 10^5)\)只含的\(L\)\(R\)的序列,初始全为L。单点修改成另一个值。对于一个字符串\(s\),如果不存在连续的\(L,R\)我们说它的符合要求的,每次修改完查询最长的符合要求的子串长度。

题解

(应该能改成区间修改+区间查询的)

这里同样有那么几个关键字,最长,子串(连续)。和其他数据结构问题一样,试试看能不能把问题拆成几个容易维护的东西。

对于一个区间\([l,r]\)来说,答案要么来自于\([l,mid]\),或\([mid+1,r]\),要么来自于左区间的右边并上右区间的左边。全部来自左区间或者右区间的答案可以直接取\(max\),问题在怎么维护两边拼起来的情形。

继续拆解问题,要维护两边拼起来的情形我们需要哪些信息?

  • 首先左区间的右端点和右区间的左端点不同,所以我们需要记录左右端点
  • 拼起来的答案是以【左区间的右端点为右端点的最长符合要求子串长度】+【右区间的左端点为左端点的最长符合要求子串长度】,嗯…那就再维护一下每个区间\([l,r]\)以左右端点为端点的答案

于是结点信息就可以这么维护(对应push_up函数):

tr[node]=max(tr[lson],tr[rson]);
lp[node]=lp[lson];rp[node]=rp[rson];
ls[node]=ls[lson];rs[node]=rs[rson];

if(rp[lson]!=lp[rson]){
    tr[node]=max(tr[node],rs[lson]+ls[rson]);
    int mid=(l+r)>>1;
    if(ls[node]==mid-l+1)ls[node]=ls[node]+ls[rson];
    if(rs[node]==r-mid)rs[node]=rs[node]+rs[lson];
}

\(tr[]\)记录答案,\(lp,rp\)记录左右端点的值,\(ls,rs\)记录左右端点为端点的答案)

6327区间加区间sin和

题意:和标题一样233,整数序列,区间加,区间sin值和,\(n\leq 2\times 10^5\)

题解

\(\sum sin(a_i+v)=\sum sin(a_i)cos(v)+cos(a_i)sin(v)=cos(v)\sum sin(a_i)+sin(v)\sum cos(a_i)\)

随便拿一个数据结构维护区间\(sin\)和跟区间\(cos\)和即可,注意开longlong

2894HotelG——长度不小于x的最左连续全0子串

题意:\(n(n\leq 5\times 10^4)\)个房间的旅店,开始全空,两个操作:

  1. 输入一个\(x\) ,找到左端点\(l\)最小的区间\([l,l+x-1]\),这些房间都是空的,输出\(l\),同时这些房间全部住进人,如果不存在这样的\(l\),输出0
  2. 输入\(x,y\),代表房间号为\([x,x+y-1]\)的房间全部退房

题解:

先把背后的模型分离出来,维护一个初始全为0的01序列,操作1等价为找到一个长度不小于\(x\)全0子串,操作2则是区间赋值。

  • 和前面要找最长LR交替的子串类似,这种涉及到“子串”的区间问题,我们不妨大胆引入一些方便我们维护的新信息
  • 以及这里要找长度\(\geq x\)的子串,这种信息我们不太好对每一个\(x\)维护,但是我们可以维护一个区间最长的符合条件的子串。即:找子串长度\(\geq x\)\(\to\) 尝试维护最长符合要求子串

要维护最长的符合要求的子串就很好做了,左右区间和中间拼起来三种情况,额外维护左右端点信息即可。

查询?

  • 能往左找就尽量往左找,不行的话中间找…
  • 再不行再找右区间
inline int query(int node,int l,int r,int len){
	if(l==r)return l;
	push_down(node,l,r);int mid=(l+r)>>1;
    
	if(tr[lson]>=len)return query(lson,l,mid,len);
	if(rs[lson]+ls[rson]>=len)return mid-rs[lson]+1;
	return query(rson,mid+1,r,len);
}

*一开始傻逼懒标记tag[]写挂了,因为是区间赋值0/1,所以对于清空的标记应该写成-1之类的取不到的数字。

2184贪婪大陆——区间计数

题意:\(n(n\leq 10^5)\)个位置,两个操作:1、选一个区间\([L,R]\)埋上类型和之前不同的地雷。2、查询\([L,R]\)内有几种不同的地雷

题解

第一反应以为是前几天的区间覆盖+统计有几种数,仔细一想发现不对,这里每个位置可以埋多个地雷。

  • 题目特殊条件往往很有用

每次埋的地雷和之前不同,原问题的查询\([L,R]\)变成查询这个区间内之前被多少个区间覆盖到

  • 回忆曾经一个多次修改,最后单点查询的题目
    这个题可以直接预处理出s[l]++,s[r+1]--,最后从左到右求和,\(s[i]\)对应的值就是这个点被覆盖了几次

好了现在要边修改边查询,仿照之前的思路,对于一个区间\([l,r]\)和当前要查询的大区间\([L,R]\),画画图会发现,只要\(l\leq R\)并且\(r\geq L\),那么\([l,r]\)就一定有1的贡献,而

接着想,我们就需要知道有多少个区间\([l,r]\)的右端点\(\geq L\),有多少个区间的左端点\(\leq R\),这个信息可以开两个树状数组维护前缀和。答案则是【左端点\(\leq R\)的个数】减去【右端点\(<L\)的个数】

int q,l,r;scanf("%d%d%d",&q,&l,&r);
if(q==1)
    modify(l,1,0);modify(r,1,1);
else
    printf("%d\n",query(r,0)-query(l-1,1));

1438无聊的数列

题意:一个序列,\(n\leq 10^5\),单点查询,区间等差数列修改:\(a_l\to a_l+k,a_{l+1}\to a_{l+1}+k+d,...,a_{r}\to a_r +k+(r-l)d\)

题解:

维护差分序列\(d_{i}=a_i-a_{i-1}\),修改操作变为\(d_l \to d_l +k,d_{l+1}...d_r\)区间加,\(d_{r+1}\)单点减,单点查询\(\to\)前缀和

注意点

这题卡了好久,主要原因在这:

modify(1,1,n,l,l,k);
if(l!=r)modify(1,1,n,l+1,r,d);
if(r!=n)modify(1,1,n,r+1,r+1,-k-(r-l)*d);

如果没判不合法的区间,在修改的时候照样能进行修改,但这样节点对应的区间会出错,导致答案错误。

4145上帝造题的七分钟2/花神游历各国——区间开方

题意:\(n\leq 10^5\)的序列,区间开根号下取整,区间和,值域\([1,10^{12}]\)

题解

吉老师《小清新线段树》里提到的问题,注意到\(n\to n^{\frac{1}{2}}\to n^{\frac{1}{4}}\)\(n>1\)\(n\)最多被开\(log n\)次就会变成1。

维护一个区间最大值,最大值\(\leq 1\)就直接跳过,否则暴力修改每个节点。

值得反思…

似乎一开始因为线段树的基础知识没太学清楚一下子不知道怎么写暴力修改…这似乎是线段树最原始的形态呀(没有懒标记的区间修改)

一直递归到l==r单点修改

inline void modify(int node,int l,int r,int ql,int qr){
	if(l>r)return;
	if(maxn[node]<=1)return;
	if(l==r){
		ll e=tr[node];e=floor(sqrt(e));
		tr[node]=maxn[node]=e;
		return;
	}
	int mid=(l+r)>>1;
	if(mid>=ql)modify(lson,l,mid,ql,qr);
	if(mid+1<=qr)modify(rson,mid+1,r,ql,qr);
	push_up(node);
}

5142区间方差

题意:\(n\leq 10^5\)的序列,单点修改,区间方差取模

题解

\(a=\frac{1}{r-l+1}\sum_{i=l}^r a_i\)

\(D=\frac{1}{r-l+1}\sum_{i=l}^r (a_i-a)^2\)

平均数用区间和维护,方差里面拆成$\sum a_i^2 +(r-l+1)a^2 -2a\sum a_i $ ,维护一个平方和。

注意点

一开始漏了\(a^2\)前面的系数…

以及涉及取模减法的问题记得res=(res+MOD)%MOD 这一步

和前面的题比起来这题挺简单的…

SCOI2010序列操作——多种标记

题意:01序列,长度\(10^5\),支持五种操作:区间赋值为0/1,区间取反,区间1的个数,区间连续1的个数。

题解

2894的加强版?连续1的个数传统艺能了,维护左右端点值和以左右端点为起点的最长连续1以及整个区间的最长连续1。注意询问需要询问一个区间\([l,r]\)的最长连续1,那和前面一样我们需要左右区间的答案和区间中点往两边拓展的答案,所以还要额外加上一个查询\([l,r]\)以区间端点为端点的最长连续1的长度。

而区间1的个数则直接变成区间和,区间覆盖和区间翻转对于区间和都很好维护。

大致问题差不多都解决了,但是写完交一发发现全WA掉…

标记合并

问题和前面AHOI2009那题类似,一个结点被重复打上懒标记可不能直接覆盖(试过套上别的数据结构来储存…MLE…),需要稍微想一下怎么合并两个标记。

需要注意的是,结点\(node\)的懒标记\(tag[node]\)是用来传给它孩子结点的,结点本身已经被更新过

如果我们分别用0,1表示区间覆盖0/1,用2表示区间翻转。

现在要对结点进行更新,如果当前的是区间覆盖就完全不用管之前的标记是什么东西,直接改掉就行。而如果当前要下放的是区间翻转,我们就需要讨论一下了:

  • 如果之前是区间覆盖,那就把覆盖标记反过来
  • 如果之前也是区间翻转,还是那句话,结点本身已经翻转过了,所以它其实还要再翻转一遍(orz我在这里卡了好久),而它的标记是用来传给孩子的,翻转两次相当于没翻转,标记直接删除。
inline void update(int node,int l,int r,int v){
	if(v==2&&tag[node]!=-1){
		if(tag[node]==1||tag[node]==0)v=tag[node]^1;
		else if(tag[node]==2)
		{
			tag[node]=-1;
			rev(node,l,r);
			return;
		}
	}
	if(v==2)
		rev(node,l,r);
	else{
		sum[node]=(r-l+1)*v;
		lp[node]=rp[node]=v;
		
		tr[node][v]=ls[node][v]=rs[node][v]=r-l+1;
		int t=v^1;
		tr[node][t]=ls[node][t]=rs[node][t]=0;
	}
	tag[node]=v;
}

BJOI2016回转寿司——子区间计数

题意:\(n\leq 10^5,L,R\),序列\(a_1,..,a_n(|a_i|\leq 10^5)\),询问有多少子区间的区间和介于\([L,R]\)

题解:

子区间和→前缀和→化简式子

\(L\leq s_{r}-s_{l-1}\leq R\),则\(L+s_{l-1}\leq s_r \leq R+s_{l-1}\)

至多\(n\)个不同的\(s_r\),另外记录\(s_{l-1}+R\)\(s_{l-1}+L\),对这\(3n\)个数据进行离散化,再从\(n\)往1枚举所有的\(l\),查询\([L+s_{l-1},R+s_{l-1}]\)内有多少个数,答案对应的就是以\(l\)为左端点的答案,查询完之后再插入\(s_r\)对应的值。

rep(i,1,n)scanf("%lld",&a[i]),s[i]=s[i-1]+a[i];
rep(i,1,n){
    s[i+n]=s[i]+l;
    s[i+2*n]=s[i]+r;
}
rep(i,1,n*3)f[i]=s[i];
sort(f+1,f+n*3+1);
rep(i,1,n*3)
    b[i]=lower_bound(f+1,f+n*3+1,s[i])-f;
//上面是读入和离散化

for(int i=n;i>=1;i--){
    res+=(ll)(query(b[i+2*n])-query(b[i+n]-1));	
    modify(b[i],1);
}
//统计答案
rep(i,1,n)if(l<=s[i]&&s[i]<=r)res++;
//以1为左端点b[i]为s_r的答案

动态开点

开一个\(s_r\)的权值线段树,查询\([L+s_{l-1},R+s_{l-1}]\)的和

#define lson (ls[node])
#define rson (rs[node])
inline void push_up(int node){
	tr[node]=tr[lson]+tr[rson];
}

inline void update(int &node,ll l,ll r,ll x,int v){
	if(!node)node=++cnt;
	if(l==r){
		tr[node]+=v;
		return;
	}
	ll mid=(l+r)>>1;
	if(x<=mid)update(lson,l,mid,x,v);
	else update(rson,mid+1,r,x,v);
	push_up(node);
}

inline int query(int node,ll l,ll r,ll ql,ll qr){
	if(!node)return 0;
	if(ql<=l&&r<=qr)return tr[node];
	ll mid=(l+r)>>1;
	int res=0;
	if(mid>=ql)res=query(lson,l,mid,ql,qr);
	if(mid+1<=qr)res+=query(rson,mid+1,r,ql,qr);
	return res;
}

CF915E——线段树动态开点

题意:一共\(n(n\leq 10^9)\)天,初始全为工作日,\(q(q\leq 3\times 10^5)\)次操作:区间全改为工作日/非工作日,每次操作后询问还有多少个工作日

题解

离散化似乎也能做,但是…既然学了动态开点

inline void push_down(int node,int l,int r){
	if(tag[node]==-1)return;
	int mid=(l+r)>>1;
	if(!lson)lson=++cnt;
	if(!rson)rson=++cnt;
	tag[lson]=tag[rson]=tag[node];
	tr[lson]=(mid-l+1)*tag[node];
	tr[rson]=(r-mid)*tag[node];
	tag[node]=-1;
}

inline void update(int node,int l,int r,int v){
	tr[node]=v*(r-l+1);
	tag[node]=v;
}

inline void modify(int &node,int l,int r,int ql,int qr,int v){
	if(!node){
		node=++cnt;
		tag[node]=-1;
	}
	if(ql<=l&&r<=qr){
		update(node,l,r,v);
		return;
	}
	int mid=(l+r)>>1;
	push_down(node,l,r);
	if(mid>=ql)modify(lson,l,mid,ql,qr,v);
	if(mid+1<=qr)modify(rson,mid+1,r,ql,qr,v);
	push_up(node);
}

注意一下数组的大小,树高确实是\(LOG=log_{2} (10^9)\approx 29.897\),但是如果点数取\(3\times 10^5 \times LOG\)的话发现会挂掉…(我第一次取\(LOG=35\)挂了)

仔细想一下好像会挂掉应该是理所当然的,只有单点修改才是每次保证至多增加\(LOG\)个新点,这里已经是区间修改了…

P4556 [Vani有约会]雨天的尾巴 ——树上差分+线段树合并

题意:\(n\)个点的树,\(m\)次操作,每次给\(u,v\)路径上的所有点一个类型为\(z\)的救济粮,在所有操作结束之后询问每个结点类型最多的救济粮是哪种,多个相同的输出编号最小的一个。

\(n,m\leq 10^5,z\leq 10^5\)(话说\(z\)开到很大应该也是没问题))

题解

初步的思路应该是注意到只有多次操作和一次询问。容易忘差分上去靠:对于一条\(u\to v\)的操作,\(u+1,v+1,lca(u,v)-1,fa(lca(u,v))-1\),现在我们想怎么去维护这个信息。

还是从暴力开始想,如果对每种救济粮都开一个数组暴力数组,最后树上跑差分,不管时间还是空间都会爆炸,但是其实一次操作只会修改至多4个结点,也就是说一共\(n\)个结点,\(4m\)次操作,均摊下来每个结点只会被操作4次,这么看来每个结点的那个“数组”其实是非常稀疏的,不如直接写个动态开点的权值线段树…嗯!

考虑对每个结点用一个权值线段树维护一个出现最多的那个\(z\)是谁(当然,要维护这个信息就需要顺便维护出现的次数\(s[i]\)),合并的时候从叶子结点往上合并答案,一个结点的最终答案就是它和它所有子树对应线段树合并之后线段树树根的答案。现在的问题就是如何合并两个线段树!

注意到这其实是个动态开点的线段树,对于某个区间\([l,r]\)如果树\(u\)\(v\)都有结点,那就递归往下合并左右子树,如果\(u\)有结点而\(v\)没有结点,那合并之后的线段树其实就应该是\(u\)对应的结点。

inline int merge(int u,int v,int l,int r){
	if(u==0||v==0)return u|v;
	if(l==r){
        //合并结点
		s[u]+=s[v];
		tr[u]=l;
		return u;
	}
	int mid=(l+r)>>1;
	ls[u]=merge(ls[u],ls[v],l,mid);
	rs[u]=merge(rs[u],rs[v],mid+1,r);
	push_up(u); 
	return u;
}

以及最后从下往上合并答案:

inline void dfs(int x){
	for(register int i=head[x];i;i=edges[i].nxt)
		if(cur!=fa[x]){
			dfs(cur); 
			rt[x]=merge(rt[x],rt[cur],1,S);
		}
	ans[x]=tr[rt[x]];
}

复杂度

留坑

4114Qtree1-树剖/边化点

题意:\(n\)个结点的树,每条边有边权,两个操作:1、把第\(i\)条边的边权变成\(t\)。2、查询\(a\)\(b\)路径上最大边权,\(a=b\)时答案为0

题解:

如果操作和询问都是针对节点的话直接套上树链剖分,这里变成\(n-1\)条边的操作,容易想到对点和边建立对应关系来维护:随便选一个结点为树根\(rot\)跑一次dfs,从树根往下走的边\(u\to v\)的边权搞成\(v\)的点权,边的信息转为维护点的信息。

注意

  • 查询链上信息的时候,对于\(u,v\)两个点,他们的\(LCA(u,v)\)的结点信息并不在\(u\to v\)的路径上,所以在做树剖的时候:
if(dep[u]<dep[v])swap(u,v);
res=max(res,query(1,1,n,dfsl[v]+1,dfsl[u]));
  • 边信息转化为点信息:
inline void pre_dfs(int x,int f){
	for(register int i=head[x];i;i=edges[i].nxt)
		if(cur!=f){
			a[cur]=W[edges[i].idx];
			pos[edges[i].idx]=cur; 
			pre_dfs(cur,x);
		}
}

3224永无乡——并查集+线段树合并

题意:\(n\)座岛屿,编号从1到\(n\),第\(i\)座岛​有一个\(p_i\)表示\(i\)是第几重要的(所有的\(p_i\)构成一个1到n的全排列),一些岛屿一开始就互相可达,两种操作:1、在岛屿\(x,y\)之间建立一座桥。2、询问和岛屿\(x\)连通的所有岛屿中,第\(k\)重要的是哪个(\(p_i\)越小越重要),\(n\leq 10^5\)

题解

互相可达→连通性→并查集

\(k\)重要→第\(k\)小→权值线段树

这里变成带修改第\(k\)小问题,但是注意到两个岛屿只会建桥不会拆桥,也就是说只有合并的操作,直接上线段树合并√

2019ICPC上海F——树链立方和

树剖裸题,题意:一棵树,点权,链赋值,链加,链乘,询问链上结点权值的立方和

题解

其实没啥好说的,裸的树剖题,大概有几个需要注意的地方

一个是这题取模是取\(10^ 9+7\),两个\(10^9\)范围乘起来一不注意就可能爆long long,所以在更新结点信息的时候需要很多取模:

tr[node][3]=((tr[node][3]*m3)%MOD+(((3ll*tr[node][2])%MOD)*((m2*p)%MOD))%MOD)%MOD;
tr[node][3]=(tr[node][3]+(((3ll*tr[node][1])%MOD)*((m*p2)%MOD))%MOD)%MOD;
tr[node][3]=(tr[node][3]+(1ll*(r-l+1)*p3)%MOD)%MOD;

tr[node][2]=((tr[node][2]*m2)%MOD+(((2ll*tr[node][1])%MOD)*((m*p)%MOD))%MOD)%MOD;
tr[node][2]=(tr[node][2]+(1ll*(r-l+1)*p2)%MOD)%MOD;

tr[node][1]=((tr[node][1]*m)%MOD+(1ll*(r-l+1)*p))%MOD;

写的时候对每个式子都稍微分析一下最大可能的范围

以及另一个就是关于线段树的理解啦:

inline void push_up(int node){
	//用孩子更新父亲
}
inline void update(int node,int l,int r,ll t,ll p,ll m){
	if(t!=-1){
        //更新信息
		//如果有赋值的话,那之前的标记要清空掉
        tt[node]=t;pt[node]=0;mt[node]=1;
	}
	pt[node]=(1ll*pt[node]*m+p)%MOD;
	mt[node]=(1ll*mt[node]*m)%MOD;
    //依然是加法乘法混合运算
	//用m和t更新结点信息
}
inline void push_down(int node,int l,int r){
	int mid=(l+r)>>1;
	update(lson,l,mid,tt[node],pt[node],mt[node]);
	update(rson,mid+1,r,tt[node],pt[node],mt[node]);
    //同样用update来下放标记
	tt[node]=-1;pt[node]=0;mt[node]=1;
}
inline void modify(int node,int l,int r,int ql,int qr,ll t,ll p,ll m){
	if(ql<=l&&r<=qr){
		update(node,l,r,t,p,m);//封装好update函数来更新一个结点
		return ;
	}
    ...
}

SDOI2009-HH的项链

题意:长度为\(n\)的序列,多次询问\([l,r]\)内有多少种不同的数\(n,m\leq 10^6\)

题解:

如果是\(10^5\)就可以上根号的莫队了…或者数据弱一点应该也卡的过去…不过我做这题的时候被加强过233

考虑对每个位置\(i\),记录\(last[i]\)表示上一个\(a[i]\)出现的位置,那样对于一个询问,我们只需要知道\([l,r]\)内有多少个\(i\),满足\(last[i]<l\)即可。

如果对每个区间都\(O(n)\)做一遍显然不行,不过我们仿照莫队的思路,如果对每个询问的右端点进行一个递增的排序,那样处理询问的时候,原序列只会被从左往右扫一遍,而每次向右端点扩展的时候维护一个\(s[]\),让\(s[i]=1\),以及\(s[last[i]]=0\),询问就是询问\(s[]\)\([l,r]\)内的和,单点修改区间求和,嗯树状数组

int pos=0;
rep(i,1,m){
    while(pos<Q[i].r){
        pos++;
        if(last[a[pos]])modify(last[a[pos]],-1);
        last[a[pos]]=pos;
        modify(pos,1);
    }
    Q[i].ans=query(Q[i].r)-query(Q[i].l-1);
}
posted @ 2021-02-23 21:04  yoshinow2001  阅读(128)  评论(0编辑  收藏  举报