「题解」CF487E Tourists

Time Limit: 2s | Memory Limit: 256MB | 题目链接

#题意简述

给定一张 \(n(n\leq10^5)\) 个点 \(m(m\leq10^5)\) 条边的无向图,每个点有权值 \(w_i(w_i\leq10^9)\),有 \(q(q\leq10^5)\) 次操作,分为以下两种:

  • 修改一个点的点权;
  • 询问 \(x\)\(y\) 的所有简单路径中可能经过的最小的点权;

#大体思路

显然所经过的路径中至少可以同时经过两个点的边双连通分量中的所有点都可以被经过,于是考虑用圆方树将原图转化为一棵树,根据圆方树的性质:没有相邻的两个圆点或两个方点,于是在圆方树上两个圆点之间的简单路径中的每一个方点都至少会有两个相邻的圆点被经过,于是这个方点所代表的双连通分量一定可以被包含进路径。

我们考虑将每个方点的点权定义为所代表的边双连通分量中的点权最小值,注意到由于一个点可能被包含于多个边双连通分量,这样修改时不好维护方点的点权,考虑退而求其次,将方点的点权定义为子树中相邻圆点的最小点权,于是每个圆点有恰好有一个指定的方点,可以用 std::multiset 维护每个方点的子树中的相邻圆点的权值,修改时直接取出修改即可。

考虑查询,在转化为圆方树上的问题后,我们所求的就是树上的一条链上的最小点权,是树链剖分的简单应用;不过需要注意,根据我们上面方点的点权定义,当两点的 LCA 为方点时,方点上方的圆点的点权未被考虑,需要特判。

#时间复杂度分析

由于每条边最多被遍历一次,于是建立圆方树的时间复杂度为 \(O(m)\),同时每个圆点(建立时)最多被加入一次 std::multiset,这一步的总体时间复杂度为 \(O(n\log n)\)

考虑建立的方点数量是 \(O(n)\) 级别的,于是树剖的两次 DFS 都是 \(O(n)\) 的时间复杂度,由于线段树的节点数为 \(O(n)\) 级别,建立线段树时每个节点最多访问一次,于是时间复杂度可以做到 \(O(n)\),这里采用的是统一将所有点权插入线段树,于是时间复杂度为 \(O(n\log n)\).

最后分析每次操作。修改时,std::multiset 的查询、删除、插入都是 \(O(\log n)\),线段树上修改也是 \(O(\log n)\);查询时,重链的数量为 \(O(\log n)\) 级别,每次线段树区间查询为 \(O(\log n)\),于是总体最差时间复杂度为 \(O(q\log^2n)\).

综上所述,总体时间复杂度可以做到 \(O(m+n+q\log^2n)\),下面的代码的时间复杂度为 \(O(m+n\log n+q\log^2n)\),由于差距的部分不是复杂度瓶颈,所以无伤大雅。

#Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
const int N = 400010;
const int INF = 0x3fffffff;

template <typename T> inline void read(T &x) {
x = 0; int f = 1; char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}

template <typename T> inline T Max(T a, T b) {return a > b ? a : b;}
template <typename T> inline T Min(T a, T b) {return a < b ? a : b;}

struct Edge {int u, v, nxt;} ge[N], te[N];
struct Node {int ls, rs, val;};
struct SegmentTree {
Node p[N]; int cnt, rt;

inline SegmentTree() {rt = cnt = 0;}
inline void pushup(int k) {
p[k].val = Min(p[p[k].ls].val, p[p[k].rs].val);
}

void insert(int &k, int l, int r, int x, int c) {
if (!k) k = ++ cnt; int mid = l + r >> 1;
if (l == r) {p[k].val = c; return;}
if (x <= mid) insert(p[k].ls, l, mid, x, c);
else insert(p[k].rs, mid + 1, r, x, c);
pushup(k);
}

int query(int k, int l, int r, int x, int y) {
if (!k) return INF; if (x <= l && r <= y) return p[k].val;
int mid = l + r >> 1, res = INF;
if (x <= mid) res = Min(res, query(p[k].ls, l, mid, x, y));
if (mid < y) res = Min(res, query(p[k].rs, mid + 1, r, x, y));
return res;
}
} t;

