APIO 2019 桥梁

题目链接

LOJ3145
LuoguP5443

题目概括

给定一张 \(N\) 个点,\(M\) 条边的无向带权图。

每次询问给定一个二元组 \((x,y)\),从 \(x\) 号节点开始出发,只允许通过边权 \(\geq y\) 的边。

问能够到达的联通块最大的大小。

要求动态修改边权

数据范围:\(N\leq 5\times 10^4,M\leq 10^5\)

思路要点

子任务讨论

暴力就不进行过多的讲解。

子任务 链

本质是一个序列问题。

你可以发现每一次的操作是进行一次向左边扩展和向右边扩展,找到第一条不满足的边。也就是说你需要找到一个包含起点的最大区间,满足在上面的所有边权都满足 \(\geq y\)。

因为支持修改操作,所以考虑用二分答案,线段树查询的方法在 \(O(nlog^2n)\) 的时间解决。

子任务 只有查询

我们假设联通块中最小的边权为 \(min\),那么某一个起点开始能够遍历整个联通块的充要条件是 \(min \ge y\)。

也就是最小边的边权 \(\ge y\),这就容易想到用 \(Kruscal\) 重构树来实现这个过程。

建立出 \(Kruscal\) 重构树之后,以每一个节点为根节点的子树,都可以被以根节点为起点的路径包含,所以只需要倍增找到最后一个满足条件的祖先节点,然后其子树大小即为答案。

也可以采用离线做法,将操作离线后,按照 \(y\) 值从大到小查询,将所有的边按照顺序加入到并查集中,答案就是联通块大小

正解

采用「子任务 只有查询」中的做法,考虑暴力。

我们按照询问分块,在当前块中的所有操作都相当于和前面所有块的到的新图重新暴力求一遍答案,然后更新为新图

将当前块中不需要修改的边加入到联通块中,考虑修改的边,将在当前操作之前的每一条边最后一次操作得到的权值和标准值 \(y\) 进行比较,否则视为未被修改。

每一次操作,同一条边可能被不同的修改操作,所以考虑要可撤销并查集

复杂度分析

笔者不会分析这一部分的复杂度,而且觉得网上大部分的分析都有一点问题。

以上复杂度分析可能存在问题

代码

#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

namespace FastIO {

template <class T> 
void rd(T& x) {
  x = 0;
  char ch = 0;
  bool f = 0;
  while (!isdigit(ch)) f |= ch == '-', ch = getchar(); 
  while (isdigit(ch)) x = x * 10 + (ch ^ 48), ch = getchar();
  f ? x = -x : 1;
}

template <class T> 
void ptf(T x) {
  if (x < 0) putchar('-'), x = -x;
  if (x > 9) ptf(x / 10);
  putchar(x % 10 + 48);
}

void read(int& x) { rd(x); }
void read(long long& x) { rd(x); }
void read(unsigned int& x) { rd(x); }
void read(unsigned long long& x) { rd(x); }
void read(char& x) { x = getchar(); }
void read(string& x) { cin >> x; }
template <class T, class R> 
void read(pair<T, R>& x) {
  read(x.first), read(x.second);
}
template <class T> 
void read(vector<T>& x) { 
  for (auto& ele : x) read(x); 
}

void write(int x) { ptf(x); }
void write(long long x) { ptf(x); }
void write(unsigned long long x) { ptf(x); }
void write(unsigned int x) { ptf(x); }
void write(char x) { putchar(x); }
void write(char* x) { printf("%s", x); }
void write(string x) { cout << x; }
template <class T> 
void write(vector<T> x) {
  for (auto ele : x)  (x); 
}
template <class T, class R> 
void write(pair<T, R> x) {
  write(x.first), putchar(','), write(x.second);
}
template <class T> 
void writeln(T x) {
  write(x), puts("");
}

}

using FastIO::read;
using FastIO::write;
using FastIO::writeln;

namespace RevocableDisjoinSet {

const int N = 1e5 + 5; 

struct DisjoinSet {
  int top, n; 
  int fa[N], sz[N];
  pair<int, int> stk[N];

  void init(int x) {
    n = x, top = 0;
    for (int i = 1; i <= n; i++) {
      fa[i] = i; 
      sz[i] = 1;
    }
  }

  int get(int x) {
    return fa[x] == x ? x : get(fa[x]); 
  }

  void merge(int x, int y) {
    int p1 = get(x), p2 = get(y);
    if (p1 != p2) {
      if (sz[p1] > sz[p2]) { swap(p1, p2); }
      fa[p1] = p2; 
      sz[p2] += sz[p1];
      stk[++top] = make_pair(p1, p2);
    }
  }

