树上差分

差分

差分是前缀和的逆运算。 d 1... n d_{1...n} d1...n是差分数组, a 1... n a_{1...n} a1...n是原数组。关系如下式。
d i = a i − a i − 1 d_i=a_i-a_{i-1} di=aiai1
则通过 d 1... n d_{1...n} d1...n数组求 a i a_i ai,为求 d 1... n d_{1...n} d1...n数组的前缀和。
a i = ∑ j = 1 i d j a_i = \sum_{j=1}^id_j ai=j=1idj
通过 d 1... n d_{1...n} d1...n数组求前缀和为
s u m i = ∑ i = 1 n ∑ j = 1 i d i = ∑ i = 1 n ( n − i + 1 ) d i sum_i=\sum_{i=1}^n{\sum_{j=1}^id_i}=\sum_{i=1}^n(n-i+1)d_i sumi=i=1nj=1idi=i=1n(ni+1)di

应用

P1083 借教室
求第一个不能减的区间。
二分借教室操作数,用差分数组 O ( n ) O(n) O(n)借教室,总时间复杂度为 O ( n log ⁡ m ) O(n\log m) O(nlogm)

#include 
#define debug(__x) cout<<#__x<<"="<<__x<
#define endl '\n'
using namespace std;
using ll = long long;
const int maxn = 1e6+5;
struct Lease{
    ll d;
    int s,t;
    Lease(ll x,int y,int z):d(x),s(y),t(z) {}
};
vector<Lease> range;
ll a[maxn];    // 差分数组
int n,m;
void sub(int l,int r){  // 借教室操作
    for(int i=l;i<=r;++i){
        a[range[i].s]-=range[i].d;
        a[range[i].t+1]+=range[i].d;
    }
}
void add(int l,int r){    // 撤销借教室操作
    for(int i=l;i<=r;++i){
        a[range[i].s]+=range[i].d;
        a[range[i].t+1]-=range[i].d;
    }
}
bool judge(){  // 判断是否有一天教室数不够借
    int now = 0;
    for(int i=1;i<=n;++i){
        now += a[i];
        if(now<0)return false;
    }
    return true;
}
int main(){
    cin.tie(0),cout.tie(0);
    ios::sync_with_stdio(false);
    cin>>n>>m;
    for(int now,last=0,i=1;i<=n;++i){
        cin>>now;
        a[i] = now - last;
        last = now;
    }
    for(int d,s,t,i=0;i<m;++i){
        cin>>d>>s>>t;
        range.emplace_back(d,s,t);
    }
    sub(0,m-1);
    if(judge()){
        cout<<0<<endl;
        return 0;
    }
    cout<<-1<<endl;
    int l=0,r=m-1,mid;
    add(l,r);
    while(l < r){
        mid = l+r>>1;
        sub(l,mid);
       // debug(l);debug(r);
        if(judge()){
            l = mid + 1;
        }else{
            r = mid;
            add(l,mid);
        }
    }
    cout<<l+1<<endl;
}

树上差分

就是在树上进行差分操作,实现功能与差分一样,可以 O ( 1 ) O(1) O(1)实现区间修改。分为点差分和边差分。
点差分
cnt[u]+=x可理解为u到树跟的路径上所有节点的权重加x。在把(u,v)路径上的点权加x的操作如下。

cnt[u] += x;
cnt[v] += x;
cnt[lca] -= x;
cnt[fa[lca]] -= x;

用差分数组求点权
v a l [ u ] = ∑ v ∈ S u b T r e e r o o t = u c n t [ v ] val[u]=\sum_{v\in SubTree_{root=u}}cnt[v] val[u]=vSubTreeroot=ucnt[v]
边差分
把每个点连到它的父节点的边权放在该点上。原理和点分治一样。在把(u,v)路径上的边权加x的操作如下。

cnt[u] += x;
cnt[v] += x;
cnt[lca] -= 2*x;

模板

在一棵树上,在给定点对的路径上的点权加1。求最大点权。树上差分最适合修改多查询少的题。这题只用1次查询。

#include 
#define debug(__x) cout<<#__x<<"="<<__x<
#define endl '\n'
using namespace std;
using ll = long long;
const int maxn = 5e4+5;
vector<int> G[maxn];
int f[maxn][15];
int cnt[maxn];
int dep[maxn];
int ans=0;
void dfs(int u,int fa){
    f[u][0] = fa;
    dep[u] = dep[fa] + 1;
    for(int i=1;i<15;++i)
        f[u][i] = f[f[u][i-1]][i-1];
    for(int son : G[u]){
        if(son == fa)continue;
        dfs(son,u);
    }
}
int lca(int x,int y){
    if(dep[x] < dep[y]) swap(x,y);
    int t = dep[x] - dep[y];
    for(int i=0;i<15;++i)
        if(t&(1<<i))
            x = f[x][i];
    if(x==y) return x;
    for(int i=14;i>=0;--i){
        if(f[x][i]!=f[y][i]){
            x=f[x][i];
            y=f[y][i];
        }
    }
    return f[x][0];
}
void sum(int u,int fa){
    for(int son : G[u]){
        if(son == fa)continue;
        sum(son,u);
        cnt[u] += cnt[son];
    }
}
int main(){
    cin.tie(0),cout.tie(0);
    ios::sync_with_stdio(false);
    int n,k;
    cin>>n>>k;
    for(int u,v,i=1;i<n;++i){
        cin>>u>>v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs(1,0);
    while(k--){
        int u,v;
        cin>>u>>v;
        int ac = lca(u,v);
        ++cnt[u];
        ++cnt[v];
        --cnt[ac];
        --cnt[f[ac][0]];
    }
    sum(1,0);   // 把差分数组转换为每个点的点权
    for(int i=1;i<=n;++i){
        if(cnt[i]>ans) ans = cnt[i];
    }
    cout<<ans<<endl;
}

你可能感兴趣的:(ACM训练,acm竞赛)