int gecnt(1), tecnt(1), ghead[N], thead[N], n, m, q, w[N];
int bel[N], low[N], dfn[N], top[N], id[N], bcnt, T, stk[N], stp;
int f[N], siz[N], d[N], rk[N], son[N];

multiset <int> s[N];

inline void add_edge_g(int u, int v) {
ge[gecnt].u = u, ge[gecnt].v = v;
ge[gecnt].nxt = ghead[u], ghead[u] = gecnt ++;
}

inline void add_edge_t(int u, int v) {
te[tecnt].u = u, te[tecnt].v = v;
te[tecnt].nxt = thead[u], thead[u] = tecnt ++;
}

void biconnect(int x) {
dfn[x] = low[x] = ++ T; stk[++ stp] = x;
for (int i = ghead[x]; i; i = ge[i].nxt)
if (!dfn[ge[i].v]) {
biconnect(ge[i].v);
low[x] = Min(low[x], low[ge[i].v]);
if (low[ge[i].v] == dfn[x]) {
int y; ++ bcnt;
do {
y = stk[stp --];
bel[y] = bcnt;
add_edge_t(bcnt, y);
s[bcnt].insert(w[y]);
} while (y != ge[i].v);
add_edge_t(x, bcnt);
}
} else low[x] = Min(low[x], dfn[ge[i].v]);
}

void dfs1(int x, int fa, int dep) {
f[x] = fa, siz[x] = 1, d[x] = dep;
for (int i = thead[x]; i; i = te[i].nxt) {
if (te[i].v == fa) continue;
dfs1(te[i].v, x, dep + 1);
siz[x] += siz[te[i].v];
if (siz[te[i].v] > siz[son[x]])
son[x] = te[i].v;
}
}

void dfs2(int x, int t) {
top[x] = t; id[x] = ++ T, rk[T] = x;
if (!son[x]) return; else dfs2(son[x], t);
for (int i = thead[x]; i; i = te[i].nxt)
if (te[i].v != f[x] && te[i].v != son[x])
dfs2(te[i].v, te[i].v);
}

void modify(int x, int c) {
t.insert(t.rt, 1, bcnt, id[x], c);
if (bel[x]) {
s[bel[x]].erase(s[bel[x]].find(w[x]));
s[bel[x]].insert(c);
t.insert(t.rt, 1, bcnt, id[bel[x]], *s[bel[x]].begin());
}
w[x] = c;
}

int query(int x, int y) {
int res = INF;
while (top[x] != top[y]) {
if (d[top[x]] < d[top[y]]) swap(x, y);
res = Min(res, t.query(t.rt, 1, bcnt, id[top[x]], id[x]));
x = f[top[x]];
}
if (id[x] < id[y]) swap(x, y);
res = Min(res, t.query(t.rt, 1, bcnt, id[y], id[x]));
if (y > n) res = Min(res, w[f[y]]); return res;
}

int main() {
read(n), read(m), read(q);
for (int i = 1; i <= n; ++ i) read(w[i]);
for (int i = 1; i <= m; ++ i) {
int u, v; read(u), read(v);
add_edge_g(u, v); add_edge_g(v, u);
}
bcnt = n; biconnect(1);
T = 0; dfs1(1, 0, 1); dfs2(1, 1);
for (int i = 1; i <= n; ++ i)
t.insert(t.rt, 1, bcnt, id[i], w[i]);
for (int i = n + 1; i <= bcnt; ++ i)
t.insert(t.rt, 1, bcnt, id[i], *s[i].begin());
while (q --) {
char opt; int x, y;
cin >> opt; read(x), read(y);
if (opt == 'C') modify(x, y);
else printf("%d\n", query(x, y));
}
return 0;
}

#总结

本题可以看做边双连通分量/圆方树的板子题,思维难度不大,主要是线段树、树链剖分等部分的细节较多,但整体实现较为容易。