P4211 [LNOI2014]LCA(离线 + 在线 做法)

这篇博客详细介绍了如何利用树剖和线段树解决求解最近公共祖先(LCA)问题。通过离线和在线两种方法,分别给出O(nlog^2n)和O(nlog^2n)复杂度的解决方案,通过拆分询问、树状数组维护区间和以及主席树更新查询等技巧,实现了高效求解。
摘要由CSDN通过智能技术生成

P4211 [LNOI2014]LCA

有一棵根节点为 1 1 1树,有 m m m次询问,每次给定 l , r , z l, r, z l,r,z,输出 ∑ i = l r d e p [ l c a ( i , z ) ] \sum\limits_{i = l} ^{r} dep[lca(i, z)] i=lrdep[lca(i,z)]

乍一看这题好像无从下手,仔细想想 l c a ( i , z ) lca(i, z) lca(i,z)有何性质,不难发现 l c a ( i , z ) lca(i, z) lca(i,z)一定是在 1 − > z 1->z 1>z的路径上的,

那么我们的答案求解就可以转换为每次对 i − > 1 i->1 i>1路径上的点加一,求 z − > 1 z->1 z>1上路径上的点的和即为我们要求的答案。

考虑如何优化这一过程:

离线求解:

将每个询问 ( l , r , z ) (l, r, z) (l,r,z)拆分成 ( 1 , l − 1 , z ) , ( 1 , r , z ) (1, l - 1, z), (1, r, z) (1,l1,z),(1,r,z),那么答案就为 ( 1 , r , z ) − ( 1 , l − 1 , z ) (1, r, z) - (1, l - 1, z) (1,r,z)(1,l1,z)了,

我们将所有拆分后询问按照 r r r排个序,从小到大每次插入点 i i i,如果当前插入的点 i i i等于每个询问的 r r r则统计一次答案,

两点之间点权的信息更新可以考虑树剖,注意答案的加减,最后输出答案即可,整体复杂度 O ( n log ⁡ n log ⁡ n ) O(n \log n \log n) O(nlognlogn)

#include <bits/stdc++.h>
#define mid (l + r >> 1)
#define lson rt << 1, l, mid
#define rson rt << 1 | 1, mid + 1, r
#define ls rt << 1
#define rs rt << 1 | 1

using namespace std;

const int N = 1e5 + 10, mod = 201314;

int head[N], to[N], nex[N], cnt = 1;

int fa[N], dep[N], sz[N], son[N], rk[N], id[N], top[N], tot;

int sum[N << 2], lazy[N << 2], ans[N], n, m;

struct Res {
  int u, v, id;

  bool operator < (const Res &t) {
    return u < t.u;
  }
}a[N];

void add(int x, int y) {
  to[cnt] = y;
  nex[cnt] = head[x];
  head[x] = cnt++;
}

void dfs1(int rt, int f) {
  fa[rt] = f, dep[rt] = dep[f] + 1, sz[rt] = 1;
  for (int i = head[rt]; i; i = nex[i]) {
    if (to[i] == f) {
      continue;
    }
    dfs1(to[i], rt);
    sz[rt] += sz[to[i]];
    if (!son[rt] || sz[son[rt]] < sz[to[i]]) {
      son[rt] = to[i];
    }
  }
}

void dfs2(int rt, int tp) {
  top[rt] = tp, rk[++tot] = rt, id[rt] = tot;
  if (!son[rt]) {
    return ;
  }
  dfs2(son[rt], tp);
  for (int i = head[rt]; i; i = nex[i]) {
    if (to[i] == fa[rt] || to[i] == son[rt]) {
      continue;
    }
    dfs2(to[i], to[i]);
  }
}

void push_up(int rt) {
  sum[rt] = (sum[ls] + sum[rs]) % mod;
}

void push_down(int rt, int l, int r) {
  if (lazy[rt]) {
    lazy[ls] = (lazy[ls] + lazy[rt]) % mod, lazy[rs] = (lazy[rs] + lazy[rt]) % mod;
    sum[ls] = (sum[ls] + 1ll * (mid - l + 1) * lazy[rt]) % mod;
    sum[rs] = (sum[rs] + 1ll * (r - mid) * lazy[rt]) % mod;
    lazy[rt] = 0;
  }
}

void update(int rt, int l, int r, int L, int R, int v) {
  if (l >= L && r <= R) {
    lazy[rt] = (lazy[rt] + 1) % mod;
    sum[rt] = (sum[rt] + r - l + 1) % mod;
    return ;
  }
  push_down(rt, l, r);
  if (L <= mid) {
    update(lson, L, R, v);
  }
  if (R > mid) {
    update(rson, L, R, v);
  }
  push_up(rt);
}

