权值线段树,动态开点线段树和线段树合并
最考验开数组的一集(巨容易\(RE\))
权值线段树
本质:线段树维护桶
叶子节点表示某数出现了几次,负责\([l,r]\)的点表示\([l,r]\)中的数共出现了几次
用途:
-
查看某数的排名
-
查看某区间内数的个数
-
查看第\(k\)大/小的数
对于第三种操作,我们每次把\(k\)与左/右子树的大小比较,若\(k \leqslant size\),那么所求就在该子树中。否则跳入另一子树并令\(k -= size\)
void add(int pos,int l,int r,int id,int k)
{//pos是数值,k一般是1/-1
if(l == r)
{
t[id] += k;
return;
}
int mid = l + r >> 1;
if(pos <= mid) add(pos,l,mid,ls(id),k);//以pos为下标
else add(pos,mid + 1,r,rs(id),k);
merge(id);
}
int query(int ql,int qr,int l,int r,int id)
{//统计[l,r]内元素个数
int ans = 0;
if(ql <= l && r <= qr) return t[id];
int mid = l + r >> 1;
if(ql <= mid)ans+=query(ql,qr,l,mid,ls(id));
if(qr > mid)ans+=query(ql,qr,mid+1,r,rs(id));
return ans;
}
int kth(int rnk,int l,int r,int id)
{
if(l == r) return l;//直到找到具体数值(叶子)
int mid = l + r >> 1;
int s1 = t[ls(id)];//左子树(较小的数集合)大小
int s2 = t[rs(id)];//右子树(较大的数集合)大小
if(rnk <= s1) return kth(rnk,l,mid,ls(id));//这里找第$k$小,所以先和左子树比
else return kth(rnk - s1,mid + 1,r,rs(id));//说明目标是右子树中第(k - size)小的
}
板子:P3369 【模板】普通平衡树
涵盖了常见操作,直接对\([-1e7-5,1e7+5]\)来操作,能过就是了,但大抵还是要离散化一下的
说到离散化,不得不提一下下面这个东东:
动态开店(bushi)
普通的线段树要预先开四倍空间,会造成大量浪费,因此产生了一种“遇山打洞,遇水架桥”的想法,即只在需要存储的时候才开新的节点,节省了空间的同时,其实也缩小了值域,是个自带离散化\(buff\)的悍匪方法
对于涉及到更新操作的函数而言(如\(add,pushdown,update\)等),需要先检查询问的节点是否建立,如果没有,现场搞一个,有了就不管
对于查询类操作(如\(query,kth\)等),如果访问的节点未建立,直接跳过
另外,这样一来每个节点的左右儿子就要存储在父亲节点中
具体实现就是
更新类:
void add(int pos,int l,int r,int &id,int k)
{
if(!id) id = ++cnt;// 未建立的话现场来一个
if(l == r)
{
t[id].num += k;
return;
}
int mid = l + r >> 1;
if(pos <= mid) add(pos,l,mid,t[id].lson,k);
else add(pos,mid + 1,r,t[id].rson,k);
merge(id);
}
void pushdown(int l,int r,int id)
{
if(t[id].tag)
{
if(!t[id].lson) t[id].lson = ++cnt;
if(!t[id].rson) t[id].rson = ++cnt;//现场建并存储
int mid = l + r >> 1;
int u = t[id].lson;
int v = t[id].rson;
t[u].tag += t[id].tag;
t[v].tag += t[id].tag;
t[u].val += (mid - l + 1) * t[id].tag;
t[v].val += (r - mid) * t[id].tag;
t[id].tag = 0;
}
}
查询类:
int query(int ql,int qr,int l,int r,int id)
{
int ans = 0;
if(!id) return 0;// 未建立就pass
if(ql <= l && r <= qr) return t[id].val;
pushdown(l,r,id);
int mid = l + r >> 1;
if(ql <= mid) ans += query(ql,qr,l,mid,t[id].lson);
if(qr > mid) ans += query(ql,qr,mid + 1,r,t[id].rson);
return ans;
}
将上面二者融合,就是
线段树合并
重叠部分以某种方式叠加,空缺部分互补,可以将若干小树拼成一颗大树,用一般建树顺序遍历各个位置,如果有一棵树此处无就拿另一个树补上,两棵树此处都有信息就叠加,叠加完后更新对应信息(如\(size\)),最终结果就相当于建了一颗完整的树
int merge(int ql,int qr,int l,int r)
{
if(!ql || !qr) return (!ql)?qr:ql;//返回此处无空缺的树的该部分
if(l == r) //叶子节点只能叠加
{
t[ql].num += t[qr].num;
return ql;
}
int mid = l + r >> 1;
//下面是将qr并入ql
t[ql].lson = merge(t[ql].lson,t[qr].lson,l,mid);//合并左子树
t[ql].rson = merge(t[ql].rson,t[qr].rson,mid + 1,r);//合并右子树
t[ql].num = t[t[ql].lson].num + t[t[ql].rson].num;//更新对应信息
return ql;
}
P3521 [POI2011] ROT-Tree Rotations
在合并的时候利用左右子树的信息来搞答案
首先交换不会影响操作点以上的部分(显然)
再次,如果逆序对完全包含在某一子树中,交换也没有影响(也显然)
考虑那些跨越左右子树的逆序对
如果不交换:
蓝框即为存在的逆序对,\(sum = p.rs.size \times q.ls.size\)
交换后:
同理,\(sum' = p.ls.size \times q.rs.size\)
那么每次要合并时,就可以借用得到的左右子信息来算更小的逆序对个数,从而累积得到答案
int merge(int ql,int qr,int l,int r)
{
if(!ql || !qr) return (!ql)?qr:ql;
if(l == r)
{
t[ql].num += t[qr].num;
return ql;
}
u += (ll)t[t[ql].rson].num * t[t[qr].lson].num;
v += (ll)t[t[ql].lson].num * t[t[qr].rson].num;//利用左右子信息
int mid = l + r >> 1;
t[ql].lson = merge(t[ql].lson,t[qr].lson,l,mid);
t[ql].rson = merge(t[ql].rson,t[qr].rson,mid + 1,r);
t[ql].num = t[t[ql].lson].num + t[t[ql].rson].num;//正常合并
return ql;//合并后的根
}
int dfs()
{
int pos;
int val;
scanf("%d",&val);
if(val == 0)
{
int ls = dfs();
int rs = dfs();//dfs到底后回溯得到左右子的信息
//还有一种方法就是存储初始时每棵小权值树的根,见下一道题
u = v = 0;
pos = ST.merge(ls,rs,1,n);//合并
ans += min(u,v);
}
else pos = ST.up(1,n,val);//对于叶子,直接建权值线段树维护具体信息
return pos;
}
P3605 Promotion Counting P
经典题目
对于当前遍历到的节点,先把其子树合并,然后查询出当前节点合并后的子树中比节点权值大的数
int merge(int x,int y)
{
if(!x) return y;
if(!y) return x;
t[x].lson = merge(t[x].lson,t[y].lson);
t[x].rson = merge(t[x].rson,t[y].rson);
t[x].num = [t[x].lson].num + t[t[x].rson].num;
return x;
}//通用板子
void dfs(int x)
{
for(int i = head[x];i;i = e[i].next)
{
int k = e[i].to;
dfs(k);
rt[x] = VT.merge(rt[x],rt[k]); //将以x为根和以k为根的树中的信息合并
}
ans[x] = VT.query(a[x] + 1,n,1,n,rt[x]);//并完后赶紧记录答案
}
这道题给蒟蒻对线段树合并的感觉更像是:初始时就一棵权值树,查到节点\(u\)时,把节点\(u\)以下(以\(u\)为根的树)的节点信息都汇总到\(u\)处,又像是一个求并集的过程。这样的话,求答案时只需累加上\(u\)点的信息,无需遍历子树
所以是点的合并
P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并
这道题其实没有那么典型
首先,它的修改以及查询时用的差分是树剖的专场,但是它要面向每个点开一堆桶建权值线段树,又是线段树合并的专场
奇美拉是吧服了c
而且考虑到要进行比较,所以要建一个伪权值树,非叶子节点表示的实际上是某区间内救济粮数量最大值以及对应的种类
void cal(int id)
{
if(t[ls(id)].num >= t[rs(id)].num) t[id].num = t[ls(id)].num,t[id].c = t[ls(id)].c;
else t[id].num = t[rs(id)].num,t[id].c = t[rs(id)].c;
}//比大小式合并
void update(int pos,int l,int r,int &id,int k)
{
//cout << 114 << endl;
if(!id) id = ++tot;
if(l == r)
{
t[id].num += k;
t[id].c = pos;//记录数量和种类
return;
}
int mid = l + r >> 1;
if(pos <= mid) update(pos,l,mid,ls(id),k);
else update(pos,mid + 1,r,rs(id),k);
cal(id);
}
int merge(int x,int y,int l,int r)
{
if(!x) return y;
if(!y) return x;
if(l == r)
{
t[x].num += t[y].num;
t[x].c = l;
return x;
}
int mid = l + r >> 1;
ls(x) = merge(ls(x),ls(y),l,mid);
rs(x) = merge(rs(x),rs(y),mid + 1,r);
cal(x);
return x;
}//为了记录种类还要加上区间左右端点
for(int i = 1;i <= m;i++)
{
int anc = lca(a[i].x,a[i].y);
VT.update(a[i].z,1,M,rt[a[i].x],1);//cout << 114 << endl;
VT.update(a[i].z,1,M,rt[a[i].y],1);
VT.update(a[i].z,1,M,rt[anc],-1);
if(fa[anc]) VT.update(a[i].z,1,M,rt[fa[anc]],-1);
}//做对点的差分,详情见于LCA篇
不过考虑到树剖的强大,是否能不合并只差分呢?
这时就要想办法区分救济粮种类了
我们可以把每次差分的修改记录下来,比如\(z,-z\),再用一棵权值线段树统一处理这些修改
不是很会就不放码子了
韶身的数列
题目的操作可以转化为记录某一区间的最大值,求和的时候就是\(maxx \times len\)
注意一定不能合并,修改什么的严格直逼叶子(也可能不是)
void add(int sl,int sr,int l,int r,int &id,int k)
{
if(!id) id = ++ cnt;
if(sl == l && sr == r)//超严格,防止以偏概全
{
// cout << l << " " << r << endl;
t[id].maxx = max(t[id].maxx,k);
return;
}
int mid = l + r >> 1;
//if(sl <= mid) add(sl,sr,l,mid,t[id].lson,k);
//if(sr > mid) add(sl,sr,mid + 1,r,t[id].rson,k);
//merge(id);
if(sr <= mid) add(sl,sr,l,mid,t[id].lson,k);
else if(sl > mid) add(sl,sr,mid + 1,r,t[id].rson,k);
else
{
add(sl,mid,l,mid,t[id].lson,k);
add(mid + 1,sr,mid + 1,r,t[id].rson,k);
}//第二种写法更严谨一些
}
void query(int l,int r,int id,int k)
{
//cout << l << " " << r << " " << id << endl;
//if(l == r) return;
if(!id) return;
t[id].maxx = max(t[id].maxx,k);
if((!t[id].lson) && (!t[id].rson))
{
ans += t[id].maxx * (r - l + 1);
return;
}
int mid = l + r >> 1;
query(l,mid,t[id].lson,t[id].maxx);
query(mid + 1,r,t[id].rson,t[id].maxx);
if(!t[id].lson) ans += t[id].maxx * (mid - l + 1);
if(!t[id].rson) ans += t[id].maxx * (r - mid);
}//考虑到该题特殊性,求和的时候采用分治,如果左右子都没有,说明是“叶子”
P3224 [HNOI2012] 永无乡
还算经典
考虑到有连边操作,可以使用并查集记录联通情况,如果不联通,就合并一次,已联通的不管
for(int i = 1;i <= m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
int fu = find(u),fv = find(v);
if(fu != fv)//不联通
{
fa[fv] = fu;
rt[fu] = VT.merge(rt[fu],rt[fv]);//合并,注意此处并查集的尿性,必须使用u,v的上位节点,即fu,fv
}
}
if(op == 'B')
{
int u,v;
scanf("%d%d",&u,&v);
int fu = find(u);
int fv = find(v);
if(fu != fv)
{
//cout << 144 << endl;
fa[fv] = fu;
rt[fu] = VT.merge(rt[fu],rt[fv]);
}
}
if(op == 'Q')
{
int x,k;
scanf("%d%d",&x,&k);
x = find(x);
//cout << "check: ";
// cout << x << endl;
int ans = VT.kth(k,1,n,rt[x]);//这里返回的是节点编号需另行记录
if(ans == 0) printf("-1\n");
else printf("%d\n",ans);
}
魔法少女LJJ
永无乡(1,2,5)+ 烧身的数列(3,4)+ 其它(6,7,8,9)
而且
\(c \leqslant 7\)
有病
对于6,由于只比大小
考虑使用对数,
,维护对数和
有病
更具体的
\(1,2,5\)照搬永无乡
对于\(3,4\),类比可知不能左右子合并,考虑到为了统一操作维护的是权值线段树,我们可以把小于(大于)\(x\)的数的个数清零,并将小于(大于)\(x\)的数的个数求出并加到\(x\)的个数中,这样就等价于把若干数修改成了\(x\)
void Re(int sl,int sr,int l,int r,int id)
{
if(!id) return;
if(sl <= l && r <= sr)
{
t[id].sum = t[id].ds = 0;
t[id].tag = 1;
return;
}
pushdown(id);
int mid = l + r >> 1;
if(sl <= mid) Re(sl,sr,l,mid,ls(id));
if(sr > mid) Re(sl,sr,mid + 1,r,rs(id));
cal(id);
}//清空
···
if(op == 3)
{
scanf("%d%d",&a,&x);
a = find(a);
int newval = VT.query(1,x - 1,1,M,rt[a]);//统计小于x的数的个数
VT.Re(1,x - 1,1,M,rt[a]);//清零
VT.update(x,1,M,rt[a],newval,log(x));//个数累加到x头上
}
if(op == 4)
{
scanf("%d%d",&a,&x);
a = find(a);
int newval = VT.query(x + 1,M,1,M,rt[a]);
VT.Re(x + 1,M,1,M,rt[a]);
VT.update(x,1,M,rt[a],newval,log(x));
}//大于的情况类似
对于\(7\),节点数就是数字个数,直接返回\(t[id].num\)即可
坑点:执行合并,查第\(k\)小等递归操作时的时候也要\(pushdown!\)
int kth(int rnk,int l,int r,int id)
{
if(l == r) return l;
pushdown(id);//下传
int mid = l + r >> 1;
int s1 = t[ls(id)].sum;
if(rnk <= s1) return kth(rnk,l,mid,ls(id));
else return kth(rnk - s1,mid + 1,r,rs(id));
}
int merge(int x,int y)
{
if(!y) return x;
if(!x) return y;
pushdown(x);
pushdown(y);//下传
t[x].sum = t[x].sum + t[y].sum;
t[x].ds = t[x].ds + t[y].ds;
ls(x) = merge(ls(x),ls(y));
rs(x) = merge(rs(x),rs(y));
return x;
}//不合并
P4219 [BJOI2014] 大融合
对于计算\((x,y)\)的负载,答案就是\(x,y\)分别所属的联通块大小的乘积
我们不妨先把最终情况下的树建出来,连边操作使用并查集来存储连通性,用线段树合并模拟连边(合并联通块大小,即数字个数),并使用子树来表示联通块
在树剖中,我们知道,\([dfn_v,dfn_v + siz_v - 1]\)是以\(v\)为根的子树,那么它相对于\([1,n]\)的补集就是与它相连的\(x\)的子树(有并查集和线段树合并把控实时性,也就是说未涉及的操作改动的答案不会被并入查询节点,所以是对的),\(query\)后相乘即可
坑点:最终大抵是个森林,必须挨个dfs,这里深度链顶什么的用不上,可以简化成
一个dfs
void dfs1(int x,int fat)
{
siz[x] = 1;
dfn[x] = ++num;
fa[x] = fat;
for(int i = head[x];i;i = e[i].next)
{
int k = e[i].to;
if(k != fat)
{
dfs1(k,x);
siz[x] += siz[k];
}
}
}//超普通的dfs
for(int i = 1;i <= q;i++)
{
int u = a[i].x,v = a[i].y;
int f = a[i].opt;
if(f == 1)
{
u = find(u);
v = find(v);
dad[v] = u;
//cout << u << v << endl;
//cout << rt[u] << endl;
rt[u] = VT.merge(rt[u],rt[v]);
}
else
{
if(v == fa[u]) swap(u,v);//保证u是v的父亲
int fu = find(u);
//cout << v << endl;
//cout << dfn[v] << " " << dfn[v] + siz[v] - 1 << endl;
//cout << rt[fu] <<endl;
ll s1 = VT.query(dfn[v],dfn[v] + siz[v] - 1,1,n,rt[fu]);//v所在联通块大小
ll s2 = VT.query(1,dfn[v] - 1,1,n,rt[fu]) + VT.query(dfn[v] + siz[v],n,1,n,rt[fu]);//u所在联通块大小
//cout << s1 << " " << s2 << endl;
printf("%lld\n",(ll)s1 * s2);
}
}
P4577 [FJOI2018] 领导集团问题
线段树合并 + dp (啊?)
先想想朴素dp
设\(dp(u,x)\)表示在以\(u\)为根的子树中,最优方案选出的节点权值的最小值为\(x\)
由于锁死了最小,所以可以一路并上去,相当于一个树形dp
\(O(n^3)\)的,可拿十分
void dfs(int x,int fa)
{
for(int i = head[x];i;i = e[i].next)
{
int k = e[i].to;
if(k == fa) continue;
dfs(k,x);
for(int j = 1;j <= m;j++)
{
int ans = -1;
for(int l = j;l <= n;l++) ans = max(ans,dp[k][l]);//求max
dp[x][j] += ans;//求和
}
}
}
对于求max那一步,我们可以考虑预处理一个后缀\(max\)数组把复杂度压到\(O(n^2)\)
如果再要优化,就可以对max数组做差分,然后使用线段树合并直接合并差分数组,最后遍历一边得到答案,\(O(nlogn)\)
更具体的,合并的过程中,如果我们在\(w_x\)处\(+1\)了,说明最小值锁死为\(w_x\),那么就要在\(w_x\)之前为\(1\)的位置\(-1\)
根据\(dp\)尿性,位置就是\(w_x - 1\)
坑点:
- 由于\(w_x \in[1,s (< n)]\),所以大的值域(\(w_x - 1\))是\([0,n]\),不是\([1,n]\)
- 不能乱合并
struct ValTree
{
#define ls(x) t[x].lson
#define rs(x) t[x].rson
struct no
{
int lson,rson,sum;
}t[N * 25];
void cal(int id){t[id].sum = t[ls(id)].sum + t[rs(id)].sum;}
void update(int pos,int l,int r,int &id,int k)
{
if(!id) id = ++ tot;
if(l == r)
{
t[id].sum += k;
return;
}
int mid = l + r >> 1;
if(pos <= mid) update(pos,l,mid,ls(id),k);
else update(pos,mid + 1,r,rs(id),k);
cal(id);
}
int find(int pos,int l,int r,int id)//找[0,a[x] - 1]内非零点
{
if(!id) return 0;
if(l == r) return t[id].sum ? l : 0;//如果该位置sum不为零且在a[x]前,就要操作
int mid = l + r >> 1;
if(pos <= mid) return find(pos,l,mid,ls(id));
else
{
int s = find(pos,mid + 1,r,rs(id));
if(s) return s;
else return find(pos,l,mid,ls(id));
}
}
int merge(int x,int y)
{
if(!x) return y;
if(!y) return x;
t[x].sum += t[y].sum;
ls(x) = merge(ls(x),ls(y));
rs(x) = merge(rs(x),rs(y));
//cal(x);//不能合并
return x;
}
void getans()
{
printf("%d",t[rt[1]].sum);//合并(遍历)完后答案汇集在根处
}
}VT;
void dfs(int x,int fa)
{
for(int i = head[x];i;i = e[i].next)
{
int k = e[i].to;
dfs(k,x);
rt[x] = VT.merge(rt[x],rt[k]);
}
VT.update(a[x],0,m,rt[x],1);
int res = VT.find(a[x] - 1,0,m,rt[x]);2
if(res) VT.update(res,0,m,rt[x],-1);//差分操作,注意下限
}
还有一个方法,这个题相当于一个树上LIS,可以从此处入手,其实求LIS时使用到的贪心就是找到第一个\(i\)满足\(dp(u,i) \geqslant w_u\),这样直接把\(u\)接到\(i\)前头,和线段树中一路并上去是一个道理
但我不会
发现里头没有\(dp\)的数组、递推式等,却用到了\(dp\)的思想,这说明\(dp\)可以进一步推广为类似记忆化搜索的操作 (也可能是不太典型)