Loading

线段树进阶(习题篇)

线段树进阶(习题篇)

动态开点线段树

CF960H动态开点模板题
首先,考虑拆贡献

\[\begin{aligned} &\frac{\sum_{i=1}^n(b_k*S_{i,k}-C)^2}{n} \\ &=\frac{\sum_{i=1}^{n}b_k*s_{i,k}^2-2*C \sum_{i=1}^{n}b_k*s_{i,k}}{n}+C^2 \end{aligned} \]

\(b_k\)是颜色i更改所用的花费,\(s_{i,k}\)是以节点为根的子树,颜色k的数目)可以发现最终只有\(S_{i,k}^2\)\(S_{i,k}\)不是常数,所以我们只要维护这两个数即可,先考虑简单点,m只有200 ,那么显然我们就可以先树剖,然后开m个线段树,对于每个节点存下\(S\),和\(S^2\),或者开一个线段树,每个节点存两个数组,分别是每种颜色\(S\)\(S^2\),合并的话考虑\(\sum_{i=1}^{n}(X_i+v)^2=\sum_{i=1}^nX_i^2+\)\(n*v^2+2*v*\sum_{i=1}^nX_i\)。只要加后半段即可
考虑若m变大怎么办,可以发现并不是每个线段树节点都有用到,有些浪费空间,考虑动态开点,对于每种颜色开个线段树,有用到某个节点就新增一个。就做完了\(O(nlog^2n)\),空间复杂度,考虑建树时已经有O(nlogn)个节点,每次修改要加logn个,一共是O((n+q)logn)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define N_ 50005
#define M_ 5000000
#define inf 1e12
#define base 1e-8
void read(ll& x)
{
    char ch=getchar();ll f=1;x=0;
    while(ch<'0'||ch>'9'){
        if(ch=='-')f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        x=x*10+ch-48;
        ch=getchar();
    }
    x=x*f;
}
ll tot_,b[N_],fa[N_],n,m,q,C,dep[N_],tot,node,F[N_],L[M_],R[M_],root[N_],ls[M_],rs[M_],tap[M_],head[N_],to[N_*2],V[N_*2],top[N_],dfn[N_],son[N_],siz[N_];
void add(ll u,ll v)
{V[++tot]=v;to[tot]=head[u];head[u]=tot;}
struct Node
{
    ll v,sq;
}c[M_];
void nd(ll& k,ll l,ll r)
{
    k=++node;
    L[k]=l;R[k]=r;
}
void push_down(ll k)
{
    ll l=L[k],r=R[k];
    ll mid = l + r >> 1;
    if(!ls[k])nd(ls[k],l,mid);
    tap[ls[k]]+=tap[k];
    if(!rs[k])nd(rs[k],mid+1,r);
    tap[rs[k]]+=tap[k];
    c[ls[k]].sq+=(c[ls[k]].v*2*tap[k]+(R[ls[k]]-L[ls[k]]+1)*tap[k]*tap[k]);
    c[rs[k]].sq+=(c[rs[k]].v*2*tap[k]+(R[rs[k]]-L[rs[k]]+1)*tap[k]*tap[k]);
    c[ls[k]].v+=((R[ls[k]]-L[ls[k]]+1)*tap[k]);c[rs[k]].v+=((R[rs[k]]-L[rs[k]]+1)*tap[k]);
    tap[k]=0;
}
void push_up(ll k)
{
    c[k].v=c[ls[k]].v+c[rs[k]].v;
    c[k].sq=c[ls[k]].sq+c[rs[k]].sq;
}
void modify(ll& k,ll l,ll r,ll p,ll q,ll val)
{
    if(!k)nd(k,l,r);
    if(l>=p&&r<=q)
    {
        c[k].sq+=(r-l+1);
        c[k].sq+=(2*c[k].v*val);
        c[k].v+=((r-l+1)*val);
        tap[k]+=val;
        return ;
    }
    if(l==r)return ;
    if(tap[k])push_down(k);
    ll mid=l+r>>1;
    if(p<=mid)modify(ls[k],l,mid,p,q,val);
    if(q>mid)modify(rs[k],mid+1,r,p,q,val);
    push_up(k);
}
void dfs(ll u,ll f)
{
    ll maxx=0;siz[u]=1;
    for(int i=head[u];i;i=to[i])
    {
        ll v=V[i];
        if(v==f)continue;
        dep[v]=dep[u]+1;fa[v]=u;
        dfs(v,u);siz[u]+=siz[v];
        if(siz[v]>maxx)son[u]=v,maxx=siz[v];
    }
}
void dfs2(ll u,ll k)
{
    top[u]=k;dfn[u]=++tot_;
    if(son[u])dfs2(son[u],k);
    for(int i=head[u];i;i=to[i])
    {
        ll v=V[i];
        if(dfn[v])continue;
        dfs2(v,v);
    }
}
void Add(ll x,ll cl,ll val)
{
    while(x)
    {modify(root[cl],1,n,dfn[top[x]],dfn[x],val),x=fa[top[x]];}
}
int main()
{
    read(n);read(m);read(q);read(C);
    for(int i=1;i<=n;i++)read(F[i]);
    for(int i=2;i<=n;i++){
        ll p;read(p);
        add(i,p);add(p,i);
    }
    for(int i=1;i<=m;i++)read(b[i]);
    dfs(1,-1);dfs2(1,1);
    for(int i=1;i<=n;i++)Add(i,F[i],1);
    while(q--)
    {
        ll t;
        read(t);
        if(t==1)
        {
            ll x,w;
            read(x);read(w);
            Add(x,F[x],-1);
            Add(x,w,1);F[x]=w;
        }
        else{
            ll k;
            read(k);
            printf("%.6f\n",C*C+(1.0*b[k]*b[k]*c[root[k]].sq-2*b[k]*C*c[root[k]].v)/n);
        }
    }
    return 0;
}
/*
 
1 4 1
1 1 2
2 1
2 2
*/

关于线段树注意些细节:1.传递tap时要注意儿子是否开过点。2.递归时遇见l==r是否return;3.tap是否清空
总结:思路不难,细节很多,遇见代数式一般都要变形,当常数参数混在一起是,要常变分离
P1502一个扫描线好题(有点像二维偏序)
首先考虑到n很小考虑枚举n,如果枚举矩形肯定会炸,考虑如果以每个星作为左下角贪心的枚举矩形,发现是假的,但也提供一个思路,如果一个矩形要覆盖到这颗星,那么他的右上角一定在以这颗星的为左下角的矩形中,那么划归转化后就变成:以每颗星为坐下角画个矩形,求一个取点使得覆盖它的矩形的权值和最大。那么就好做了,扫描线一下,将求矩形面积并->求最大值
注意几个细节:
1.题目中说边框不算,所以每个矩形实际范围是(x,y)~(x+w-1,y+H-1),注意到如果H是1,那么扫描线的时候区间加会出bug,有俩种解决办法,一种是将他给的坐标和H,W都x2 另一种将add时范围-1,不减
2.考虑到只查询线段树根的值,所以考虑标记永久化,反正+val后一定会-val,push_up时+标记就可以了
3.有些矩形边界x会重合,sort的时候考虑一下就行
总结:扫描线的时候一般都是标记永久化,正解想不出多想想暴力获得启发。

posted @ 2025-11-12 21:52  leaf_ydc  阅读(6)  评论(0)    收藏  举报