树上差分

树上差分

点差分

  • cf[maxn] cf[i]记录点i被经过了多少次

  • 当s->t最短路径上每个点都被经过一次,则:

    cf[s]++;
    cf[t]++;
    cf[lca(s,t)]--;
    cf[ father[lca(s,t)] ]--;
    

边差分

  • cf[maxn] 记录点i到其父节点的边被经过了多少次

  • 当s->t最短路径上每个边都被经过一次,则:

    cf[s]++;
    cf[t]++;
    cf[lca(s,t)] -= 2;
    

核心算法

  • LCA,可以用倍增法,在线求LCA(s,t),

    int depth[maxn];//记录每个结点的深度 根节点深度为0
    int fa[maxn][20];//fa[i][j]表示i结点向上爬 2^j是哪个结点
    //预处理出每个结点的深度和其直接父节点
    void dfs(int u, int pre, int d){
        fa[u][0] = pre;//向上一个当然是直接父节点 pre
        depth[u] = d;//深度
        for(int i=head[u]; i; i=edge[i].nxt){
            int v = edge[i].v;
            if(v != pre){
                //求点到树根的距离
                dist[v] = dist[u] + edge[i].w;
                dfs(v , u, d+1);
            }
        }
    }
    //倍增预处理出fa数组
    void init(){
        //有点区间DP的意思
        for(int j=0; (1<<(j+1))<n; j++){
            for(int i=1; i<=n; i++){
                if(fa[i][j] < 0) fa[i][j+1] = -1;
                else fa[i][j+1] = fa[fa[i][j]][j];
            }
        }
    }
    //给定俩个结点在线求其LCA
    int LCA(int u, int v){
        //保证v是较深的点
        if(depth[u] > depth[v]) swap(u , v);
        int temp = depth[v] - depth[u];//深度差
        //先把v调到与u等高处
        for(int i=0; (1<<i)<=temp; i++){
            if((1<<i) & temp) v = fa[v][i];
        }
        if(u==v) return u;
        //然后两个人比翼双飞
        for(int i=(int)(log(1.0*n)/log(2.0)); i >= 0; i--){
            if(fa[u][i] != fa[v][i]){
                u = fa[u][i];
                v = fa[v][i];
            }
        }
        return fa[u][0];
    
    }
    
  • DFS遍历树得到差分数组,得到cf[i] (这个顶点或者边被经过了多少次)

    //遍历差分 
    void dfs2(int u, int f){
        for(int i=head[u]; i; i=edge[i].nxt) {
            int v = edge[i].v;
            if(v == f) continue;
            dfs2(v , u);
            cf[u] += cf[v];
        }
    }
    

