数据结构相关

还是决定单独拎出来写...

线段树

好像自己从来没写过动态开点(?)

动态开点

顾名思义,动态的开线段树上的节点,以达到节省空间的目的,这种技巧我们常用在普通线段树无法开下/值域过大时可以使用,动态开点线段树上的区间修改需要用到标记永久化,当然标记需要满足结合律和交换律,互相覆盖的标记是用不了的。

给一个单点加,区间查询的代码,树状数组1。

code

标记永久化

直接接着上文讲了,会发现,在动态开点线段树上,我们不太好维护区间加法,每次下传标记空间还是会爆掉,我们此时考虑每个点的标记不进行下传,查询时把路径上的标记再统计上即可,当然,这种思路只能适用于一些满足结合律和交换律的东西上,比如加法)。而且,这玩意不仅可以用在动态开点线段树,主席树,或者一些难以下传标记的东西里面。

例题:SP11470CF960HP3313

一个是主席树区间加的板子,一个是标记永久化+树剖+期望,一个是树剖+动态开点线段树,从另一篇博客抄过来的啦,会补的,别急。

维护历史信息

吉老师线段树,还要维护多个信息。

线段树二分

线段每个区间上二分以处理某些问题。

李超线段树

线段树每个节点维护最优线段,以中点作为比较的依据,假设现在有两条线段 \(l_1\)\(l_2\),假如 \(l_1\) 在这个区间的中点高于 \(l_2\),那么显然他在这个区间更优,交换最优线段(默认取最大 \(y\) 值),显然这样并没有结束,继续向左右分治,重复上述过程,就能够维护这条线段,而我们需要在线段树上将这条线段拆成 \(\log\) 条,然后分别向下传递,所以插入复杂度 \(\log^2\),查询时我们考虑便利到每一个区间看看当前区间的最优线段在 \(k\) 的取值,然后取个最大值即可,挺好理解的。

依次,我们就可以维护平面内多条线段,发现斜率优化也是用这东西,所以很多斜率优化的题就可以李超树,会在 dp 专题写的。

动态开点code

可持久化

写完动态开点就感觉这东西很简单了,考虑我们要维护每个时间戳下的线段树,暴力建出所有线段树显然是空间爆炸的,我们可以利用动态开点,记录每棵线段树的根,分别作操作,单点修改就普通的直接修改,区间修改就需要标记永久化了,具体地,我们不进行下传标记的操作,因为这样我们的空间还是会爆炸,我们把只修改每个拆分出的小区间的标记,进而在查询时依次合并信息做到保证时间和空间复杂度。区间加板子 SP11470

感觉现在是懂了主席树的思想了,考虑我们有一个静态区间问题,比如 P1972 [SDOI2009] HH的项链,我们要询问静态区间不同颜色数,其实可以用类似动态扫描线的做法,扔到主席树上,每个主席树维护前缀区间的信息,然后差分出答案,比方说这题我们是令答案为 \(\sum_{i=l}^{r} [las_i < l]\),然后在两棵前缀主席树上作差查询答案,其实这里的主席树就可以看做动态开点的权值线段树了。

