首页 > 其他分享 >线段树分治

线段树分治

时间:2024-09-02 20:47:55浏览次数:4  
标签:sz return int 线段 分治 back edge getfa

前置知识:可撤销化并查集

注意:可撤销化并查集的作用和删边不一样,其只能撤销最近的一次操作。

既然只需撤销,那么只需在合并并查集时用个 vector 记录下合并的哪两个点,撤销时就直接还原就行了。

这里要强调一下,可撤销化并查集不能路径压缩,只能启发式合并。

代码

int f[MAXN], sz[MAXN];
vector<pii> edge;

int getfa(int u) {
  return (!f[u] ? u : getfa(f[u]));
}

bool Merge(int u, int v) {
  u = getfa(u), v = getfa(v);
  if(u == v) {
    edge.emplace_back(0, 0);
    return 0;
  }
  if(sz[u] > sz[v]) {
    swap(u, v);
  }
  f[u] = v, sz[v] += sz[u];
  edge.emplace_back(u, v);
  return 1;
}

bool Cancal() {
  auto [u, v] = edge.back();
  edge.pop_back();
  sz[v] -= sz[u], f[u] = 0;
  return u && v;
}

线段树分治

这里只讲以下这个问题,其他的可以用类似的方法解决:

给定一个 \(N\) 个结点的图,初始时图上没有边,接下来将会进行一系列加入/删除边的操作,请在每次询问时求出图中连通块数量。

我们可以去思考每一条边在哪些时刻存在,以下面这个为例:

在图中,绿色的区域表示这条边存在的时间段。然后,我们考虑用类似于线段树的方式进行区间修改。

我们对以下这个样例画一棵线段树:

+ 1 2
+ 3 4
+ 1 5
- 1 5
- 1 2
+ 1 3
+ 4 5
- 1 3

这里每个结点都有一个 vector 来记录它所对应的区间要加入的边,接着我们考虑在这个线段树上遍历。

image-20240902204420022

在遍历时,进入某个结点我们就把绿色的边加入,回溯时就把红色的边删除(撤销)。并在每个叶子节点处记录答案即可。

空间复杂度 \(O(N+Q)\),时间复杂度 \(O(Q\log Q\cdot\log N)\)。

代码

#include<bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;

const int MAXN = 300005;

int f[MAXN], sz[MAXN], cnt;
vector<pii> edge;

int getfa(int u) {
  return (!f[u] ? u : getfa(f[u]));
}

bool Merge(int u, int v) {
  u = getfa(u), v = getfa(v);
  if(u == v) {
    edge.emplace_back(0, 0);
    return 0;
  }
  if(sz[u] > sz[v]) {
    swap(u, v);
  }
  f[u] = v, sz[v] += sz[u];
  edge.emplace_back(u, v);
  return 1;
}

bool Cancal() {
  auto [u, v] = edge.back();
  edge.pop_back();
  sz[v] -= sz[u], f[u] = 0;
  return u && v;
}

struct Segment_Tree {
  int l[MAXN << 2], r[MAXN << 2], ans[MAXN];
  vector<pii> e[MAXN << 2];
  void build(int u, int s, int t) {
    if(s > t) {
      return;
    }
    l[u] = s, r[u] = t;
    if(s == t) {
      return;
    }
    int mid = (s + t) >> 1;
    build(u << 1, s, mid), build((u << 1) | 1, mid + 1, t);
  }
  void update(int u, int s, int t, pii g) {
    if(l[u] >= s && r[u] <= t) {
      e[u].push_back(g);
      return;
    }
    if(s <= r[u << 1]) {
      update(u << 1, s, t, g);
    }
    if(t >= l[(u << 1) | 1]) {
      update((u << 1) | 1, s, t, g);
    }
  }
  void Getans(int u) {
    for(auto [u, v] : e[u]) {
      cnt -= Merge(u, v);
    }
    if(l[u] == r[u]) {
      ans[l[u]] = cnt;
      for(auto [u, v] : e[u]) {
        cnt += Cancal();
      }
      return;
    }
    Getans(u << 1), Getans((u << 1) | 1);
    for(auto [u, v] : e[u]) {
      cnt += Cancal();
    }
  }
}tr;

int n, q;
bool op[MAXN];
map<int, int> l[MAXN];
vector<pii> g;

void FileIO(const string &s) {
  freopen((s + ".in").c_str(), "r", stdin);
  freopen((s + ".out").c_str(), "w", stdout);
}