例题

  • 洛谷 P3128 [USACO15DEC]最大流Max Flow

  • 题意:点差分板子题

    //LCA 倍增 大家一起跳跳跳
    #include<iostream>
    #include<queue>
    #include<list>
    #include<vector>
    #include<cstring>
    #include<set>
    #include<stack>
    #include<map>
    #include<cmath>
    #include<algorithm>
    #include<string>
    #include<stdio.h>
    using namespace std;
    typedef long long ll;
    #define MS(x,i) memset(x,i,sizeof(x))
    #define rep(i,s,e) for(int i=s; i<=e; i++)
    #define sc(a) scanf("%d",&a)
    #define scl(a) scanf("%lld",&a)
    #define sc2(a,b) scanf("%d %d", &a, &b)
    #define debug printf("debug......
    ");
    #define pfd(x) printf("%d
    ",x)
    #define pfl(x) printf("%lld
    ",x)
    const double eps=1e-8;
    const double PI = acos(-1.0);
    const int inf = 0x3f3f3f3f;
    const ll INF = 0x7fffffff;
    const int maxn = 5e4+10;
    int dx[4] = {0, 0, 1, -1};
    int dy[4]  = {1, -1, 0 , 0};
    
    int t,n,k;
    vector<int> G[maxn];
    int s,e,lca;
    int depth[maxn];//记录每个结点的深度 根节点深度为0
    int fa[maxn][20];//fa[i][j]表示i结点向上爬 2^j是哪个结点
    
    int ans = -1;//最终答案 
    int value[maxn];//每个结点的点权 
    
    
    //预处理出每个结点的深度和其直接父节点
    void dfs(int u, int pre, int d){
        fa[u][0] = pre;//向上一个当然是直接父节点 pre
        depth[u] = d;//深度
        for(int i=0; i<G[u].size(); i++){
            int v = G[u][i];
            if(v != pre){
                dfs(v , u, d+1);
            }
        }
    }
    //倍增预处理出fa数组
    void init(){
        //有点区间DP的意思
        for(int j=0; (1<<(j+1))<n; j++){
            for(int i=1; i<=n; i++){
                if(fa[i][j] < 0) fa[i][j+1] = -1;
                else fa[i][j+1] = fa[fa[i][j]][j];
            }
        }
    }
    //给定俩个结点在线求其LCA
    int LCA(int u, int v){
        //保证v是较深的点
        if(depth[u] > depth[v]) swap(u , v);
        int temp = depth[v] - depth[u];//深度差
        //先把v调到与u等高处
        for(int i=0; (1<<i)<=temp; i++){
            if((1<<i) & temp) v = fa[v][i];
        }
        if(u==v) return u;
        //然后两个人比翼双飞
        for(int i=(int)(log(1.0*n)/log(2.0)); i >= 0; i--){
            if(fa[u][i] != fa[v][i]){
                u = fa[u][i];
                v = fa[v][i];
            }
        }
        return fa[u][0];
    
    }
    
    //遍历差分 
    void dfs2(int u, int f){
        for(int i=0; i<G[u].size(); i++){
            int v = G[u][i];
            if(v == f) continue;
            dfs2(v , u);
            value[u] += value[v];
        }
        if(value[u] > ans) ans = value[u];
    }
    
    
    int main(){
        int u,v;
        while(sc2(n,k) != EOF){
            MS(value , 0);
            rep(i , 1, n) G[i].clear();
            rep(i , 1, n-1){
                sc2(u,v);
                G[u].push_back(v);
                G[v].push_back(u);
            }
            dfs(1 , 0 , 1) ;//初始化LCA 
            init(); 
            for(int i=1; i<=k; i++){
                sc2(u , v);
                value[u]++;
                value[v]++;
                int lca = LCA(u,v);
                value[lca]--;
                value[fa[lca][0]]--;
            }
            
            dfs2(1 , 0);
            pfd(ans);
        }
        return 0;
    }
    
    
  • 洛谷 P2680 运输计划 (边差分)

  • 题意: 给定一棵N个结点的带权树,以及K条路径,现在可以使某一个边的权值变为0,使某条路径的总长度变小,问这些路径的最大值的最小值是多少?

  • 思路:

    • 先找出最长路径mx和最长边me,那么答案ans必然处于[mx-me , mx]之间

    • 二分答案ans,什么时候ans是合法的呢?一是ans>=mx,必然合法;而是若有ct条路径均大于ans,想要使得减去某一条边后这ct条路径各自长度都小于ans,则必然是存在一条长度为len的公共边,且len >= mx - ans(这ct条路径中的最长路径减去这条边后路径长不超过ans)

    • 什么时候它才是公共边?那就是对这ct条边进行差分,进行cf操作后跑dfs,然后看是否存在一条边cf[i] = ct && len[i] >= mx - ans

      //LCA 倍增 大家一起跳跳跳
      #include<iostream>
      #include<queue>
      #include<list>
      #include<vector>
      #include<cstring>
      #include<set>
      #include<stack>
      #include<map>
      #include<cmath>
      #include<algorithm>
      #include<string>
      #include<stdio.h>
      using namespace std;
      typedef long long ll;
      #define MS(x,i) memset(x,i,sizeof(x))
      #define rep(i,s,e) for(int i=s; i<=e; i++)
      #define sc(a) scanf("%d",&a)
      #define scl(a) scanf("%lld",&a)
      #define sc2(a,b) scanf("%d %d", &a, &b)
      #define debug printf("debug......
      ");
      #define pfd(x) printf("%d
      ",x)
      #define pfl(x) printf("%lld
      ",x)
      const double eps=1e-8;
      const double PI = acos(-1.0);
      const int inf = 0x3f3f3f3f;
      const ll INF = 0x7fffffff;
      const int maxn = 3e5+10;
      int dx[4] = {0, 0, 1, -1};
      int dy[4]  = {1, -1, 0 , 0};
      
      int t,n,k;
      
      int head[maxn];//head数组 
      int cnt;//边数 
      int dist[maxn];//点i到根节点的路径长度 
      int cf[maxn];//cf[i]表示i到father[i]这条边被经过了多少次
      int dis[maxn];//dis[i]表示第i个路径的总长度 
      int mx;//最长路径长度 
      int me;//最长边 
      int ans;//最终结果 
      struct node{
          int v;
          int nxt;
          int w;
      }edge[maxn<<1];
      
      int lca[maxn]; //存第i个路径两端点的LCA 
      int depth[maxn];//记录每个结点的深度 根节点深度为0
      int fa[maxn][20];//fa[i][j]表示i结点向上爬 2^j是哪个结点
      int l[maxn],r[maxn];//存路径端点 
      
      void Init(){
          cnt = 1;
          mx = me = -1; 
          rep(i , 0 , n){
              head[i] = 0;
              dis[i] = 0; 
          }
      }
      void addEdge(int u, int v, int w){
          edge[cnt].v = v;
          edge[cnt].w = w;
          edge[cnt].nxt = head[u];
          head[u] = cnt++;
      }
      //预处理出每个结点的深度和其直接父节点
      void dfs(int u, int pre, int d){
          fa[u][0] = pre;//向上一个当然是直接父节点 pre
          depth[u] = d;//深度
          for(int i=head[u]; i; i=edge[i].nxt){
              int v = edge[i].v;
              if(v != pre){
                  dist[v] = dist[u] + edge[i].w;
                  dfs(v , u, d+1);
              }
          }
      }
      //倍增预处理出fa数组
      void init(){
          //有点区间DP的意思
          for(int j=0; (1<<(j+1))<n; j++){
              for(int i=1; i<=n; i++){
                  if(fa[i][j] < 0) fa[i][j+1] = -1;
                  else fa[i][j+1] = fa[fa[i][j]][j];
              }
          }
      }
      //给定俩个结点在线求其LCA
      int LCA(int u, int v){
          //保证v是较深的点
          if(depth[u] > depth[v]) swap(u , v);
          int temp = depth[v] - depth[u];//深度差
          //先把v调到与u等高处
          for(int i=0; (1<<i)<=temp; i++){
              if((1<<i) & temp) v = fa[v][i];
          }
          if(u==v) return u;
          //然后两个人比翼双飞
          for(int i=(int)(log(1.0*n)/log(2.0)); i >= 0; i--){
              if(fa[u][i] != fa[v][i]){
                  u = fa[u][i];
                  v = fa[v][i];
              }
          }
          return fa[u][0];
      
      }
      
      //遍历差分 
      void dfs2(int u, int f){
          for(int i=head[u]; i; i=edge[i].nxt) {
              int v = edge[i].v;
              if(v == f) continue;
              dfs2(v , u);
              cf[u] += cf[v];
          }
      }
      
      //这条边必须出现了ct次, 并且它的长度不小于len 
      bool dfsCheck(int u, int f, int ct, int len){
          for(int i=head[u]; i ; i=edge[i].nxt){
              int v = edge[i].v;
              if(v == f) continue;
              if(cf[v] == ct && edge[i].w >= len) return 1;
              if(dfsCheck(v , u, ct , len)) return 1;
          } 
          return 0;//根节点返回还找不到,那就不存在这样的公共边 
      }
      
      bool judge(int ans){
          int ct = 0;//路径长度超过ans的路径个数
          MS(cf , 0);//把各点经过的次数清零 
          if(ans >= mx) return 1;//如果当前答案大于等于最长路径 肯定行
          rep(i , 1, k) { //寻找哪些路径长度大于当前ans,找到,求是否存在一条比较长的公共边使得减去后mx小于等于ans 
              if(dis[i] > ans){
                  ct++;
                  cf[l[i]]++;
                  cf[r[i]]++;
                  cf[lca[i]] -= 2;
              }
          }
          dfs2(1 , 0); //计算在这些超过ans长度的路径中,每条边被经过的次数 
          return dfsCheck(1 , 0, ct , mx - ans); 
      } 
      
      int main(){
          int u,v,w;
          while(sc2(n,k) != EOF){
              Init();
              //建树 
              rep(i , 1, n-1){
                  sc2(u,v);
                  sc(w);
                  addEdge(u,v,w);
                  addEdge(v,u,w);
                  me = max(me , w); 
              }
      
              dfs(1 , 0 , 1) ;//初始化LCA 
              init(); //初始化LCA
              
              //读取路径 
              rep(i , 1, k){
                  sc2(u , v);
                  l[i] = u;
                  r[i] = v; 
                  lca[i] = LCA(u,v);
                  dis[i] = dist[u] + dist[v] - 2*dist[lca[i]] ;//求该路径的长度 
                  mx = max(mx , dis[i]);//求出最长路径 
              }
              
              int lo = mx - me;
              int hi = mx + 1;
              while(lo <= hi){
                  int mid = (lo + hi) / 2;
                  if(judge(mid)){
                      hi = mid - 1;
                      ans = mid;
                  } 
                  else{
                      lo = mid + 1;
                  }
              }
              
              pfd(ans);
          }
          return 0;
      }
      
原文地址:https://www.cnblogs.com/czsharecode/p/10933185.html