Living-Dream 系列笔记 第82期

Posted on 2024-10-13 13:37  _XOFqwq  阅读(2)  评论(0编辑  收藏  举报

树上差分

概述:擅长在树上一条路径上统计边或者点的信息。

下文令差分数组为 \(d_i\)\(lca\) 为路径两端点的 LCA,\(fa_i\)\(i\) 的父亲。

  • 按边的差分

    \(a\)\(b\) 的路径上的边权加 \(val\)

\[d_a+val \to d_a\\ d_b+val \to d_b\\ d_{lca} - 2 \times val \to d_{lca} \]

  • 按点的差分:

    \(a\)\(b\) 的路径上的点权加 \(val\)

\[d_a+val \to d_a\\ d_{lca}-val \to d_{lca}\\ d_b+val \to d_b\\ d_{fa_{lca}}-val \to d_{fa_{lca}} \]

CF191C

一眼树剖。

容易发现这是一道绿题,因此不需要使用如此复杂的数据结构。

又因为这是统计边的信息,考虑树上差分。

其实这就是个板子,相当于每次旅行将 \(a \to b\) 路径上的边边权均 \(+1\)。直接按上文公式做即可。

注意因为差分数组是挂在点上的,所以需要维护一个边和点之间的映射。

code
//
//  CF191C.cpp
//  
//
//  Created by _XOFqwq on 2024/10/11.
//

#include <bits/stdc++.h>
using namespace std;

const int N=1e5+5;
int n,k;
int id[N],dp[N][31],diff[N],dep[N];
vector<pair<int,int> > G[N];

void init_lca(int cur,int fa){
    dep[cur]=dep[fa]+1;
    dp[cur][0]=fa;
    for (int i=1; (1<<i)<=dep[cur]; i++) {
        dp[cur][i]=dp[dp[cur][i-1]][i-1];
    }
    for (auto [nxt,i] : G[cur]) {
        if (nxt==fa) {
            continue;
        }
        id[i]=nxt;
        init_lca(nxt,cur);
    }
}
int get_lca(int x,int y){
    if (dep[x]<dep[y]) {
        swap(x,y);
    }
    for (int i=21; i>=0; i--) {
        if (dep[dp[x][i]]>=dep[y]) {
            x=dp[x][i];
        }
    }
    if (x==y) {
        return x;
    }
    for (int i=21; i>=0; i--) {
        if (dp[x][i]!=dp[y][i]) {
            x=dp[x][i],y=dp[y][i];
        }
    }
    return dp[x][0];
}
void dfs(int cur,int fa){
    for (auto [nxt,i] : G[cur]) {
        if (nxt==fa) {
            continue;
        }
        dfs(nxt,cur);
        diff[cur]+=diff[nxt];
    }
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for (int i=1,u,v; i<n; i++) {
        cin>>u>>v;
        G[u].push_back({v,i});
        G[v].push_back({u,i});
    }
    init_lca(1,0);
    cin>>k;
    for (int i=1,x,y; i<=k; i++) {
        cin>>x>>y;
        int lca=get_lca(x,y);
        diff[x]++;
        diff[y]++;
        diff[lca]-=2;
    }
    dfs(1,0);
    for (int i=1; i<n; i++) {
        cout<<diff[id[i]]<<' ';
    }
    return 0;
}

P2680

还是很巧妙的一道题。

最短时间取决于 \(m\) 条路径的最大边权和,我们要最小化这个东西,直接考虑二分。

对于 \(m\) 条路径,只有边权和 \(> mid\) 的才有必要使用虫洞,设它们的数量为 \(tot\)

我们要将这 \(tot\) 条路径的边权和全部变得 \(\le mid\),因此只能选所有路径的公共边变成虫洞,并且还得选边权最大的。

找出公共边等价于找出被覆盖 \(tot\) 次的边,直接树上差分即可。

然后这题就做完了。需要适当卡常,详见代码。

code
//
//  P2680.cpp
//
//
//  Created by _XOFqwq on 2024/10/11.
//

#include <bits/stdc++.h>
using namespace std;

const int N=3e5+5;
int n,m,tot,w[N];
int ddep[N],dep[N],dp[N][31],id[N],to[N],diff[N];
struct PATH{
    int u,v,dis,lca;
}path[N];
struct EDGE{
    int v,w,e;
};
vector<EDGE> G[N];

namespace fastIO{
    #define BUF_SIZE 1000000
    bool IOerror=0;
    inline char nc() {
        static char buf[BUF_SIZE],*p1=buf+BUF_SIZE,*pend=buf+BUF_SIZE;
        if(p1==pend){
            p1=buf;
            pend=buf+fread(buf,1,BUF_SIZE,stdin);
            if(pend==p1){
                IOerror=1;
                return -1;
            }
        }
        return *p1++;
      }
      inline bool blank(char ch) {
          return ch==' '||ch=='\n'||ch=='\r'||ch=='\t';
      }
      inline void read(int &x) {
          char ch;
          while(blank(ch=nc()));
          if(IOerror)    return;
          for(x=ch-'0';(ch=nc())>='0'&&ch<='9';x=(x<<3)+(x<<1)+ch-'0');
      }
      #undef BUF_SIZE
};
using namespace fastIO; //卡常 1

bool cmp(PATH x,PATH y){
    return x.dis>y.dis;
}
void init_lca(int cur,int fa){
    dep[cur]=dep[fa]+1;
    dp[cur][0]=fa;
    for (int i=1; (1<<i)<=dep[cur]; i++) {
        dp[cur][i]=dp[dp[cur][i-1]][i-1];
    }
    for (auto [v,w,e] : G[cur]) {
        if (v==fa) {
            continue;
        }
        id[e]=v;
        to[v]=e;
        ddep[v]=ddep[cur]+w;
        init_lca(v,cur);
    }
}
int get_lca(int x,int y){
    if (dep[x]<dep[y]) {
        swap(x,y);
    }
    for (int i=21; i>=0; i--) {
        if (dep[dp[x][i]]>=dep[y]) {
            x=dp[x][i];
        }
    }
    if (x==y) {
        return x;
    }
    for (int i=21; i>=0; i--) {
        if (dp[x][i]!=dp[y][i]) {
            x=dp[x][i],y=dp[y][i];
        }
    }
    return dp[x][0];
}
void dfs(int cur,int fa){
    for (auto [v,w,e] : G[cur]) {
        if (v==fa) {
            continue;
        }
        dfs(v,cur);
        diff[cur]+=diff[v];
    }
}
bool check(int x){
    tot=0;
    memset(diff,0,sizeof diff);
    for (int i=1; i<=m&&path[i].dis>x; i++,tot++) {
        diff[path[i].u]++;
        diff[path[i].v]++;
        diff[path[i].lca]-=2;
    }
    if (!tot) {
        return 1;
    }
    dfs(1,0);
    int mx=-1e9;
    for (int i=1; i<n; i++) {
        if (diff[id[i]]==tot) {
            mx=max(mx,w[i]);
        }
    }
    return path[1].dis-mx<=x;
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    read(n),read(m);
    int mxw=-1e9;
    for (int i=1,u,v; i<n; i++) {
        read(u),read(v),read(w[i]);
        G[u].push_back({v,w[i],i});
        G[v].push_back({u,w[i],i});
        mxw=max(mxw,w[i]);
    }
    init_lca(1,0);
    for (int i=1,u,v; i<=m; i++) {
        read(path[i].u),read(path[i].v);
        path[i].lca=get_lca(path[i].u,path[i].v);
        path[i].dis=ddep[path[i].u]+ddep[path[i].v]-2*ddep[path[i].lca];
    }
    sort(path+1,path+m+1,cmp);
    int l=path[1].dis-mxw-1,r=path[1].dis+1; //卡常 2
    while (l+1<r) {
        int mid=(l+r)>>1;
        if (check(mid)) {
            r=mid;
        } else {
            l=mid;
        }
    }
    cout<<r;
    return 0;
}

P8201

更加巧妙的一道题。

首先推式子,

\(dep_i\) 表示从根节点到 \(i\) 的路径上的点权异或和:

\[dis_{t,a} \oplus dis_{t,b}\\ =dep_a \oplus dep_b \oplus w_{lca} \oplus w_t\\ =k \]

\(dep_a \oplus dep_b\)\(lca\) 的权值抵消了,要异或回来;\(t\) 本身应当被抵消,因此要再异或一次以抵消掉它)