int main() {
  ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
  FileIO("connect");
  cin >> n >> q;
  cnt = n;
  tr.build(1, 1, q);
  for(int i = 1, u, v; i <= q; ++i) {
    char c;
    cin >> c;
    if(c == '+') {
      cin >> u >> v;
      l[u][v] = l[v][u] = i;
      g.emplace_back(u, v);
    }else if(c == '-') {
      cin >> u >> v;
      tr.update(1, l[u][v], i - 1, {u, v});
      l[u][v] = l[v][u] = 0;
    }else {
      op[i] = 1;
    }
  }
  for(auto [u, v] : g) {
    if(l[u][v]) {
      tr.update(1, l[u][v], q, {u, v});
    }
  }
  fill(sz + 1, sz + n + 1, 1);
  tr.Getans(1);
  for(int i = 1; i <= q; ++i) {
    if(op[i]) {
      cout << tr.ans[i] << "\n";
    }
  }
  return 0;
}

标签:sz,return,int,线段,分治,back,edge,getfa
From: https://www.cnblogs.com/yaosicheng124/p/18393506

相关文章

  • codeforces做题记录(1924B)& 回顾线段树
    1924B.SpaceHarbour题意:n个点排成一行,其中某些点上面建有港湾,港湾有一个权值,对每个点我们定义点的权值为“左边(包括自己)第一个港湾上的权值\(\times\)到右边(包括自己)第一个港湾的距离”(保证在一开始1号和n号点上都有港湾)。有q次操作:操作1给定x和v,表示在x点上建立权值为v的......
  • 算法设计与分析:实验二 分治法——最近点对问题
    实验内容:对于平面上给定的N个点,给出所有点对的最短距离,即,输入是平面上的N个点,输出是N点中具有最短距离的两点。要求随机生成N个点的平面坐标,应用蛮力法编程计算出所有点对的最短距离。要求随机生成N个点的平面坐标,应用分治法编程计算出所有点对的最短距离。分别对N=100000—10......
  • 优化半群结构的线段树信息维护
    今天在做区间历史和。感觉给每个标记一个含义实在太抽象了,遂听从白神建议学习矩阵维护信息和优化半群结构。前置知识:大魔法师,用矩阵维护轮换信息。我们发现区间历史和事实上是对“历史和”变量被“和”变量轮换加法的结果,不知道为什么以前没反应过来和大魔法师有关。区间加和区......
  • 线段覆盖问题
    1.线段不覆盖问题给出\(n\)个线段,选择尽量多的线段使得线段无重叠,问最多可以选多少条线段。解析考虑贪心,将线段按右端点从小到大排序,如果这条线段的左端点大于上一条线段的右端点那就选择这条线段。为什么这么贪是对的呢,因为将右端点排序可以使右边剩余的空间尽量大,那么剩余......
  • Luogu P4425 转盘 题解 [ 黑 ] [ 线段树 ] [ 贪心 ] [ 递归 ]
    转盘:蒟蒻的第一道黑,这题是贪心和线段树递归合并的综合题。贪心破环成链的trick自然不用多说。首先观察题目,很容易发现一个性质:只走一圈的方案一定最优。这个很容易证,因为再绕一圈回来标记前面的和等前面的标记完之后继续走是等价的,并且再绕一圈甚至可能更劣。于是,我们只用走......
  • 线段树
    线段树区间加区间和区间乘法的懒标记优先级高于加法,且会对加法懒标记造成影响voidpush_up(node&u,node&l,node&r){ u.sum=l.sum+r.sum;//}voidpushup(vector<node>&tr,intu){push_up(tr[u],tr[u<<1],tr[u<<1|1]);}voidpush_down(vector<node......
  • 【数据结构】【模板】李超线段树
    李超线段树定义可以看看洛谷的模板题目:作用优化动态规划,如果可以将一个动态规划的转移式子转化为\(y=kx+b\)的形式,那么我们可以边转移边将\(y=kx+b\)这条线段放入李超线段树,然后在下次转移时,直接调用下次计算出来的\(x\)位置上的最大值或最小值。(结合题目理解......
  • Luogu P4588 数学运算 题解 [ 绿 ] [ 线段树 ]
    LuoguP4588数学运算。虽然是一个很典的题,但里面的思想还是比较值得记录的。假做法一开始看到此题还以为是乘法逆元的模板题,但看到\(m\)与\(M\)不互质,就知道这种做法是假的了。注意exgcd虽然能求模数为合数的逆元,但是要是两数不互质就什么算法都搞不了了。因此,本题不能......
  • 线段树模版:从入门到入坟
    线段树模版:从入门到入坟线段树——单点修改1.求区间最值#include<bits/stdc++.h>#defineintlonglongusingnamespacestd;constintN=200010;typedeflonglongll;structnode{ intminx; intl,r;}tr[N*4];inta[N];voidupdate(intp){ tr[p].minx=min(tr[......
  • 牛客周赛 Round 57 D-小红的线段(贪心)
     示例1输入2-1200011011输出112N34Y说明连接第一个点和第二个点,和直线没有交点。连接第三个点和第四个点,和直线有交点。 贪心策略:把点集分为三部分,直线上方m1、直线下方m2以及在直线上m3,我们可以发现:m1中的点和m2中的任意点相连都能通过直线;m3......