首页 > 其他分享 >P4565 & CF757G 笔记

P4565 & CF757G 笔记

时间:2024-01-31 19:45:34浏览次数:17  
标签:sz val P4565 int 分治 笔记 CF757G root dis

前置知识:线段树合并,可持久化线段树,边分治,可能会用到一点点虚树。

P4565

边分树神题啊。。。

题意

给定两棵边有边权的树 \(T_1, T_2\),结点数都为 \(n\)。设 \(d_i(x)\) 表示第 \(i\) 棵树上 \(x\) 的带权深度,
求一组点对 \((x, y)\),使得 \(d_1(x) + d_1(y) - d_1(p) - d_2(p')\) 最大,
其中 \(p, p'\) 分别代表 \(x, y\) 在两树上的 \(LCA\)。为了方便,只需输出最大值。

题解

先考虑变换一下这个式子,设 \(dis_i(x, y)\) 表示 \((x, y)\) 在第 \(i\) 棵树上的距离。那么原式就等于 \({1 \over 2}(dis_1(x, y) + d_1(x) + d_1(y) - 2d_2(p'))\)。
于是沿用通道那题的做法,在 \(T_1\) 上新建结点 \(i'\) 与 \(i\) 连接,边权为 \(d_1(i)\),接着在 \(T_2\) 上枚举 \(p'\),找出最长路就行了?!
很不幸,这道题的边权可以是负数。所以无法合并最长路。

于是换一种思路,考虑将 \(dis_1(x, y)\) 转化一下。将 \(T_1\) 边分治,在一次分治过程中,设 \(h(x)\) 表示 \(x\) 到分治中心一侧的距离,答案变成了 \(p(x) + p(y) + d_1(x) + d_1(y) - 2d_2(p')\),设 \(val_x = p_x + d_1(x)\),那么又变为 \(val_x + val_y - 2d_2(p')\)。在 \(T_2\) 中建出当前分治块的虚树,然后枚举一下 \(p'\) 就行了。复杂度 \(\mathcal{O}(n \log^2 n)\)。
然而,注意到 \(n \leqslant 366666\),所以这种做法虽然简单,但如果实现不好就需要大量卡常。而事实上,我们也能给出更为优秀的做法。

考虑先枚举 \(p'\),然后算它的子树在 \(T_1\) 中对答案的贡献。于是不得不提到一个科技,叫边分树。

边分树

边分树是由 \(n\) 个长度为 \(\mathcal{O}(\log n)\) 的链合并而成的。而且是一棵二叉树。

想要构建出 \(n\) 个链,一种方法就是:初始时建立 \(n\) 个二叉链,每个都为空,称第 \(i\) 个节点对应第 \(i\) 条链。对树进行边分治,设 \(u, v\) 为分治边 \(e\) 的两端点,钦定 \(dep(u) < dep(v)\),将 \(u\) 那一侧的所有点对应的二叉链底端的左儿子赋为 \(e\),将 \(v\) 侧的所有点对应的二叉链底端的右儿子赋为 \(e\)。然后进行下一次分治。

也就是说一条二叉链上的点都是原树的一条边。

当两个边分树合并时,其方法类似于线段树合并,这里就不细说。最后合并完 \(n\) 个二叉链的边分树等同于边分治时边与边的关系树,并且也是一棵二叉树。

边分树最强的地方就是可以维护某些信息。回到这道题,先枚举 \(p'\),这时递归它在 \(T_2\) 的儿子,每个儿子都维护一个它们的点集合并出来的边分树。在合并两个儿子的时候,可以顺便在合并时统计答案。思考 \((x, y)\) 在边分时应该何时统计它们的答案,应该是 \(x, y\) 在同一分治块时。对应到边分树上就是:到某一深度前,\(x, y\) 的边分树左右儿子关系都相同,在这一深度时,左右儿子关系出现第一次不同。那么为了统计答案,边分树上每个节点维护:这个点代表连通块内 \(val\) 的最大值。合并时,当前两个节点祖先左右儿子关系一定相同(类似线段树合并),答案不难统计。时间复杂度 \(\mathcal{O}(n \log n)\)。

代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 8E5 + 5;
int n;
i64 d[2][N], val[N];
struct lsqxx {
  int head[N >> 1], nex[N], sz[N], ctot;
  struct E {
    int to, nxt;
    int dis;
  } edge[N];
  void add(int x, int y, int z) {
    edge[++ctot] = (E) {x, y, z};
    ++sz[x]; ++sz[y];
    nex[ctot] = head[x]; head[x] = ctot;
  }
} g[2];
i64 ans = -1E15;
int ftot, root[N], ld[N];
struct bfs {
  struct node {
    int ls, rs;
    i64 val;
  } t[N * 40];
  int merge(int x, int y, int rt) {
    if (!x || !y) return x | y;
    if (t[x].ls && t[y].rs) 
      ans = max(ans, t[t[x].ls].val + t[t[y].rs].val - 2 * d[1][rt]);
    if (t[x].rs && t[y].ls)
      ans = max(ans, t[t[x].rs].val + t[t[y].ls].val - 2 * d[1][rt]);
    t[x].val = max(t[x].val, t[y].val);
    t[x].ls = merge(t[x].ls, t[y].ls, rt);
    t[x].rs = merge(t[x].rs, t[y].rs, rt);
    return x;
  }
} t;
namespace bfz {
  int sz[N], dmx, m, root, dsum, dep[N], head[N], nex[N << 1], tot = 1;
  bool vis[N];
  struct E {int to, nxt, dis;} edge[N << 1];
  void add(int x, int y, int z) {
    edge[++tot] = (E) {x, y, z};
    nex[tot] = head[x]; head[x] = tot;
  }
  void rebuild(int x, int fa) {
    int tmp = 0, len = g[0].sz[x], last;
    for (int i = g[0].head[x]; i; i = g[0].nex[i]) {
      int v = g[0].edge[i].nxt, w = g[0].edge[i].dis;
      if (v == fa) continue;
      ++tmp; if (tmp == 1) {
        add(x, v, w); add(v, x, w);
        last = x;
      } else if (tmp == len - (x != 1)) {
        add(last, v, w); add(v, last, w);
      } else {
        ++m;
        add(m, last, 0); add(last, m, 0);
        last = m;
        add(v, m, w); add(m, v, w);
      }
    }
    for (int i = g[0].head[x]; i; i = g[0].nex[i]) {
      int v = g[0].edge[i].nxt, w = g[0].edge[i].dis;
      if (v != fa) {
        dep[v] = dep[x] + 1;
        d[0][v] = d[0][x] + w;
        rebuild(v, x);
      }
    }
  }
  void getroot(int x, int fa) {
    sz[x] = 1;
    for (int i = head[x]; i; i = nex[i]) {
      int v = edge[i].nxt;
      if (v == fa || vis[i >> 1]) continue;
      getroot(v, x); sz[x] += sz[v];
      int tmp = max(sz[v], dsum - sz[v]);
      if (dmx > tmp) {
        dmx = tmp;
        root = i;
      }
    }
  }
  void dfs(int x, int fa, i64 dis, bool typ) {
    if (x <= n) {
      val[x] = d[0][x] + dis;
      t.t[++ftot] = (bfs :: node) {0, 0, val[x]};
      if (!typ) t.t[ld[x]].ls = ftot;
      else t.t[ld[x]].rs = ftot;
      ld[x] = ftot;
    }
    for (int i = head[x]; i; i = nex[i]) {
      int v = edge[i].nxt, w = edge[i].dis;
      if (vis[i >> 1] || v == fa) continue;
      dfs(v, x, dis + w, typ);
    }
  }
  void solve(int x, int nsum) {
    dmx = 1E9; dsum = nsum; root = 0;
    getroot(x, 0); if (!root) return ;
    vis[root >> 1] = 1;
    int u = edge[root].to, v = edge[root].nxt, w = edge[root].dis;
    bool cnm = 0;
    if (dep[u] > dep[v]) swap(u, v), cnm = 1;
    dfs(u, v, 0, 0); dfs(v, u, w, 1);
    if (cnm) swap(u, v);
    solve(u, dsum - sz[v]); solve(v, sz[v]);
  }
  void solve() {
    m = n;
    rebuild(1, 0);
    solve(1, m);
  }
}
void dfs(int x, int fa) {
  for (int i = g[1].head[x]; i; i = g[1].nex[i]) {
    int v = g[1].edge[i].nxt, w = g[1].edge[i].dis;
    if (v == fa) continue;
    d[1][v] = d[1][x] + w;
    dfs(v, x);
    root[x] = t.merge(root[x], root[v], x);
  }
}
signed main(void) {
  ios :: sync_with_stdio(false);
  cin.tie(nullptr); cout.tie(nullptr);
  cin >> n;
  for (int i = 0; i < 2; ++i) {
    for (int j = 1; j < n; ++j) {
      int u, v, w; cin >> u >> v >> w;
      g[i].add(u, v, w);
      g[i].add(v, u, w);
    }
  } 
  for (int i = 1; i <= n; ++i) ld[i] = root[i] = ++ftot;
  bfz :: solve(); 
  dfs(1, 0);
  for (int i = 1; i <= n; ++i) {
    ans = max(ans, 2 * (d[0][i] - d[1][i]));
  }
  cout << ans / 2 << '\n';
}
/*
类人群星闪耀时:
1. 初始 root 没开节点。
2. dmx=tmp 写成 tmp=dmx。
3. 没判 x=y 的情况。
4. 三度化建边忘记赋边权。
5. 根据 dep 交换 u, v 后忘记交换回来。
*/

CF757G

边分树练习题

题意

给定一棵 \(n\) 个结点的树和一个长度为 \(n\) 的排列 \(p\),边有边权。接下来有 \(m\) 个操作,每个操作有两种类型:

  • 1 l r x 询问 \(\sum\limits_{i = l}^r dis(p_i, x)\)。
  • 2 x,交换 \(p_x, p_{x + 1}\),满足 \(x < n\)。

强制在线。

题解

看到路径问题,可以想到点分治。于是一个初步的做法就是,记 \(k_{p_i} = i\),建立一棵点分树,记 \(dis_x\) 为 \(x\) 到分治中心的距离,点分树上每个点维护分治块内以 \(k_i\) 为下标的,\(dis_i\) 为值的动态开店权值线段树。但是这样时空复杂度都是 \(\mathcal{O}(n \log^2 n)\),这道题有点卡空间,不是很推荐。

考虑边分树,沿用暴力写挂的思路,可以研发出可持久化边分树。类似于主席树,先边分治一遍弄出每个点对应的二叉链,然后 \(root_i\) 在 \(root_{i - 1}\) 的基础上合并上 \(p_i\) 对应的二叉链即可。这里边分树每个结点维护的信息显然是分治块内 \(dis\)(到分治边一侧的距离)之和。

这样在询问的时候,对于第一种操作,找到 \(x\) 对应的二叉链,然后进行一个 \(query\),用 \(root_r\) 的答案减去 \(root_{l - 1}\) 的答案就行了。对于第二种操作,可以发现,竟然这只会改变 \(root_x\) 这一棵可持久化边分树!所以找到 \(p_{x + 1}\) 对应的二叉链,然后暴力重构一下 \(root_x\) 就行了。

代码
#include <bits/stdc++.h>
#define big() (n >= 100000)
using namespace std;
using i64 = long long;
const int N = 6E5 + 5;
int n, q, pe[N];
vector <pair <int, int>> G[N];
int root[N << 1], ld[N << 1], tot;
struct bfs {
  struct node {
    int ls, rs;
    i64 sum, cnt;
  } t[N * 40];
  void insert(int x, i64 val, bool p) {
    t[++tot] = (node) {0, 0, val, 1};
    (!p ? t[ld[x]].ls : t[ld[x]].rs) = tot;
    ld[x] = tot;
  }
  int update(int p, int q) {
    int rt = ++tot; t[rt] = t[p];
    if (!q) return rt; //cnmd
    t[rt].sum += t[q].sum; t[rt].cnt += t[q].cnt;
    t[rt].ls = update(t[rt].ls, t[q].ls);
    t[rt].rs = update(t[rt].rs, t[q].rs);
    return rt;
  }
  i64 query(int p, int q) {
    if (!p || !q) return 0;
    i64 res = 0;
    if (t[p].ls && t[q].rs)
      res += t[t[p].ls].sum + t[t[p].ls].cnt * t[t[q].rs].sum;
    if (t[p].rs && t[q].ls)
      res += t[t[p].rs].sum + t[t[p].rs].cnt * t[t[q].ls].sum;
    return res + query(t[p].ls, t[q].ls) + query(t[p].rs, t[q].rs);
  }
} t;
i64 CCnt;
namespace bfz {
  int head[N << 1], nex[N << 2], dmx, dsum, m, tot = 1, dep[N << 1];
  bool vis[N << 1];
  struct E {int to, nxt, dis;} edge[N << 2];
  void add(int x, int y, int z) {
    edge[++tot] = (E) {x, y, z};
    nex[tot] = head[x]; head[x] = tot;
  }
  void rebuild(int x, int fa) {
    int tmp = 0, len = G[x].size(), last;
    for (auto [v, w] : G[x]) {
      if (v == fa) continue;
      ++tmp; if (tmp == 1) {
        add(x, v, w); add(v, x, w);
        last = x;
      } else if (tmp == len - (x != 1)) {
        add(last, v, w); add(v, last, w);
      } else {
        ++m;
        add(last, m, 0); add(m, last, 0);
        last = m;
        add(m, v, w); add(v, m, w);
      }
    }
    for (auto [v, w] : G[x]) if (v != fa)
      rebuild(v, x);
  }
  int sz[N << 1], root;
  void getroot(int x, int fa) {
    ++CCnt;
    sz[x] = 1;
    for (int i = head[x]; i; i = nex[i]) {
      int v = edge[i].nxt;
      if (vis[i >> 1] || v == fa) continue;
      getroot(v, x); sz[x] += sz[v];
      int tmp = max(sz[v], dsum - sz[v]);
      if (dmx > tmp) {
        dmx = tmp;
        root = i;
      }
    }
  }
  void dfs(int x, int fa, i64 dis, bool typ) {
    if (x <= n) 
      t.insert(x + n, dis, typ);
    for (int i = head[x]; i; i = nex[i]) {
      int v = edge[i].nxt, w = edge[i].dis;
      if (v == fa || vis[i >> 1]) continue;
      dfs(v, x, dis + w, typ);
    }
  }
  void solve(int x, int nsum) {
    dsum = nsum; dmx = 1E9; root = 0;
    getroot(x, 0); if (!root) return ;
    vis[root >> 1] = 1;
    int u = edge[root].to, v = edge[root].nxt, w = edge[root].dis;
    bool cnm = 0;
    if (dep[u] > dep[v]) swap(u, v), cnm = 1;
    dfs(u, v, 0, 0); dfs(v, u, w, 1);
    if (cnm) swap(u, v);
    solve(u, dsum - sz[v]); solve(v, sz[v]);
  }
  void prework(int x, int fa) {
    for (int i = head[x]; i; i = nex[i]) {
      int v = edge[i].nxt;
      if (v == fa) continue;
      dep[v] = dep[x] + 1;
      prework(v, x);
    }
  }
  void solve() {
    m = n;
    rebuild(1, 0);
    prework(1, 0);
    solve(1, m);
  }
}
signed main(void) {
  ios :: sync_with_stdio(false);
  cin.tie(0); cout.tie(0);
  cin >> n >> q;
  for (int i = 1; i <= n; ++i) cin >> pe[i];
  for (int i = 1; i < n; ++i) {
    int u, v, w; cin >> u >> v >> w;
    G[u].emplace_back(v, w);
    G[v].emplace_back(u, w);
  } for (int i = 1; i <= n; ++i) root[i + n] = ld[n + i] = ++tot;
  bfz :: solve();
  for (int i = 1; i <= n; ++i) 
    root[i] = t.update(root[i - 1], root[pe[i] + n]);
  i64 lastans = 0;
  for (int i = 1; i <= q; ++i) {
    i64 opt, x; cin >> opt;
    if (opt == 1) {
      i64 l, r; cin >> l >> r >> x;
      l ^= lastans;
      r ^= lastans;
      x ^= lastans;
      cout << (lastans = \
      (t.query(root[r], root[x + n]) - t.query(root[l - 1], root[x + n]))) << '\n';
      lastans %= (1 << 30);
    } else {
      cin >> x; x ^= lastans;
      swap(pe[x], pe[x + 1]);
      root[x] = t.update(root[x - 1], root[pe[x] + n]);
    }
  }
  return 0;
}

标签:sz,val,P4565,int,分治,笔记,CF757G,root,dis
From: https://www.cnblogs.com/CTHOOH/p/17998170

相关文章

  • 阅读笔记3
    阅读《程序员的修炼之道:从小工到专家》第八章后,我对团队沟通和协作的重要性有了更深入的理解。这一章节详细探讨了如何建立高效的团队沟通机制,以及如何提高团队协作能力,以达到更好的软件开发效果。首先,作者强调了沟通在团队中的重要性。在软件开发过程中,团队成员之间需要频繁......
  • 1/31 学习进度笔记
    今日完成了商单案例:源码:#coding:utf8frompysparkimportStorageLevelfrompyspark.sqlimportSparkSessionfrompyspark.sqlimportfunctionsasFfrompyspark.sql.typesimportStringTypeif__name__=='__main__':spark=SparkSession.builder.appName(&qu......
  • 阅读笔记
    《人月神话》是软件工程领域的一部经典之作,它以其独特的视角和深刻的洞察力,让我对软件开发有了更加全面和深入的认识。在阅读这本书的过程中,我深深地被作者对软件开发的独到见解所吸引。作者通过自己在IBM公司从事大型软件项目开发的亲身经历,向我们揭示了软件开发过程中的种种困......
  • 阅读笔记2
    阅读完《程序员的修炼之道:从小工到专家》第七章后,我对掌握编程语言的重要性有了更深入的理解。这一章节详细探讨了如何选择适合自己的编程语言,以及如何精通掌握一门或多门编程语言。首先,作者强调了编程语言在程序员职业生涯中的重要性。编程语言是程序员表达思想、解决问题的重要......
  • 性能测试学习笔记
    一、性能测试的基本操作1、概念:体现当前服务器到底能不能带动开发的软件2、服务器优化:升级配置(服务器升级内容、cpu),横向扩容(多台服务器)3、并发:  二、性能测试的调优(具体怎么调,由开发/运维去做),测试指导开发问题发生的位置 ......
  • kali学习笔记-06-Webshell文件上传漏洞使用
    kali学习笔记-06-Webshell文件上传漏洞使用KaliLinux网络安防一、使用weevely制作一句话木马脚本在KaliLinux的终端中输入命令weevely,可以从错误提示中看到基本的使用方法。二、配置OWASP靶机三、参考文献WebShell文件上传漏洞.3......
  • python网络编程笔记(一)Socket 编程入门
    一:Socket简介套接字起源于20世纪70年代加利福尼亚大学伯克利分校版本的Unix,即人们所说的BSDUnix。因此,有时人们也把套接字称为“伯克利套接字"或"BSD套接字”。一开始,套接字被设计用在同-台主机上多个应用程序之间的通讯BSDSocket接口是TCP/IP网络的API在Linux,Unix和W......
  • kali学习笔记-05-DVWA XSS跨站脚本攻击
    kali学习笔记-05-DVWA XSS跨站脚本攻击KaliLinux网络安防一、反射型XSS攻击在OWASP的DVWA上,选中XSSreflected页面,在输入框内输入张三,页面反应正常。尝试输入一句script脚本。<script>alert('xss')</script>出现了如下的系统弹框,也就意味着后端服务器没有对特殊字符做......
  • Tomcat学习笔记
    1.Tomcat总体架构Tomcat要实现2个核心功能:处理Socket连接,负责网络字节流与Request和Response对象的转化。加载和管理Servlet,以及具体处理Request请求。Tomcat设计了两个核心组件连接器(Connector)和容器(Container)来分别做这两件事情。连接器负责对外交流,容器负责内部处理......
  • CMake 笔记
    cmake笔记目录cmake笔记CMake命令行选项常用CMake语法特殊变量变量常用基本测试安装其它其它基本使用FetchContentvcpkgwindowslinux及wslwindows配环境还能不能再恶心点......