也即:

\[dep_a \oplus dep_b \oplus w_{lca} \oplus k=w_t \]

问题转化为有多少个点 \(t\) 满足上式。

我们将询问离线,然后一遍 dfs 统计出从根节点到每个点的路径上当前点权出现次数,询问时进行求一下 \(a \to son_{lca}\)\(b \to lca\) 这两段的出现次数(前缀和),拼起来判断是否 \(>0\) 即可。

(对于树上信息问题,先推式子,接着求出从根节点出发的信息,然后利用前缀和 / 差分之类的东西维护)

code
//
//  P8201.cpp
//  
//
//  Created by _XOFqwq on 2024/10/13.
//

#include <bits/stdc++.h>
using namespace std;

const int N=5e5+5;
int n,m;
int a[N],b[N],lca[N],dep[N],dp[N][31];
long long k[N],w[N],dis[N];
vector<int> qry[N],G[N];
unordered_map<int,long long> cnt,ans[N];

void init_LCA(int cur,int fa){
    dis[cur]=dis[fa]^w[cur];
    dep[cur]=dep[fa]+1;
    dp[cur][0]=fa;
    for (int i=1; (1<<i)<=dep[cur]; i++) {
        dp[cur][i]=dp[dp[cur][i-1]][i-1];
    }
    for (int i : G[cur]) {
        if (i==fa) {
            continue;
        }
        init_LCA(i,cur);
    }
}
int get_LCA(int x,int y){
    if (dep[x]<dep[y]) {
        swap(x,y);
    }
    for (int i=21; i>=0; i--) {
        if (dep[dp[x][i]]>=dep[y]) {
            x=dp[x][i];
        }
    }
    if (x==y) {
        return x;
    }
    for (int i=21; i>=0; i--) {
        if (dp[x][i]!=dp[y][i]) {
            x=dp[x][i],y=dp[y][i];
        }
    }
    return dp[x][0];
}
void dfs(int cur,int fa){
    cnt[w[cur]]++;
    for (int i : qry[cur]) {
        ans[cur][i]=cnt[i];
    }
    for (int i : G[cur]) {
        if (i==fa) {
            continue;
        }
        dfs(i,cur);
    }
    cnt[w[cur]]--;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m;
    for (int i=1; i<=n; i++) {
        cin>>w[i];
    }
    for (int i=1,u,v; i<n; i++) {
        cin>>u>>v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    init_LCA(1,0);
    for (int i=1; i<=m; i++) {
        cin>>a[i]>>b[i]>>k[i];
        lca[i]=get_LCA(a[i],b[i]);
        long long val=dis[a[i]]^dis[b[i]]^w[lca[i]]^k[i];
        qry[a[i]].push_back(val);
        qry[b[i]].push_back(val);
        qry[lca[i]].push_back(val);
        qry[dp[lca[i]][0]].push_back(val);
    }
    dfs(1,0);
    for (int i=1; i<=m; i++) {
        long long val=dis[a[i]]^dis[b[i]]^w[lca[i]]^k[i];
        cout<<(ans[a[i]][val]-ans[lca[i]][val]+ans[b[i]][val]-ans[dp[lca[i]][0]][val]?"yEs\n":"nO\n");
    }
    return 0;
}