int query(int rt, int l, int r, int L, int R) {
  if (l >= L && r <= R) {
    return sum[rt];
  }
  push_down(rt, l, r);
  int ans = 0;
  if (L <= mid) {
    ans = query(lson, L, R);
  }
  if (R > mid) {
    ans = (ans + query(rson, L, R)) % mod;
  }
  return ans;
}

void update(int rt) {
  while (rt) {
    update(1, 1, n, id[top[rt]], id[rt], 1);
    rt = fa[top[rt]];
  }
}

int query(int rt) {
  int ans = 0;
  while (rt) {
    ans = (ans + query(1, 1, n, id[top[rt]], id[rt])) % mod;
    rt = fa[top[rt]];
  }
  return ans;
}

int main() {
  // freopen("in.txt", "r", stdin);
  // freopen("out.txt", "w", stdout);
  // ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
  scanf("%d %d", &n, &m);
  for (int i = 2, fa; i <= n; i++) {
    scanf("%d", &fa);
    add(fa + 1, i);
  }
  dfs1(1, 0);
  dfs2(1, 1);
  for (int i = 1, l, r, u; i <= m; i++) {
    scanf("%d %d %d", &l, &r, &u);
    l++, r++, u++;
    a[i * 2 - 1] = {l - 1, u, i * 2 - 1};
    a[i * 2] = {r, u, i * 2};
  }
  sort(a + 1, a + 1 + 2 * m);
  int cur = 1;
  while (cur <= 2 * m && a[cur].u == 0) {
    cur++;    
  }
  for (int i = 1; i <= n; i++) {
    update(i);
    while (cur <= 2 * m && a[cur].u == i) {
      int res = query(a[cur].v);
      if (a[cur].id & 1) {
        ans[a[cur].id + 1 >> 1] = (ans[a[cur].id + 1 >> 1] + mod - res) % mod;
      }
      else {
        ans[a[cur].id >> 1] = (ans[a[cur].id >> 1] + res) % mod;
      }
      cur++;
    }
  }
  for (int i = 1; i <= m; i++) {
    printf("%d\n", ans[i]);
  }
  return 0;
}

在线求解:

考虑主席树,第 i i i棵主席树为前 i − 1 i - 1 i1棵主席树的信息 + 节点编号为 i i i的点到根节点权值都 + 1 +1 +1的信息。

不难想到,最后我们的答案就是在 [ l , r ] [l, r] [l,r]区间的主席树上查询在 1 − > z 1->z 1>z之间的点的权值和。

主席树上打 l a z y lazy lazy,空间按道理应该是 O ( n log ⁡ n log ⁡ n ) O(n \log n \log n) O(nlognlogn)级别的,支持在线求解时间复杂度也是 O ( n log ⁡ n log ⁡ n ) O(n \log n \log n) O(nlognlogn)级别的。

#include <bits/stdc++.h>

using namespace std;

const int N = 5e4 + 10, mod = 201314;

int head[N], to[N], nex[N], cnt = 1;

int fa[N], son[N], sz[N], top[N], dep[N], rk[N], id[N], tot;

int root[N], ls[N * 200], rs[N * 200], sum[N * 200], lazy[N * 200], n, m, num;

void add(int x, int y) {
  to[cnt] = y;
  nex[cnt] = head[x];
  head[x] = cnt++;
}

void dfs1(int rt, int f) {
  fa[rt] = f, dep[rt] = dep[f] + 1, sz[rt] = 1;
  for (int i = head[rt]; i; i = nex[i]) {
    if (to[i] == f) {
      continue;
    }
    dfs1(to[i], rt);
    sz[rt] += sz[to[i]];
    if (!son[rt] || sz[son[rt]] < sz[to[i]]) {
      son[rt] = to[i];
    }
  }
}

void dfs2(int rt, int tp) {
  top[rt] = tp, rk[++tot] = rt, id[rt] = tot;
  if (!son[rt]) {
    return ;
  }
  dfs2(son[rt], tp);
  for (int i = head[rt]; i; i = nex[i]) {
    if (to[i] == fa[rt] || to[i] == son[rt]) {
      continue;
    }
    dfs2(to[i], to[i]);
  }
}

void update(int &rt, int pre, int l, int r, int L, int R, int v) {
  rt = ++num;
  ls[rt] = ls[pre], rs[rt] = rs[pre], sum[rt] = sum[pre], lazy[rt] = lazy[pre];
  sum[rt] = (sum[rt] + 1ll * (min(r, R) - max(l, L) + 1) * v) % mod;
  if (l >= L && r <= R) {
    lazy[rt] = (lazy[rt] + v) % mod;
    return ;
  }
  int mid = l + r >> 1;
  if (L <= mid) {
    update(ls[rt], ls[pre], l, mid, L, R, v);
  }
  if (R > mid) {
    update(rs[rt], rs[pre], mid + 1, r, L, R, v);
  }
}