  void revoke(int goal) {
    while (top > goal) {
      fa[stk[top].first] = stk[top].first;
      fa[stk[top].second] = stk[top].second; 
      sz[stk[top].second] -= sz[stk[top].first];
      top--; 
    }
  }

  int check(int x, int y) {
    return get(x) == get(y);
  }
};

}

// ===============

RevocableDisjoinSet::DisjoinSet dsu;

const int N = 5e4 + 5, M = 1e5 + 5; 

struct Ed {
  int u, v, w, id; // 现在第 i 条边 原来的编号
  
  bool operator<(const Ed& rhs) const {
    return w > rhs.w || (w == rhs.w && id < rhs.id);
  }
} ed[M], ed2[M], tmp[M];

struct Op {
  int t, x, y, id;

  bool operator<(const Op& rhs) const {
    return y > rhs.y;
  }
} op[M], op1[M], op2[M];

int n, m, q, tot;
int ismdy[M], did[M], ans[M];
int rk[M]; // 第 i 条边的 rank (按照 w 排序)

/**
 * 对于当前的边 i, 原来的编号为 ed[i].i 
 * 其中原来的编号是没有排序过的,现在的编号为排序完的下标
 */
void solve() {
  dsu.init(n);
  memset(ismdy, 0, sizeof ismdy);
  int t1 = 0, t2 = 0, pt = 1; // pt 遍历现在的编号
  for (int i = 1; i <= tot; i++) 
    if (op[i].t == 1) {
      op1[++t1] = op[i];
      ismdy[op[i].x] = 1; // 将这条边的原来的编号打上修改标记
    } else 
      op2[++t2] = op[i];
  // op2 是询问 op1 是修改
  sort(op2 + 1, op2 + 1 + t2);
  for (int i = 1; i <= t2; i++) {
    while (pt <= m && ed[pt].w >= op2[i].y) {
      if (!ismdy[ed[pt].id]) 
        dsu.merge(ed[pt].u, ed[pt].v);
      pt++; 
    }
    int temp = dsu.top;
    for (int j = 1; j <= t1; j++) 
      did[op1[j].x] = 0;
    for (int j = 1; j <= t1; j++) 
      if (op1[j].id < op2[i].id) 
        did[op1[j].x] = j; 
    for (int j = 1; j <= t1; j++) 
      if (did[op1[j].x] == j) {
        if (op1[j].y >= op2[i].y) 
          dsu.merge(ed[rk[op1[j].x]].u, ed[rk[op1[j].x]].v);
      } else if (did[op1[j].x] == 0 && ed[rk[op1[j].x]].w >= op2[i].y) 
        dsu.merge(ed[rk[op1[j].x]].u, ed[rk[op1[j].x]].v);
    ans[op2[i].id] = dsu.sz[dsu.get(op2[i].x)];
    dsu.revoke(temp);
  }
  memset(did, 0, sizeof did);
  int l = 1, r = 1, num = 0, t = 0;
  for (int i = t1; i >= 1; i--) 
    if (!did[op1[i].x]) {
      did[op1[i].x] = 1; 
      ed2[++num] = ed[rk[op1[i].x]];
      ed2[num].w = op1[i].y; 
    }
  sort(ed2 + 1, ed2 + 1 + num);
  while (l <= m && r <= num) {
    while (l <= m && did[ed[l].id]) 
      l++;
    if (ed[l].w >= ed2[r].w) 
      tmp[++t] = ed[l], l++;
    else 
      tmp[++t] = ed2[r], r++;
  }
  while (l <= m) {
    if (!did[ed[l].id])
      tmp[++t] = ed[l];
    l++;
  }
  while (r <= num) 
    tmp[++t] = ed2[r], r++;
  for (int i = 1; i <= m; i++) {
    ed[i] = tmp[i];
    rk[ed[i].id] = i; 
  }
}

int main() {
  read(n), read(m);
  for (int i = 1; i <= m; i++) {
    read(ed[i].u), read(ed[i].v), read(ed[i].w);
    ed[i].id = i; 
  }
  sort(ed + 1, ed + 1 + m);
  for (int i = 1; i <= m; i++) 
    rk[ed[i].id] = i; 
  read(q);
  tot = 0;
  for (int i = 1; i <= q; i++) {
    read(op[++tot].t), read(op[tot].x), read(op[tot].y);
    op[tot].id = i; 
    if (tot == 500) {
      solve();
      tot = 0;
    }
  }
  if (tot) 
    solve();
  for (int i = 1; i <= q; i++) 
    if (ans[i]) 
      writeln(ans[i]);
  return 0;
}

后言

  • 当操作复杂,且不容易优化的时候,可以尝试用分块来降低复杂度。
  • 还是强调一定要一步步分析问题,也要通过题目的性质来看出题目的模型。
上一篇:CRT8.0字体配色方案设置


下一篇:P1032 字串变换