• 差分


    1. 差分数组

    • 差分数组的定义:记录当前位置的数与上一位置的数的差值.
    原数组 (a_i) 9 4 7 5 9
    差分数组 (b_i) 9 -5 3 -2 4
    差分数组的前缀和 9 4 7 5 9
    • 显然:(a_i=sum_{j=1}^i b_j)

    • 差分数组能够高效的解决区间修改单点查询的问题。

      • 比如我们对区间[2,4]每个元素加上5,我们只需在差分数组:(b_2+=5,b_5-=5) ,然后求前缀和即可

        原数组 (a_i) 9 4 7 5 9
        差分数组 (b_i) 9 0 3 -2 -1
        差分数组的前缀和 9 9 12 10 9
    • 差分数组很容易算出前缀和:

      • (sum_x=sum_{i=1}^x a_i) = (sum_{i=1}^x sum_{j=1}^i b_i)
      • 容易发现,(b_1)使用了(x) 次,(b_2) 使用了 (x-1) 次,(b_3) 使用了 (x-2) 次……
      • 因此可得:(sum_x = sum_{i=1}^x (x-i+1)*b_i)

    2. 树上差分

    1. 相关概念
    • 树的以下两个性质:
      1. 任意两个节点之间有且只有一条路径。
      2. 根节点确定时,一个节点只有一个父亲节点。
    • 这两个性质都很容易证明。那么我们知道,如果假设我们要考虑的是从uv的路径,uvlcaa,那么很明显,如果路径中有一点u′已经被访问了,且u′≠a,那么u'的父亲也一定会被访问,这是根据以上性质可以推出的。
    • 所以,我们可以将路径拆分成两条链,u->aa->v
    • 树上差分有什么作用?举个例子,如果题目要求对树上的一段路径进行操作,并询问某个点或某条边被经过的次数,树上差分就可以派上用场了。这就是树上差分的基本操作。
    • 树上差分,就是利用差分的性质,对路径上的重要节点进行修改(而不是暴力全改),作为其差分数组的值,最后在求值时,利用dfsdfs遍历求出差分数组的前缀和,就可以达到降低复杂度的目的。
    2. 点差分
    • 如何给树上的一条链x ~ y每个点+1呢?(假设xy的祖先)

      • 我们还是需要回到差分数组来进行考虑。设原数组为a,差分数组为d

      • 假如给d[i]+1,其实就相当于给a[i]~a[n]每个元素+1(a[i]=sum_{j=1}^i d[j]))。

      • 那么,如果给树上的一个节点xd[x] +1,那它表示给哪一段区间的a +1呢?子树? 但子树中可能有很多条链啊,总不能去记录这次+1对应哪一条链吧。所以,这条链只能是x到根节点这条链!

      • 所以,d[x]+1,相当于a[root]~a[x]链上的每个节点都+1

      • 回到上面的问题——如何区间修改?

        • 先考虑简单的情况,当 xy 的祖先时。

        • 这个问题的答案应该就很明显了:d[fa[x]]−1,d[y]+1fa[x]表示x的父亲)

        • 相当于:a[y] ~ a[root]的链元素+1a[fa[x]] ~ a[root]链的元素−1,发现a[fa[x]] ~ a[root]这段区间其实加一完之后又减了一,所以他们并没有改变,于是就实现了a[y] ~ a[x]这段区间+1

        • 如果x不是y的祖先,假设a=lca(x,y)

          • 很容易想到把链x~y分成两个链,x~aa~y。即d[x]+1,d[y]+1

          • 但这样链a~root增加了2,我们让d[a]-1,此时fa[a]~root变成了-1,需要d[fa[a]]-1完美解决。

        • 要想知道点i被修改了多少,即为节点i所有子节点差分之和加上当前节点的差分。

    • 例题:松鼠的新家(luogu p3258)

      Description

      • 松鼠的新家是一棵树,前几天刚刚装修了新家,新家有n个房间,并且有 n-1 根树枝连接,每个房间都可以相互到达,且俩个房间之间的路线都是唯一的。天哪,他居然真的住在“树”上。
      • 松鼠想邀请小熊前来参观,并且还指定一份参观指南,他希望小熊能够按照他的指南顺序,先去 (a_1),再去 (a_2),……,最后到 (a_n),去参观新家。可是这样会导致重复走很多房间,懒惰的维尼不停地推辞。可是松鼠告诉他,每走到一个房间,他就可以从房间拿一块糖果吃。
      • 小熊是个馋家伙,立马就答应了。现在松鼠希望知道为了保证维尼有糖果吃,他需要在每一个房间各放至少多少个糖果。
      • 因为松鼠参观指南上的最后一个房间 (a_n) 是餐厅,餐厅里他准备了丰盛的大餐,所以当维尼在参观的最后到达餐厅时就不需要再拿糖果吃了。

      Input

      • 第一行一个正整数 n,表示房间个数

      • 第二行 n个正整数,依次描述 (a_1, a_2,cdots,a_n)

      • 接下来 n-1 行,每行两个正整数 x,y,表示标号 xy的两个房间之间有树枝相连。

      Output

      • 一共 n 行,第 i 行输出标号为 i 的房间至少需要放多少个糖果,才能让小熊有糖果吃。

      Input

      5
      1 4 5 3 2
      1 2
      2 4
      2 3
      4 5
      

      output

      1
      2
      1
      2
      1    
      

      Hint

      • 对于全部的数据,(2 le n le 3 imes 10^5, 1 le a_i le n)
    • 分析:

      • 小熊从房间u到达房间v,其路径上的每个房间都需要准备一颗糖,很容易想到树上差分。

      • 路径1~4,4~5中间的4号房间只需一个糖果,还有最后的房间2也不需要放糖果,所以需要减去1

      • Code

        #include <bits/stdc++.h>
        const int maxn=3e5+5;
        struct edge{
            int to,next;
        }e[2*maxn];
        int n,len;
        int a[maxn],head[maxn],deep[maxn],f[maxn][21],sum[maxn];
        void Insert(int u,int v){
            e[++len].to=v;e[len].next=head[u];head[u]=len;
        }
        void dfs(int u,int fa){
            deep[u]=deep[fa]+1;f[u][0]=fa;
            for(int i=1;(1<<i)<=deep[u];i++)
                f[u][i]=f[f[u][i-1]][i-1];
            for(int i=head[u];i;i=e[i].next){
                int v=e[i].to;
                if(v!=fa)dfs(v,u);
            }
        }
        int lca(int u,int v){
            if(deep[u]<deep[v])std::swap(u,v);
            int len=deep[u]-deep[v],k=0;
            while(len){
                if(len & 1)u=f[u][k];
                ++k;len>>=1;
            }    
            if(u==v) return u;
            for(int i=20;i>=0;i--){
                if(f[u][i]!=f[v][i])
                    u=f[u][i],v=f[v][i];            
            }    
            return f[u][0];
        }
        void search(int u){//求每个节点的差分和
            for(int i=head[u];i;i=e[i].next){
                int v=e[i].to;
                if(v==f[u][0]) continue;
                search(v);
                sum[u]+=sum[v];
            }
        }
        int main(){
            scanf("%d",&n);
            for(int i=1;i<=n;i++)
                scanf("%d",&a[i]);
            for(int i=1;i<=n-1;i++){
                int u,v;scanf("%d%d",&u,&v);
                Insert(u,v);Insert(v,u);
            }
            dfs(1,0);
            for(int i=1;i<=n-1;i++){
                int x=a[i],y=a[i+1],LCA=lca(x,y);        
                sum[x]++;
                sum[y]++;
                sum[LCA]--;
                sum[f[LCA][0]]--;
            }
            search(1);
            for(int i=2;i<=n;i++)//除了起点,其他的需要减去1
                sum[a[i]]--;
            for(int i=1;i<=n;i++)
                printf("%d
        ",sum[i]);
            return 0;
        }
        
    3. 边差分
    • cf[i]代表从ii的父亲这一条路径经过的次数。令a=lca(u,v) 因为关于边的差分,a表示a到其父亲的那条边,不在u~v路径中,所以cf[u]++,cf[v]++,cf[a]−=2

    • 例题:luogu P2680 运输计划

    • 题意:一棵树上有m条道路,可以使任意一条道路的权值变为0,怎样使长度最长的道路长度最小。

    • 分析:

      • 很容易想到,找到这m条路径中最长的一条,并把路径上最长的边归零,但改变一条边,可能多条路径的长度也要改变,而且不是一定要把最长的边归零就一定最好。

      • 我们可以二分最终答案,答案肯定在0~maxl(maxl表示最长路径长度)之间。我们需要修改的是现在路径长度超过mid的链中的边,那具体修改那条边呢?

      • 假设长度超过mid的路径有cnt条,显然我们修改的这条边必须同时影响着cnt条链,即这条边必须为这cnt条链都经过。否则修改无意义。

      • 所以我们二分答案,把经过了cnt次的边中长度最大的边归零,判断这cnt条边中最长的链-归零边<=mid

      • 判断一条边被经过了多少次,正好用树上差分的边差分。

      • Code

        #include <bits/stdc++.h>
        const int maxn=3e5+5;
        int f[maxn][22],head[maxn],edge[maxn],deep[maxn],dis[maxn];
        int st[maxn],ed[maxn],c[maxn],lca[maxn],len[maxn];
        int n,m,lene,maxl,ans,cnt,cutedge;
        struct Edge{int to,w,next;}e[maxn<<1];
        void Insert(int x,int y,int z){
            e[++lene].to=y;e[lene].w=z;e[lene].next=head[x];head[x]=lene;
        }
        void dfs(int u){//预处理
            deep[u]=deep[f[u][0]]+1;
            for(int i=1;(1<<i)<=deep[u];i++)
                f[u][i]=f[f[u][i-1]][i-1];
            for(int i=head[u];i;i=e[i].next){
                int v=e[i].to;
                if(v==f[u][0])continue;
                f[v][0]=u;
                edge[v]=e[i].w;//edge[v]记录v到父节点u的边长
                dis[v]=dis[u]+e[i].w;//求解v到根节点的距离
                dfs(v);
            }
        }
        int Lca(int u,int v){
            if(deep[u]<deep[v])std::swap(u,v);
            int len=deep[u]-deep[v],k=0;
            while(len){
                if(len & 1)u=f[u][k];
                ++k;len>>=1;
            }
            if(u==v)return u;
            for(int i=20;i>=0;i--)
                if(f[u][i]!=f[v][i])
                    u=f[u][i],v=f[v][i];
            return f[u][0];
        }
        int dist(int i){//求出第i条路径的长度
            return dis[st[i]]+dis[ed[i]]-2*dis[lca[i]];
        }
        int dfs2(int u){
            int tot=c[u];//计算u到其父亲这条边访问次数
            for(int i=head[u];i;i=e[i].next){
                int v=e[i].to;
                if(v==f[u][0])continue;
                tot+=dfs2(v);
            }
            if(tot==cnt)//计算经过cnt次最长的边
                cutedge=std::max(cutedge,edge[u]);
            return tot;
        }
        
        bool check(int mid){
            cnt=0;cutedge=0;//cnt记录超过mid的路径数,cutedge记录要删掉的边
            for(int i=1;i<=n;i++)c[i]=0;//差分数组清零 
            for(int i=1;i<=m;i++)
                if(len[i]>mid){//超出mid的区间进行差分,并计数
                    cnt++;c[st[i]]++;c[ed[i]]++;c[lca[i]]-=2;
                }
            if(cnt==0)return 1;//没有超过的
            dfs2(1);//求出被经过cnt次,且最长的边
            return maxl-cutedge<=mid;
        }
        int main(){
            scanf("%d%d",&n,&m);
            for(int i=1;i<n;i++){
                int u,v,w;scanf("%d%d%d",&u,&v,&w);
                Insert(u,v,w);Insert(v,u,w);
            }
            dfs(1);
            for(int i=1;i<=m;i++){
                scanf("%d%d",&st[i],&ed[i]);
                lca[i]=Lca(st[i],ed[i]);
                len[i]=dist(i);
                maxl=std::max(maxl,len[i]);
            }
            int l=0,r=maxl;//最短时间在[0,maxl]之间
            while(l<=r){
                int mid=(l+r)>>1;
                if(check(mid))ans=mid,r=mid-1;
                else l=mid+1;
            }
            printf("%d
        ",ans);
            return 0;
        }
        
  • 相关阅读:
    LeetCode 图解 | 200 .岛屿数量
    再不跳槽,应届毕业生拿的都比我多了!
    二叉树就是这么简单(面试常考点)
    我和面试官之间关于操作系统的一场对弈!写了很久,看完你就可以盘面试官了...
    非常详细的 Linux C/C++ 学习路线总结!助我拿下腾讯offer
    这款开源神器,让你能在 iPad 上随心所欲写代码!
    不瞒你说,我最近整理了一套面试题(Java岗)
    如何看待第三方百度云 Pandownload 作者被捕?
    【故事】为了避免产品经理和程序猿干架,我用大白话讲清楚了浏览器缓存原理...
    自己拥有一台服务器可以做哪些很酷的事情?搭建私有云网盘!
  • 原文地址:https://www.cnblogs.com/hbhszxyb/p/12854704.html
Copyright © 2020-2023  润新知