int query(int rt1, int rt2, int l, int r, int L, int R) {
  if (l >= L && r <= R) {
    return (sum[rt2] - sum[rt1] + mod) % mod;
  }
  int mid = l + r >> 1, ans = (1ll * (min(r, R) - max(l, L) + 1) * (lazy[rt2] - lazy[rt1]) % mod + mod) % mod;
  if (L <= mid) {
    ans = (ans + query(ls[rt1], ls[rt2], l, mid, L, R)) % mod;
  }
  if (R > mid) {
    ans = (ans + query(rs[rt1], rs[rt2], mid + 1, r, L, R)) % mod;
  }
  return ans;
}

int query(int l, int r, int rt) {
  int ans = 0;
  while (rt) {
    ans = (ans + query(root[l], root[r], 1, n, id[top[rt]], id[rt])) % mod;
    rt = fa[top[rt]];
  }
  return ans;
}

int main() {
  // freopen("in.txt", "r", stdin);
  // freopen("out.txt", "w", stdout);
  scanf("%d %d", &n, &m);
  for (int i = 2, fa; i <= n; i++) {
    scanf("%d", &fa);
    add(fa + 1, i);
  }
  dfs1(1, 0);
  dfs2(1, 1);
  for (int i = 1; i <= n; i++) {
    int rt = i;
    root[i] = root[i - 1];
    while (rt) {
      update(root[i], root[i], 1, n, id[top[rt]], id[rt], 1);
      rt = fa[top[rt]];
    }
  }
  for (int i = 1, l, r, rt; i <= m; i++) {
    scanf("%d %d %d", &l, &r, &rt);
    l++, r++, rt++;
    printf("%d\n", query(l - 1, r, rt));
  }
  return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LCA+路径压缩的方式可以用于求解树上的桥,具体实现步骤如下: 1. 对于树上每个节点,记录其在树中的深度(或者高度)以及其父亲节点。 2. 对于每个节点,记录其在树上的最小深度(或最小高度)以及其所在子树中深度最小的节点。 3. 对于每条边(u, v),设u的深度小于v的深度(或者高度),则如果v的子树中没有深度小于u的节点,则(u, v)是桥。 具体的实现过程如下: 首先,我们需要对树进行预处理,求出每个节点的深度以及其父亲节点。可以使用深度优先搜索(DFS)或广度优先搜索(BFS)来实现。在这里我们使用DFS来实现: ```c++ vector<int> adj[MAX_N]; // 树的邻接表 int n; // 树的节点数 int dep[MAX_N], fa[MAX_N]; // dep[i]表示节点i的深度,fa[i]表示节点i的父亲节点 void dfs(int u, int f, int d) { dep[u] = d; fa[u] = f; for (int v : adj[u]) { if (v != f) { dfs(v, u, d + 1); } } } ``` 接下来,我们需要计算每个节点所在子树中深度最小的节点。我们可以使用LCA(最近公共祖先)的方法来实现。具体来说,我们可以使用倍增算法来预处理出每个节点的2^k级祖先,并且在查询LCA时使用路径压缩的方式优化时间复杂度。这里我们不展开讲解LCA和倍增算法的细节,如果你对此感兴趣,可以参考其他资料进行学习。 ```c++ const int MAX_LOG_N = 20; // log2(n)的上取整 int anc[MAX_N][MAX_LOG_N]; // anc[i][j]表示节点i的2^j级祖先 int mn[MAX_N]; // mn[i]表示节点i所在子树中深度最小的节点 void precompute() { // 预处理anc数组 for (int j = 1; j < MAX_LOG_N; j++) { for (int i = 1; i <= n; i++) { if (anc[i][j - 1] != -1) { anc[i][j] = anc[anc[i][j - 1]][j - 1]; } } } // 计算mn数组 for (int i = 1; i <= n; i++) { mn[i] = i; for (int j = 0; (1 << j) <= dep[i]; j++) { if ((dep[i] & (1 << j)) != 0) { mn[i] = min(mn[i], mn[anc[i][j]]); i = anc[i][j]; } } } } ``` 最后,我们可以使用LCA+路径压缩的方式来判断每条边是否为桥。具体来说,对于每条边(u, v),我们需要判断v的子树中是否存在深度小于u的节点。如果存在,则(u, v)不是桥,否则(u, v)是桥。 ```c++ bool is_bridge(int u, int v) { if (dep[u] > dep[v]) swap(u, v); if (mn[v] != u) return true; // 子树中存在深度小于u的节点 return false; // 子树中不存在深度小于u的节点 } ``` 完整代码如下:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值