再者就是查询区间第 \(k\) 大的问题,也是两棵权值线段树作差,然后比较左右子树的大小再向左/右,跟平衡树查询的思路是一致的。给一个裸题 P2633 Count on a tree,树剖+主席树维护 \(k\) 小值,具体来讲,我们需要维护从每个点到根的前缀主席树,然后答案就是 \(rt_u+rt_v-rt_{lca}-rt_{fa_{lca}}\),所以我们需要维护四棵主席树作差的查询 (。

练习

P2824

二分不好想到,但想到二分就秒了,首先考虑 01 序列怎么搞,排序就简单的查个数量然后直接覆盖,但是考虑怎么将原序列变成 01 序列,发现我们可以固定一个阀值,大于就 1,小于就 0,交换完成之后再查询选点的值,显然,选点最后一个为 1 的值就是答案,所以这东西是具有单调性的,可以二分,复杂度 \(O(n\log^2n)\)

P3313

总算是填了前面的大坑,动态开点+树剖,板子题,就是注意细节,树剖都能写错我也是唐完了。

平衡树

从洛谷搬过来了。

开新坑,从寒假拖到暑假我也是无敌了。

这里统一写,平衡树的值 lazy 标记下传和线段树是基本一致的。

AVL 树

一种严格平衡的 BST,通过左旋右旋操作维护平衡因子实现平衡。这贴两张左右旋的图。毕竟这不是重点。

左旋

右旋

Splay 树

一种很常用的平衡树,也能用在 LCT(喜) 由于 AVL 树维护平衡因子的麻烦操作,导致的代码冗长,我们更加常用的是类似于 AVL 的 Splay 伸展树和 Treap 堆,此处先介绍 Splay。

操作

还是挨个写吧。作为伸展树的 Splay 也使用 AVL 树的左右旋操作,但是他需要再操作完成之后再将操作节点转到根,或者多转一次来降低复杂度。

  1. 旋转 rotate。把左右旋扔到一起做了,通过位运算实现,写一下步骤。记 \(x\)\(y\)\(z\) 分别为当前点,父亲,祖父。
  • \(x\) 放到原本 \(y\)\(z\) 的那颗子树上,将 \(x\) 的父亲改为 \(z\)
  • 假设原本 \(x\)\(y\) 的右子树,那么我们把 \(x\) 的左子树挂到 \(y\) 的右子树上,把那个子树的父亲改为 \(y\)。反之亦然。
  • 接上,此时我们把 \(x\) 的左子树改为 \(y\),改一下 \(y\) 的父亲。反之亦然。
  • 两遍 pushup,注意还是从下往上,先 \(y\)\(x\)
inline void rotate(ll x){
	ll y=tree[x].fa,z=tree[y].fa,k=tree[y].ch[1]==x;
	tree[z].ch[tree[z].ch[1]==y]=x;tree[x].fa=z;
	tree[y].ch[k]=tree[x].ch[k^1];tree[tree[x].ch[k^1]].fa=y;
	tree[x].ch[k^1]=y;tree[y].fa=x;
	pushup(y),pushup(x);
}
  1. splay,将某点旋转至某处,Splay的核心操作。假如当前还没有转到指定点,懒得想了,假如当前是一字型 (\(LL\)\(RR\)),我们就转 \(x\),否则转 \(x\)父亲 \(y\)。假如要转到的点是根,那么将该点改成根。注意每次判断之后需要再旋转一次 \(x\)
inline void splay(ll x,ll k){
	while(tree[x].fa!=k){
		ll y=tree[x].fa,z=tree[y].fa;
		if(z!=k) (tree[y].ch[0]==x)^(tree[z].ch[0]==y)?rotate(x):rotate(y);
		rotate(x);
	}
	if(!k) root=x;
}
  1. insert,插入操作。从一个点开始,每次按照权值向左/右找,同时记录父亲。假如碰到了空点,或者找到了有应该插入的值的点,那么跳出循环,下面判断一下是因为什么跳出的即可,新建点。最后将这个点转到根节点。
inline void insert(ll x){
	ll u=root,fa=0;
	while(u&&tree[u].val!=x) fa=u,u=tree[u].ch[x>tree[u].val];
	if(u) tree[u].same++;
	else u=++tot,tree[fa].ch[x>tree[fa].val]=u,tree[tot]={{0,0},fa,x,1,1};
	splay(u,0);
}
  1. getx,找到权值为某值的点。由于 Splay 的特殊性质,我们反复向左/右走,路径上记录答案即可。注意结束操作之后不用 splay 一下。
inline ll getx(ll x){//找到权值为 x 的点的标号 
	ll u=root,ans;
	while(u){
		if(tree[u].val>=x) ans=u,u=tree[u].ch[0];
		else u=tree[u].ch[1];
	}
	return ans;
}
  1. Next,找前驱/后继。对于找单点的前驱/后继,由于他的答案可能在父亲上面,我们要先将他旋转至根,向左向右找即可。找到答案之后 splay 一下。
inline ll Next(ll x,ll op){//找前驱,后继 
	ll p=getx(x);splay(p,0);
	ll u=root;
	if((tree[u].val>x&&op)||(tree[u].val<x&&!op)) return u;
	u=tree[u].ch[op];
	while(tree[u].ch[op^1]) u=tree[u].ch[op^1];
	splay(u,0);
	return u;
}

6.kth,找区间第 k 大。我们按照 BST 性质往左右找即可,往右找的时候减去左子树和当前点大小。最后 splay 一下降低复杂度。好像不 splay 复杂度是假的?

inline ll kth(ll x){
	ll u=root;
	if(tree[root].siz<x) return 0;
	while(u){
		if(x>tree[tree[u].ch[0]].siz+tree[u].same) x-=(tree[tree[u].ch[0]].siz+tree[u].same),u=tree[u].ch[1];
		else if(tree[tree[u].ch[0]].siz>=x) u=tree[u].ch[0];
		else{splay(u,0);return u;}
	}
	splay(x,0);
	return 0;
}
  1. del,删除。我怎么少了一个,删除单点的话,假设我们现在要删除 \(x\),那我们找出他的前驱 \(y\),后继 \(z\),将前驱转到根节点,将后继转到 \(y\) 的右儿子,此时 \(z\) 的左儿子就是 \(x\)。直接删掉即可。
inline void del(ll x){
	ll pre=Next(x,0),suf=Next(x,1);
	splay(pre,0),splay(suf,pre);
	ll u=tree[suf].ch[0];
	if(tree[u].same>1) tree[u].same--,splay(u,0);
	else tree[suf].ch[0]=0,splay(suf,0);
}

8.getk,查询排名。好像名字写错了,将他插入之后转到根,此时左儿子大小就是答案。

inline ll getk(ll x){
	insert(x);
	ll ans=tree[tree[root].ch[0]].siz;
	del(x);
	return ans;
}
  1. 这里再说一个操作吧,提取区间。假定现在要一个区间 \([l,r]\),我们将 \(l-1\) 转到根,\(r+1\) 转到根节点的右儿子即可,此时根节点右儿子的左子树就是要查询区间,注意,由于哨兵的影响,我们需要将下标向右移一位,就是 \(l\)\(r+2\)

  2. 怎么还有一个。回收内存机制,因为这种大数据结构中存在插入删除操作,假如我们反复增删,导致的下标爆炸,所以我们采用一个数组,将删掉的下标扔进去,用的时候再拿出来,就可以起到减少空间复杂度的效果。

感觉 splay 比 Treap 麻烦呢,代码还要卡常,难绷。

code

Treap

一种看脸的平衡树,同时满足 Tree 和 Heap 性质的平衡树。

Treap 中,每个节点有两个权值,一个 \(key\) 表示它原本的权值,我们再赋予一个 \(val\) 的随机权,令它满足大根堆的基本性质,以保证复杂度。

操作

想了想还是一边贴代码一边写吧。

  1. 新建。字面意思,注意 Treap 需要给随机权 \(val\)
  2. 左旋。同 AVL 树的左旋操作,Treap 的左右旋分开了,感觉好理解了很多。此处设定 \(x\) 为左旋的关键点\(u\) 为左旋关键点的右儿子,我们此处要将 \(x\) 左旋下去,或者称将 \(u\) 旋转上来。我们现将 \(u\) 的左子树挂到 \(x\) 的右子树上,再将 \(u\) 的左儿子改为 \(x\) 就结束了,注意 puhsup 时先操作下面的再操作上面的。
inline void zag(ll &x){//left
	ll u=tree[x].r;
	tree[x].r=tree[u].l;tree[u].l=x;x=u;
	pushup(tree[x].l),pushup(x);
}
  1. 右旋。其实是左旋是一样。\(u\)\(x\) 的左儿子,令 \(u\) 的右子树挂到 \(x\) 的左子树上即可,再传递。或者可以将左右旋操作理解为,\(u\)\(x\) 交换位置,但需要换个方向,修改后 \(x\)\(u\) 的哪里,就把 \(u\) 没地方放的那个子树挂过去。
inline void zig(ll &x){//right
	ll u=tree[x].l;
	tree[x].l=tree[u].r;tree[u].r=x,x=u;
	pushup(tree[x].r),pushup(x);
}
  1. 建树。Treap 比较特殊的操作。依次插入哨兵,赋完随机权后,假如两个点随机权反了,就左旋一下,完事。
inline void build(){
	make_node(-INF),make_node(INF);
	root=1;tree[1].r=2;
	pushup(root);
	if(tree[1].val<tree[2].val) zag(root);
}

5.插入。由于 Treap 是同时满足 BST 和堆性质的数据结构。我们可以先按照 BST 的方式,按照大小把它插入。还是讲一下流程吧,当遍历到一个点,假如他的键值等于当前插入值,直接计数器增加,假如键值小于,往右走,大于往左走。插入之后再按照堆的性质比较,一遍遍再转上来即可,由于还是要满足 BST 的性质,所以需要全程左右旋,只要子树的 \(val\) 值大于当前点就旋转,左子树右旋,右子树左旋。(还挺异或的?)

inline void insert(ll &x,ll key){
	if(!x) x=make_node(key);
	else if(tree[x].key==key) tree[x].cnt++;
	else if(tree[x].key>key){
		insert(tree[x].l,key);
		if(tree[tree[x].l].val>tree[x].val) zig(x);
	}
	else{
		insert(tree[x].r,key);
		if(tree[tree[x].r].val>tree[x].val) zag(x);
	}
	pushup(x);
}

6.删除。一个比较难理解的操作,但不要紧,我们分情况讨论一下,假定当前点为 \(x\)

  • \(x\) 的键值大于删除的键值时,说明答案在左子树,往左走。
  • \(x\) 的键值小于删除的键值时,说明答案在右子树,往右走
  • 当我们找到了这个键值,假如这个节点处有值,那我们也不用动了,直接删除,因为这里本来也满足平衡性。假如这个地方只有一个值,那我们 Treap 没法像 Splay 一样把他转上去删掉,我们需要把它转下去。假如这里有左子树,而且左子树的权值大于右子树的权值,那么我们右旋之后他还是可以满足性质的,我们直接右旋把当前点转下去即可。假如它不满足这个条件,我们直接给他往另一边转即可。当然,这是它不是叶子的情况,如果是叶子,直接清掉这个点就行了。
inline void del(ll &x,ll key){
	if(!x) return;
	if(tree[x].key==key){
		if(tree[x].cnt>1) tree[x].cnt--;
		else if(tree[x].l||tree[x].r){
			if(!tree[x].r||tree[tree[x].l].val>tree[tree[x].r].val) zig(x),del(tree[x].r,key);
			else zag(x),del(tree[x].l,key);
		}
		else x=0;
	}
	else if(tree[x].key>key) del(tree[x].l,key);
	else del(tree[x].r,key);
	pushup(x);
}
  1. 通过值查排名。其实到这就已经很 BST 了,分类讨论:
  • 假如当前点是查找的 \(key\),那么他的排名就是左子树的大小 \(+1\)
  • 假如当前点的键值大于 \(key\),我们需要到左子树寻找。
  • 假如当前点的键值小于 \(key\),我们需要到右子树寻找,顺便加上左子树大小和当前点相同的个数。
inline ll getrank(ll x,ll key){
	if(!x) return 1;
	if(tree[x].key==key) return tree[tree[x].l].siz+1;
	if(tree[x].key>key) return getrank(tree[x].l,key);
	return tree[tree[x].l].siz+tree[x].cnt+getrank(tree[x].r,key);
}
  1. 上面那个操作反过来,通过排名查值。假如左子树的大小大于 \(rank\),往左走,假如左子树大小加上当前点的个数大于等于 \(rank\) 说明这个点就是查询的点,否则就往右子树走,用 \(rank\) 减去左子树大小和这个点大小。
inline ll getkey(ll x,ll rank){
	if(!x) return INF;
	if(tree[tree[x].l].siz>=rank) return getkey(tree[x].l,rank);
	if(tree[tree[x].l].siz+tree[x].cnt>=rank) return tree[x].key;
	return getkey(tree[x].r,rank-tree[tree[x].l].siz-tree[x].cnt);
}
  1. 查前驱。简单函数,假如当前键值大于等于 \(key\) 说明需要往左走,否则往右走,对路径取个最小值。
inline ll getpre(ll x,ll key){
	if(!x) return -INF;
	if(tree[x].key>=key) return getpre(tree[x].l,key);
	return max(tree[x].key,getpre(tree[x].r,key));
}
  1. 查后继。正好反过来,假如键值小于等于 \(key\) 说明需要往右走,否则往左走,对路径取个最大值。
inline ll getsuf(ll x,ll key){
	if(!x) return INF;
	if(tree[x].key<=key) return getsuf(tree[x].r,key);
	return min(tree[x].key,getsuf(tree[x].l,key));
}	

操作都挺短的,看代码吧,注意哨兵的影响。

code

FHQ-Treap

一种码量小,思想简洁的平衡树,但是没法用于 LCT 问题,通过分裂和合并来实现各种操作,跑起来好像比 Splay 要快一点。

操作

  1. Split 分裂。设我们将一棵树按照 \(k\) 的权值分裂,分类讨论:
  • 假如 \(x\) 的权值大于 \(k\) 说明 \(x\) 的右子树一定会在拆分后的新树上,移动指针到左子树即可。
  • 假如 \(x\) 的权值等于 \(k\) 说明此时 \(x\) 的左子树一定不在拆分后的树里,移动指针。
  • 假如 \(x\) 的权值小于 \(k\) 说明此时 \(x\) 及其子树一定符合条件,会在拆分后的树里面,移动指针。
inline void split(ll p,ll k,ll &pl,ll &pr){
	if(!p){pl = pr = 0;return;}
	else if(tree[p].key <= k) pl = p,split(tree[p].r,k,tree[p].r,pr);
	else pr = p,split(tree[p].l,k,pl,tree[p].l); 
	pushup(p);
}
  1. merge 合并。此时给定两棵树的指针 \(l\)\(r\)\(val_i\) 表示 \(i\) 的随机权值。由于 FHQ 也要满足大根堆的性质,当考虑两棵树如何合并时,我们有限考虑他们的随机权。
  • \(val_l<val_r\) 说明此时 \(l\) 的左子树不用动,往 \(l\) 的右子树走去合并即可,记得 pushup。
  • \(val_r<val_l\) 说明此时 \(r\) 的左子树不用动,往 \(r\) 的左子树走去合并即可。记得 pushup。
  • 假如当前某一棵树为空,直接返回另一棵树。
inline ll merge(ll pl,ll pr){
	if(!pl||!pr) return pl+pr;
	if(tree[pl].val<tree[pr].val){tree[pl].r=merge(tree[pl].r,pr);pushup(pl);return pl;}
	else{tree[pr].l=merge(pl,tree[pr].l);pushup(pr);return pr;}
}

感觉代码还是挺简单的,其余的操作可以按照 Treap 做,或者按照 FHQ 的 Split 和 Merge 操作也是可以的。

code

莫队

一种离线算法,可以用 \(O(n\sqrt n)\) 的复杂度处理区间查询问题,当然,也可以带修,下文也会提到。

关于复杂度

莫队优化的关键是排序 + 分块,将每个询问离线下来,按照左端点所在块从小到大排序,假如左端点在同一个块,按照右端点从小到大排序。

处理问题时,我们可以通过移动左右端点来不断更新区间答案,而且排序后前后两个左端点的距离(移动次数)不会超过 \(2\times \sqrt n\) 次,总共 \(n\) 个查询,复杂度相乘也就是 \(O(n\sqrt n)\)。而因为右端点无序,但是固定左端的情况下按升序排列,所以因为左端有 \(O(\sqrt n)\) 块,而右端点一次移动 \(O(n)\) 次,总的也就是 \(O(n \sqrt n)\)。所以,莫队算法的总复杂度就是 \(O(n\sqrt n)\)

其实,这种排序方式并不是最优解,最优解应该按照曼哈顿距离建最小生成树,但因为本身写这个就是暴力算法,这个也就无所谓了。

一种优化方式,奇偶性排序。在移动莫队指针的过程中,两个询问之间左右移动,可能会出现多余移动的情况,那么我们可以按照奇块正序,偶块反序的方式来排序,可以优化 30% 左右。

P1972 HH的项链

模板,开桶维护区间数个数,虽然加强了数据,卡卡也是能过的。

code

P1494 小 Z 的袜子

假设有 \(k\) 个相同的数,那么会有 \(\left (_{2}^{k} \right )\) 种选法,总共 \(\sum \left (_{2}^{cnt_x} \right )\) 种,\(cnt_i\) 表示 \(i\) 的数量。区间内随便选一对的方案数是 \(\left (_{2}^{r-l+1} \right )\),答案就是这俩比一下就完了,之后莫队扩缩区间维护。

code

P5268 [SNOI2017] 一个简单的询问

\(get(l,r,x)\) 表示 \([l,r]\) 区间内 \(x\) 的出现次数,求 \(\sum get(l_1,r_1,i)\times get(l_2,r_2,i)\)

会发现这个式子很难直接推,考虑做一下差分,\(get(l_1,r_1,x)\) 可以拆成 \(get(1,r_1,x) - get(1,l_1-1,x)\)

拓展到整个式子(令 \(g(l,x)\) 表示 \(get(1,l,x)\))

\[get(l_1,r_1,i)\times get(l_2,r_2,i)=(g(r_1,i)-g(l_1-1,i))\times (g(r_2,i)-g(l_2-1,i)) \]

\[=g(r_1,i)g(r_2,i)-g(l_1-1,i)g(r_2,i)-g(r_1,i)g(l_2,-1)+g(l_1-1,i)g(l_2-1,i) \]

做个前缀和,就可以将整个式子当作四个询问,直接莫队统计答案即可。

code

P4396 [AHOI2013] 作业

这题相对于板子,只是加了个取值在 \([l,r]\) 的限制,对于这个东西,完全可以考虑树状数组,每次莫队扔进树状数组,出来直接前缀和统计答案即可。

所以 蓝 + 黄 = 紫?

复杂度 \(O(n\sqrt n\log n)\)

code

带修莫队

在区间问题中,可能会存在修改操作,虽然是离线算法,但莫队也是能做的。

假设普通莫队有 \((i,j)\) 两个维度,表示区间左右端点,那我们可以再加上一维时间维 \((i,j,t)\) 表示区间在第 \(t\) 秒的答案。

P1903 数颜色 / 维护队列

以本题为例,将时间维加入排序,当作排序的第三关键字。莫队操作中,假如当前有一个修改操作,将 \(a\) 位置与 \(b\) 位置交换,将 \(a\) 位置 \(del\),将 \(b\) 位置 \(add\)。即可,反操作只是将 \(a\)\(b\) 交换。

code

树上莫队

现在考虑将莫队放到树上,其实直接按照欧拉序把树扔到一维平面上,欧拉序这东西就是每次深搜走到走回来都记录一下,这东西剖一下就好了,但是 \(lca\) 可能不在一段欧拉序中,所以需要特判,之后做莫队即可。

Count on a tree II

模板题。

code

P4074 [WC2013] 糖果公园

给定树上 \(n\) 个数,每个数有 \(w_i\) 的权值,区间内第 \(i\) 个数的价值权重是 \(v_i\),总权值定义为区间内 \(\sum w_i\times v_{cnt_i}\),每次单点修改或者查询链上价值和。

上两种莫队的综合。斯人码量题

由于是普通莫队,直接增加/减少上式即可,没什么好强调的,重点的是时间维度的意义,代表的是修改操作的下标,对应修改操作也是原树内固定的,不用再映射,注意欧拉序上的分类讨论,标记数组不要忘记。

code

填坑

回滚莫队

变种莫队,用于处理难以增加/删除的问题,比如区间众数,\(O(1)\) 增加,\(O(n)\) 删除,我们就可以只进行一类操作,通过回滚解决问题,这也将回滚莫队分为只增不删莫队和只删不增莫队,此处先介绍只增不删莫队。

回滚莫队的流程:

  • 序列分块,划分出每个询问左右端点的所在块,通过排序使得左端点按所在块排序,右端点根据左端点升序排列。

  • 分情况讨论,假如 \(l\)\(r\) 在同一块内,我们可以 \(O(\sqrt n)\) 的处理询问

  • \(l\)\(r\) 为莫队操作的区间,假如 \(l\)\(r\) 不在同一块内,\(L\)\(R\) 为左端点所在块的左右端点,\(x\)\(y\) 为询问的左右端点。初始时,令 \(l \gets R+1\)\(r \gets R\)

  • 介于这种情况下通常认为增加是 \(O(1)\) 的,我们先移动 \(r\)\(y\)记录下当前答案,然后新建变量,记录 \(l^{'}\) 向左增加的答案,之后统计答案,将 \(l\) 指针删掉来时增加的量,回到 \(R+1\),实现回滚。

  • 当然,这只是一个块内询问的处理方法,假如当前询问与上一次询问不在同一块内,需要重新移动 \(l\)\(r\)\(R+1\)\(R\),或许可以将这种回滚莫队看作对每个块都做一遍莫队( ? )。

AT_joisc2014_c 歴史の研究

价值定义为区间内某值出现次数与该值的乘积,每次询问求区间最大价值。

好像比板子还要板,拿这个当板子挺好。

code

P5906 【模板】回滚莫队&不删除莫队

定义价值为区间内相同值的下标差绝对值,求区间价值最大值。

好像没那么板了( ? ),记区间内每个值出现的最早/最晚出现的位置为 \(min_{a_i}\)\(max_{a_i}\),向右增加时,注意需要时刻更改 \(max_i\),在回滚时,假如当前 \(max_i = i\),说明已经找到了最靠右的位置,直接清零。

注意莫队的移动顺序和数组清理。

code

关于莫队的一些注意问题

记得设块长,每个题块长可能不同。

奇偶优化回滚莫队是不能用的。

注意莫队移动指针时的顺序:

while(l>q[i].l) add(--l);
while(r<q[i].r) add(++r);
while(l<q[i].l) del(l++);
while(r>q[i].r) del(r--);

也可以看 wiki 上的详解。

注意细节,经常卡卡常。

\(l=1,r=0\)

posted @ 2024-08-24 09:30  Wei_Han  阅读(23)  评论(0)    收藏  举报