连通性相关
连通性相关
无向图连通性
通常使用并查集维护,显然连通性具有传递性。
对于连通性判断带删除操作的,大多可以时光倒流,使删除被插入取代,方便后期并查集。
CF1556G Gates to Another World
给定一张 \(2^n\) 个点的无向图,编号为 \(0 \sim 2^n - 1\) ,\(x, y\) 之间有链边当且仅当 \(x, y\) 二进制下恰有一位不同。\(m\) 次操作,操作有:
- 删除点 \(l \sim r\) ,保证一个点不会被删两次。
- 判断 \(x, y\) 是否连通。
\(n \le 50\) ,\(m \le 5 \times 10^4\)
先时光倒流转化为加边,然后考虑如何动态维护连通性。
由于点数很多,考虑将一段区间的点视为一个整体。考虑线段树结构,对于每一个非叶子点,其左右子树的对应位置上的叶子节点互相差一个二进制位,因此可以递归对所有子树对应位置上的叶子节点之间连边。
记录一下每次连边的顺序,用并查集维护线段树上点的连通性,时间复杂度 \(O(n^2 m \times \alpha)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int M = 5e4 + 7, N = 1e7 + 7;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
inline bool check(int x, int y) {
return find(x) == find(y);
}
} dsu;
struct Node {
ll x, y;
int op;
} nd[M];
vector<pair<int, int> > e[M];
int lc[N], rc[N], tag[N], ans[M];
int n, m, tot = 1;
inline void pushdown(int x) {
if (!lc[x])
lc[x] = ++tot;
if (!rc[x])
rc[x] = ++tot;
if (tag[x])
tag[lc[x]] = tag[rc[x]] = tag[x], tag[x] = 0;
}
void update(int x, ll nl, ll nr, ll l, ll r, int k) {
if (l <= nl && nr <= r) {
tag[x] = k;
return;
}
pushdown(x);
ll mid = (nl + nr) >> 1;
if (l <= mid)
update(lc[x], nl, mid, l, r, k);
if (r > mid)
update(rc[x], mid + 1, nr, l, r, k);
}
inline bool isleaf(int x) {
return !lc[x] && !rc[x];
}
void connect(int x, int y) {
if (isleaf(x) && isleaf(y))
e[min(tag[x], tag[y])].emplace_back(x, y);
else if (isleaf(x))
connect(x, lc[y]), connect(x, rc[y]);
else if (isleaf(y))
connect(lc[x], y), connect(rc[x], y);
else
connect(lc[x], lc[y]), connect(rc[x], rc[y]);
}
int locate(int x, ll nl, ll nr, ll pos) {
if (isleaf(x))
return x;
ll mid = (nl + nr) >> 1;
return pos <= mid ? locate(lc[x], nl, mid, pos) : locate(rc[x], mid + 1, nr, pos);
}
signed main() {
scanf("%d%d", &n, &m);
ll all = (1ll << n) - 1;
tag[1] = m + 1;
for (int i = 1; i <= m; ++i) {
char str[5];
scanf("%s%lld%lld", str, &nd[i].x, &nd[i].y);
nd[i].op = (str[0] == 'a');
if (!nd[i].op)
update(1, 0, all, nd[i].x, nd[i].y, i);
}
for (int i = 1; i <= tot; ++i)
if (!isleaf(i))
connect(lc[i], rc[i]);
dsu.prework(tot);
for (auto it : e[m + 1])
dsu.merge(it.first, it.second);
for (int i = m; i; --i) {
for (auto it : e[i])
dsu.merge(it.first, it.second);
if (nd[i].op)
ans[i] = dsu.check(locate(1, 0, all, nd[i].x), locate(1, 0, all, nd[i].y));
}
for (int i = 1; i <= m; ++i)
if (nd[i].op)
printf("%d\n", ans[i]);
return 0;
}
CF1307F Cow and Vacation
给定一棵树,有 \(r\) 个点为关键点。\(m\) 次询问 \(x, y\) 之间是否存在合法路径,一条路径合法当且仅当极长不经过关键点的子路径边数 \(\le k\) 。
\(n, m \le 2 \times 10^5\)
先判掉距离 \(\leq k\) 的情况,则合法路径上一定存在关键点。
先考虑两个点都是关键点的情况,相当于一次只能走到距离不超过 \(k\) 的点。由于本题是双向可达的,所以可以将可达的点用并查集维护。具体的以每个关键点为起点跑 \(dis \leq \frac{k}{2}\) 的 bfs,然后将这些范围内的点合并即可,拆边为点即可处理 \(k\) 是奇数的情况。
对于一般情况,考虑两端点移动到距离 \(\leq k\) 的关键点上,然后就转化为上一种情况。具体判断就是两端先走 \(k\) 步,然后判定是否在同一连通块即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 7, LOGN = 19;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
vector<int> kp;
int fa[N][LOGN], dep[N], dis[N];
int n, k, q, r;
void dfs(int u, int f) {
fa[u][0] = f, dep[u] = dep[f] + 1;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : G.e[u])
if (v != f)
dfs(v, u);
}
inline int jump(int x, int h) {
for (int i = 0; h; ++i, h >>= 1)
if (h & 1)
x = fa[x][i];
return x;
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
x = jump(x, dep[x] - dep[y]);
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
inline void bfs() {
memset(dis + 1, -1, sizeof(int) * (n * 2 - 1));
queue<int> q;
for (int it : kp)
dis[it] = 0, q.emplace(it);
while (!q.empty() && dis[q.front()] < k) {
int u = q.front();
q.pop();
for (int v : G.e[u]) {
dsu.merge(u, v);
if (dis[v] == -1)
dis[v] = dis[u] + 1, q.emplace(v);
}
}
}
signed main() {
scanf("%d%d%d", &n, &k, &r), kp.resize(r);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, n + i), G.insert(n + i, u);
G.insert(v, n + i), G.insert(n + i, v);
}
for (int &it : kp)
scanf("%d", &it);
dfs(1, 0), dsu.prework(n * 2 - 1), bfs();
scanf("%d", &q);
while (q--) {
int x, y;
scanf("%d%d", &x, &y);
int lca = LCA(x, y), d = dep[x] + dep[y] - dep[lca] * 2;
puts(d <= k * 2 || dsu.find(dep[x] - dep[lca] >= k ? jump(x, k) : jump(y, d - k)) ==
dsu.find(dep[y] - dep[lca] >= k ? jump(y, k) : jump(x, d - k)) ? "YES" : "NO");
}
return 0;
}
强连通分量
有向强连通图:任意两个点可以互相到达的有向图。
强连通分量(SCC):极大的强连通子图。
Tarjan 算法
维护一个栈存储搜索到的还未确定强连通分量的点,定义:
- \(dfn_u\) :点 \(u\) 被搜索到的次序。
- \(low_u\) :\(u\) 子树中能够回溯到的最早的已经在栈中的点。
对于 \(u\) 的出点 \(v\) ,分类讨论:
-
\(v\) 未被访问过:继续 dfs ,并用 \(low_v\) 更新 \(low_u\) ,这是因为 \(v\) 可以回溯到的栈中点 \(u\) 一定可以回溯到。
-
\(v\) 被访问过:
-
已在栈中:根据 \(low\) 的定义,用 \(dfn_v\) 更新 \(low_u\) 。
-
不在栈中:说明 \(v\) 已搜索完毕,无需处理。
-
对于一个 SCC,显然只有一个 \(u\) 满足 \(dfn_u = low_u\) ,其一定是这个 SCC 的根(dfs 树上的最浅点)。因此回溯过程中,若 \(dfn_u = low_u\) ,则出现一个 SCC。
时间复杂度 \(O(n + m)\) 。
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v])
Tarjan(v), low[u] = min(low[u], low[v]);
else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++scc;
while (sta[top] != u)
leader[sta[top--]] = scc;
leader[sta[top--]] = scc;
}
}
Kosaraju 算法
由两次 dfs 组成:
- 第一次 dfs:遍历所有点,并在回溯时入栈。
- 第二次 dfs:在反图上依次从栈顶开始 dfs ,此时遍历到的点集就是一个 SCC。
时间复杂度 \(O(n + m)\) ,可以用 bitset 优化做到 \(O(\frac{n^2}{\omega})\) 。
void dfs1(int u) {
vis[u] = true;
for (int v : G.e[u])
if (!vis[v])
dfs1(v);
sta[++top] = u;
}
void dfs2(int u) {
leader[u] = scc;
for (int v : rG.e[u])
if (!leader[v])
dfs2(v);
}
inline void kosaraju() {
for (int i = 1; i <= n; ++i)
if (!vis[i])
dfs1(i);
for (; top; --top)
if (!leader[sta[top]])
++scc, dfs(sta[top]);
}
应用
CF1515G Phoenix and Odometers
给定一张带边权的有向图,\(q\) 次询问,每次给出 \(x, s, t\) ,判定是否存在一条经过 \(x\) 的回路满足长度与 \(-s\) 在模 \(t\) 意义下同余。
\(n, m, q \le 2 \times 10^5\)
首先不难发现每个 SCC 的答案是一致的,且不同 SCC 之间相互独立,故考虑对于每个 SCC 分开计算。
假设经过 \(u\) 有两个长度为 \(a\) 和 \(b\) 的环,那么就相当于找两个非负整数 \(x\) 和 \(y\),使得 \(ax + by = w\),其中 \(w\) 为题中的路径长,根据裴蜀定理得到上述方程成立当且仅当 \(\gcd(a, b) \mid w\) 。
考虑如何求出经过点 \(u\) 的所有环长度的 \(\gcd\) 。由于所有的非树边 \(u \to v\) 的贡献都是 \(dis_u + w - dis_v\) ,于是搜索时顺便记录贡献即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
ll dis[N], g[N];
int dfn[N], low[N], sta[N], leader[N];
bool vis[N];
int n, m, q, dfstime, top, scc;
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (auto it : G.e[u]) {
int v = it.first;
if (!dfn[v])
Tarjan(v), low[u] = min(low[u], low[v]);
else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++scc;
while (sta[top] != u)
leader[sta[top--]] = scc;
leader[sta[top--]] = scc;
}
}
void dfs(int u, int id) {
vis[u] = true;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (leader[v] != id)
continue;
if (!vis[v])
dis[v] = dis[u] + w, dfs(v, id);
else
g[id] = __gcd(g[id], abs(dis[u] - dis[v] + w));
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
G.insert(u, v, w);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
Tarjan(i);
for (int i = 1; i <= n; ++i)
if (!vis[i])
dfs(i, leader[i]);
scanf("%d", &q);
while (q--) {
int x, s, t;
scanf("%d%d%d", &x, &s, &t);
puts((g[leader[x]] ? s % gcd(g[leader[x]], t) : s) ? "NO" : "YES");
}
return 0;
}
CF1361E James and the Chase
给定一张有向强连通图。一个点是好的当且仅当它到其它点有且只有一条简单路径。如果好的点至少有 \(20\%\) ,则输出所有好的点, 否则输出 \(-1\) 。
\(\sum n \le 10^5\) ,\(\sum m \le 2 \times 10^5\)
考虑如何判定 \(u\) 是好的,只要以 \(u\) 为根建出 dfs 树,若无横叉边或前向边则 \(u\) 即为好的。于是可以做到 \(O(n)\) 的判定。
考虑确定一个好点 \(u\) 后求出其余好点。
确定一个好点考虑随机化,随机选取一定数量的点进行上述算法流程,若均不满足条件则输出 \(-1\) 。
下面考虑在 \(u\) 为好点的情况下求出所有好点。以 \(u\) 为根建立 dfs 树,考虑某个 \(v\) 的子树,由于整个图的强连通性,\(v\) 的子树中有连向其祖先的返祖边。不难发现这样的边有且仅有一条,否则 \(v\) 有两条路径可以到 \(fa_v\) 或者根本无法到达 \(fa_v\) ,此时 \(v\) 就不是好点。
记录所有 \(v\) 子树内返祖到根的祖先的边的数量,若存在恰好一条返祖边就顺便记录其指向的点。假设 \(v\) 的子树这条返祖边指向了 \(w\),那么 \(v\) 是好点,当且仅当 \(w\) 是好点。
由此可以得到判定条件:一个点 \(v\) 是好点,当且仅当 \(v\) 的子树内有且仅有一条连向 \(v\) 的祖先的返祖边,并且这条边所连向的点是好点。
树上差分即可做到线性,注意最后求出好点后还需判断占比是否 \(\ge 20 \%\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G, T;
int cnt[N], upto[N], dep[N];
bool vis[N], in[N], ans[N];
mt19937 myrand(time(0));
int n, m;
bool dfs1(int u) {
vis[u] = in[u] = true;
for (int v : G.e[u]) {
if (vis[v] && !in[v])
return false;
else if (!vis[v]) {
dep[v] = dep[u] + 1, T.insert(u, v);
if (!dfs1(v))
return false;
} else {
++cnt[u], --cnt[v];
if (dep[v] < dep[upto[u]])
upto[u] = v;
}
}
return in[u] = false, true;
}
inline bool check(int u) {
memset(cnt + 1, 0, sizeof(int) * n);
memset(upto + 1, 0, sizeof(int) * n);
memset(vis + 1, false, sizeof(bool) * n);
memset(in + 1, false, sizeof(bool) * n);
T.clear(n), dep[0] = n + 1, dep[u] = 1;
return dfs1(u);
}
inline void dfs2(int u) {
for (int v : T.e[u]) {
dfs2(v), cnt[u] += cnt[v];
if (dep[upto[v]] < dep[upto[u]])
upto[u] = upto[v];
}
}
void dfs3(int u) {
if(cnt[u] == 1 && dep[upto[u]] < dep[u] && ans[upto[u]])
ans[u] = true;
for (int v : T.e[u])
dfs3(v);
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m), G.clear(n);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v);
}
vector<int> id(n);
iota(id.begin(), id.end(), 1), shuffle(id.begin(), id.end(), myrand);
int root = -1;
for (int i = 0; i < min(n, 100); ++i)
if (check(id[i])) {
root = id[i];
break;
}
if (root == -1) {
puts("-1");
continue;
}
memset(ans + 1, false, sizeof(bool) * n);
dfs2(root), ans[root] = true, dfs3(root);
if (count(ans + 1, ans + 1 + n, true) * 5 < n)
puts("-1");
else {
for (int i = 1; i <= n; ++i)
if (ans[i])
printf("%d ", i);
puts("");
}
}
return 0;
}
BZOJ5218 省队十连测 友好城市
给出一张有向图,\(q\) 次询问仅保留编号属于 \([l_i, r_i]\) 的边时有多少无序对城市满足可以两两到达。
\(n \le 150\) ,\(m \le 3 \times 10^5\) ,\(q \le 5 \times 10^4\)
注意到 \(n\) 很小,使用 Kosaraju 配合莫队即可,时间复杂度 \(O(\frac{q n^2}{\omega} + q \sqrt m)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1.5e2 + 7, M = 3e5 + 7, Q = 5e4 + 7;
struct Edge {
int u, v;
} E[M];
struct Query {
int l, r, *ans, bid;
inline bool operator < (const Query &rhs) const {
return bid == rhs.bid ? (bid & 1 ? r < rhs.r : r > rhs.r) : bid < rhs.bid;
}
} qry[Q];
bitset<N> e1[N], e2[N];
bitset<N> vis;
int cnt1[N][N], cnt2[N][N], ans[Q], sta[N];
int n, m, q, block, top, scc;
inline void Add(int x) {
int u = E[x].u, v = E[x].v;
if (!cnt1[u][v])
e1[u][v] = true;
if (!cnt2[v][u])
e2[v][u] = true;
++cnt1[u][v], ++cnt2[v][u];
}
inline void Del(int x) {
int u = E[x].u, v = E[x].v;
--cnt1[u][v], --cnt2[v][u];
if (!cnt1[u][v])
e1[u][v] = false;
if (!cnt2[v][u])
e2[v][u] = false;
}
void dfs1(int u) {
vis.set(u);
bitset<N> now = ~vis & e1[u];
while (now.any())
dfs1(now._Find_first()), now &= ~vis;
sta[++top] = u;
}
int dfs2(int u) {
vis.set(u);
bitset<N> now = ~vis & e2[u];
int siz = 1;
while (now.any())
siz += dfs2(now._Find_first()), now &= ~vis;
return siz;
}
inline int kosaraju() {
vis.reset(), scc = 0;
int res = 0;
for (int i = 1; i <= n; ++i)
if (!vis.test(i))
dfs1(i);
vis.reset();
for (; top; --top)
if (!vis.test(sta[top])) {
int siz = dfs2(sta[top]);
res += siz * (siz - 1) / 2;
}
return res;
}
signed main() {
scanf("%d%d%d", &n, &m, &q), block = sqrt(m);
for (int i = 1; i <= m; ++i)
scanf("%d%d", &E[i].u, &E[i].v);
for (int i = 1; i <= q; ++i)
scanf("%d%d", &qry[i].l, &qry[i].r), qry[i].bid = qry[i].l / block, qry[i].ans = ans + i;
sort(qry + 1, qry + 1 + q);
for (int i = 1, l = 1, r = 0; i <= q; ++i) {
while (l > qry[i].l)
Add(--l);
while (r < qry[i].r)
Add(++r);
while (l < qry[i].l)
Del(l++);
while (r > qry[i].r)
Del(r--);
*qry[i].ans = kosaraju();
}
for (int i = 1; i <= q; ++i)
printf("%d\n", ans[i]);
return 0;
}
P5163 WD与地图
给定一张 \(n\) 个点 \(m\) 条边的有向图,每个点带点权。\(q\) 次操作,操作有:
- 删除一条边。
- 单点加点权。
- 求某点所在 SCC 的点权前 \(k\) 大值和,若不足 \(k\) 个则取全部。
\(n \le 10^5\) ,\(m, q \le 2 \times 10^5\)
首先时光倒流将删边转化为加边,则需要动态维护 SCC。考虑离线求出每条边合并 SCC 的时间,这样就可以用并查集维护 SCC,线段树合并处理询问。
考虑整体二分,假设目前已知 \([L, R]\) 内的边合并 SCC 的时间 \(\in [l, r]\) ,初始时每条边的时间 \(t\) 设为加入时间。记 \(mid = \lfloor \frac{l + r}{2} \rfloor\) ,拿出所有 \(t \le mid\) 的边跑一边 Tarjan,此时两端不在同一 SCC 的边均有 \(t > mid\) ,由此可以递归处理。实现时有一些细节:
- 跑 Tarjan 时不能遍历孤立点,否则时间复杂度不对。
- 递归 \([mid + 1, r]\) 时可以把原先在同一 SCC 的点缩起来。
时间复杂度 \(O(q \log m + m \log q + q \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7, M = 2e5 + 7;
struct Edge {
int u, v, t;
} e[M], el[M], er[M];
struct Node {
int op, x, y;
} nd[M];
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
map<pair<int, int>, int> mp;
vector<int> vec;
ll ans[M];
int val[N], dfn[N], low[N], sta[N], leader[N];
int n, m, q, dfstime, top;
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v])
Tarjan(v), low[u] = min(low[u], low[v]);
else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
while (sta[top] != u)
leader[sta[top--]] = u;
leader[sta[top--]] = u;
}
}
void solve(int l, int r, int L, int R) {
if (L > R || l == r)
return;
int mid = (l + r) >> 1;
for (int i = L; i <= R; ++i)
if (e[i].t <= mid)
vec.emplace_back(e[i].u), vec.emplace_back(e[i].v), G.insert(e[i].u, e[i].v);
for (int it : vec)
if (!dfn[it])
Tarjan(it);
int tl = 0, tr = 0;
for (int i = L; i <= R; ++i) {
if (e[i].t <= mid && (!leader[e[i].u] || !leader[e[i].v] || leader[e[i].u] != leader[e[i].v]))
e[i].t = mid + 1;
if (e[i].t <= mid)
el[++tl] = e[i];
else {
if (leader[e[i].u])
e[i].u = leader[e[i].u];
if (leader[e[i].v])
e[i].v = leader[e[i].v];
er[++tr] = e[i];
}
}
memcpy(e + L, el + 1, sizeof(Edge) * tl), memcpy(e + L + tl, er + 1, sizeof(Edge) * tr);
for (int it : vec)
dfn[it] = low[it] = leader[it] = 0, G.e[it].clear();
dfstime = 0, vec.clear();
solve(l, mid, L, L + tl - 1), solve(mid + 1, r, L + tl, R);
}
namespace SMT {
const int S = 3e7 + 7;
ll s[S];
int rt[N], lc[S], rc[S], cnt[S];
int tot;
void update(int &x, int nl, int nr, int p, int k) {
if (!x)
x = ++tot;
cnt[x] += k, s[x] += 1ll * p * k;
if (nl == nr)
return;
int mid = (nl + nr) >> 1;
if (p <= mid)
update(lc[x], nl, mid, p, k);
else
update(rc[x], mid + 1, nr, p, k);
}
int merge(int a, int b, int l, int r) {
if (!a || !b)
return a | b;
cnt[a] += cnt[b], s[a] += s[b];
if (l == r)
return a;
int mid = (l + r) >> 1;
lc[a] = merge(lc[a], lc[b], l, mid);
rc[a] = merge(rc[a], rc[b], mid + 1, r);
return a;
}
ll query(int x, int nl, int nr, int k) {
if (!x)
return 0;
if (nl == nr)
return 1ll * nl * min(cnt[x], k);
int mid = (nl + nr) >> 1;
return cnt[rc[x]] <= k ? s[rc[x]] + query(lc[x], nl, mid, k - cnt[rc[x]]) : query(rc[x], mid + 1, nr, k);
}
} // namespace SMT
signed main() {
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; ++i)
scanf("%d", val + i);
for (int i = 1; i <= m; ++i)
scanf("%d%d", &e[i].u, &e[i].v), mp[make_pair(e[i].u, e[i].v)] = i;
for (int i = q; i; --i) {
scanf("%d%d%d", &nd[i].op, &nd[i].x, &nd[i].y);
if (nd[i].op == 1)
e[mp[make_pair(nd[i].x, nd[i].y)]].t = i;
else if (nd[i].op == 2)
val[nd[i].x] += nd[i].y;
}
solve(0, q, 1, m), dsu.prework(n);
for (int i = 1; i <= n; ++i)
SMT::update(SMT::rt[i], 1, 1e9, val[i], 1);
for (int i = 0, j = 1; i <= q; ++i) {
if (i) {
int u = dsu.find(nd[i].x);
if (nd[i].op == 2) {
SMT::update(SMT::rt[u], 1, 1e9, val[nd[i].x], -1);
SMT::update(SMT::rt[u], 1, 1e9, val[nd[i].x] -= nd[i].y, 1);
} else if (nd[i].op == 3)
ans[i] = SMT::query(SMT::rt[u], 1, 1e9, nd[i].y);
}
for (; j <= m && e[j].t == i; ++j) {
int fx = dsu.find(e[j].u), fy = dsu.find(e[j].v);
if (fx != fy)
dsu.merge(fx, fy), SMT::rt[fx] = SMT::merge(SMT::rt[fx], SMT::rt[fy], 1, 1e9);
}
}
for (int i = q; i; --i)
if (nd[i].op == 3)
printf("%lld\n", ans[i]);
return 0;
}
[ARC092F] Two Faced Edges
给定一张有向图,对每条边判定将其反向后 SCC 的数量是否变化。
\(n \leq 10^3\) ,\(m \leq 2 \times 10^5\)
对于一条边 \(u \to v\) ,反向后 SCC 的数量变化当且仅当以下两个条件恰好满足其一:
- 删去 \(u \to v\) 的边后 \(u\) 能到达 \(v\) 。
- 删去 \(u \to v\) 的边后 \(v\) 能到达 \(u\) 。
后者删去与否没有区别,因此可以直接 dfs 判定。
对于前者,考虑把所有 \(u\) 相同的边一起处理,记 \(u\) 的出点为 \(v_{1 \sim k}\) 。考虑按 \(v_{1, 2, \cdots, k}\) 的顺序 dfs 一遍,记录每个点 \(w\) 是被哪个 \(v\) 搜到的,记为 \(p(w)\) 。然后再按 \(v_{k, k - 1, \cdots, 1}\) 的顺序 dfs 一遍,记录每个点 \(w\) 是被哪个 \(v\) 搜到的,记为 \(q(w)\) 。则对于一个 \(v_i\) ,若 \(p(v_i) = q(v_i)\) ,则说明 \(v_i\) 无法被其他的 \(v\) 走到,因此删去 \(u \to v_i\) 后 \(u\) 无法到达 \(v\) 。
dfs 用 bitset 优化,时间复杂度 \(O(\frac{n^3}{\omega} + m)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7, M = 2e5 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
bitset<N> e[N], reach[N], vis;
int p[N], q[N];
bool ans[M];
int n, m;
void dfs1(int u) {
vis.set(u);
while ((e[u] & ~vis).any())
dfs1((e[u] & ~vis)._Find_first());
}
void dfs2(int u, int *p, int r) {
vis.set(u), p[u] = r;
while ((e[u] & ~vis).any())
dfs2((e[u] & ~vis)._Find_first(), p, r);
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v, i), e[u].set(v);
}
for (int i = 1; i <= n; ++i)
vis.reset(), dfs1(i), reach[i] = vis;
for (int u = 1; u <= n; ++u) {
vis.reset(), vis.set(u);
for (auto it : G.e[u])
if (!vis.test(it.first))
dfs2(it.first, p, it.first);
reverse(G.e[u].begin(), G.e[u].end());
vis.reset(), vis.set(u);
for (auto it : G.e[u])
if (!vis.test(it.first))
dfs2(it.first, q, it.first);
for (auto it : G.e[u])
ans[it.second] = reach[it.first].test(u) ^ (p[it.first] != q[it.first]);
}
for (int i = 1; i <= m; ++i)
puts(ans[i] ? "diff" : "same");
return 0;
}
2-SAT
2-SAT 问题:给定一串布尔变量,每个变量只能为真或假。要求对这些变量进行赋值,满足布尔方程。
考虑建立图论模型:
- 点的状态:将点 \(u\) 拆分成 \(u0,u1\) 两个点,分别表示 \(u\) 点为假、真。
- 边的状态:若连的边为 \(u \to v\) ,就表示选择 \(u\) 就必须选 \(v\) 。
- 注意连边不能漏连,每个条件的逆否命题也要连边。
由所构造的状态可知,对于图中的每一个强连通分量,如果选择了其中任意一个点,那就意味着这个强连通分量中的所有点都要选。显然 \(x0,x1\) 不可以同时选,由此可判断有无解。
构造方案时,对于每个点的两种状态,选择拓扑序大的,舍弃掉另一个,这样可以尽可能不冲突。如果要求字典序最小,就 dfs 枚举点 \(1 \to 2n\) ,贪心选取即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int dfn[N], low[N], sta[N], leader[N];
int n, m, dfstime, top, scc;
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v])
Tarjan(v), low[u] = min(low[u], low[v]);
else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++scc;
while (sta[top] != u)
leader[sta[top--]] = scc;
leader[sta[top--]] = scc;
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, x, v, y;
scanf("%d%d%d%d", &u, &x, &v, &y);
G.insert(u + (x ^ 1) * n, v + y * n);
G.insert(v + (y ^ 1) * n, u + x * n);
}
for (int i = 1; i <= n * 2; ++i)
if (!dfn[i])
Tarjan(i);
for (int i = 1; i <= n; ++i)
if (leader[i] == leader[i + n])
return puts("IMPOSSIBLE"), 0;
puts("POSSIBLE");
for (int i = 1; i <= n; ++i)
printf("%d ", leader[i + n] < leader[i]);
return 0;
}
P3825 [NOI2017] 游戏
给定一串由
A、B、C、X组成的序列,其中X有 \(d\) 个。A、B、C分别表示该位置不能填的字母,X表示三个字母均可以填。给出 \(m\) 条限制,形如 \(i\) 处填 \(x\) 则 \(j\) 处必须填 \(y\) ,构造一组合法填入
A、B、C的方案。\(n \le 5 \times 10^4\) ,\(m \le 10^5\) ,\(d \le 8\)
暴力枚举每个 \(x\) 地图不填 \(A\) 或不填 \(B\) 。因为不填 \(A\) 就可以填 \(B, C\) ,不填 \(B\) 就可以填 \(A, C\) ,这样就包含了 \(A, B, C\) 三种赛车。
时间复杂度降为 \(O((n+m) \times 2^d)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct Node {
int x, y;
char cx, cy;
} nd[N];
int dfn[N], low[N], leader[N], sta[N];
char str[N];
int n, d, m, dfstime, top, scc;
inline int trans(int x) {
return x <= n ? x + n : x - n;
}
inline int getid(int x, char op) {
if (str[x] == 'a')
return op == 'b' ? x : x + n;
else
return op == 'a' ? x : x + n;
}
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v])
Tarjan(v), low[u] = min(low[u], low[v]);
else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u]) {
++scc;
while (sta[top] != u)
leader[sta[top--]] = scc;
leader[sta[top--]] = scc;
}
}
inline bool solve() {
memset(dfn + 1, 0, sizeof(int) * (n * 2));
memset(low + 1, 0, sizeof(int) * (n * 2));
memset(leader + 1, 0, sizeof(int) * (n * 2));
G.clear(n * 2), dfstime = scc = 0;
for (int i = 1; i <= m; ++i) {
if (str[nd[i].x] == nd[i].cx)
continue;
int x = getid(nd[i].x, nd[i].cx), y = getid(nd[i].y, nd[i].cy);
if (str[nd[i].y] == nd[i].cy)
G.insert(x, trans(x));
else
G.insert(x, y), G.insert(trans(y), trans(x));
}
for (int i = 1; i <= n * 2; ++i)
if (!dfn[i])
Tarjan(i);
for (int i = 1; i <= n; ++i)
if (leader[i] == leader[i + n])
return false;
return true;
}
bool dfs(int pos) {
if (pos > n)
return solve();
else if (str[pos] != 'x')
return dfs(pos + 1);
for (int i = 0; i < 2; ++i) {
str[pos] = 'a' + i;
if (dfs(pos + 1))
return true;
}
return str[pos] = 'x', false;
}
signed main() {
cin >> n >> d >> (str + 1) >> m;
for (int i = 1; i <= m; ++i) {
cin >> nd[i].x >> nd[i].cx >> nd[i].y >> nd[i].cy;
nd[i].cx = tolower(nd[i].cx), nd[i].cy = tolower(nd[i].cy);
}
if (!dfs(1))
return cout << -1, 0;
for (int i = 1; i <= n; ++i) {
if (str[i] == 'a')
cout << (leader[i] < leader[i + n] ? 'B' : 'C');
else if (str[i] == 'b')
cout << (leader[i] < leader[i + n] ? 'A' : 'C');
else
cout << (leader[i] < leader[i + n] ? 'A' : 'B');
}
return 0;
}
[ARC161E] Not Dyed by Majority (Cubic Graph)
给出每个点的度数恰为 \(3\) 的无向图。一次操作为将每个点的颜色变为所有邻居颜色的众数。构造黑白颜色序列使得无论如何染色,恰好一次操作后都不可能变为该颜色序列。
\(\sum n \le 5 \times 10^4\) ,保证 \(n\) 为偶数
考虑如何判定一个颜色序列是否可作为操作后的颜色序列。
设操作前的颜色序列为 \(a_i\)(不妨使 \(a_i = 0, 1\) 代表黑和白),操作后的颜色序列为 \(b_i\) 。对于点 \(i\),\(e_{i, j}\) 为与 \(i\) 相邻的三个点。
对每个 \(i\) 有两种情况:
- \(b_i = 0\) :则对于 \(j \not = k\),若 \(a_{e_{i, j}} = 1\) 则 \(a_{e_{i, k}} = 0\) 。
- \(b_i = 1\) :则对于 \(j\ne k\),若 \(a_{e_{i, j}} = 0\) 则 \(a_{e_{i, k}} = 1\) 。
这是一个关于 \(a_i\),有 \(6n\) 个条件的 2-SAT 问题,可以 \(O(n)\) 解决,目标转化为找到一个方案使得 2-SAT 问题无解。
考虑 \(1\) 的邻域 \(x,y,z\),以及他们的邻域 \(\{ 1, x_0, x_1 \}, \{ 1, y_0, y_1 \}, \{1, z_0, z_1\}\) ,若 \(d_{x_0}=d_{x_1},d_{y_0}=d_{y_1},d_{z_0}=d_{z_1}\) ,那么无论 \(d_1\) 是什么,答案都不会更改。因此至少有 \(\frac 1{16}\) 的序列会互相重复,那么期望 \(O(1)\) 次随机后能得到一组解。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G, nG;
int col[N], dfn[N], low[N], sta[N], leader[N];
mt19937 myrand(time(0));
int n, dfstime, scc, top;
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : nG.e[u]) {
if (!dfn[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++scc;
while (sta[top] != u)
leader[sta[top--]] = scc;
leader[sta[top--]] = scc;
}
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d", &n), G.clear(n);
for (int i = 1; i <= n / 2 * 3; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
for (;;) {
nG.clear(n * 2);
for (int u = 1; u <= n; ++u) {
col[u] = myrand() & 1;
for (int v : G.e[u])
for (int w : G.e[u])
if (v != w) {
if (col[u])
nG.insert(v, w + n);
else
nG.insert(v + n, w);
}
}
memset(dfn + 1, 0, sizeof(int) * (n * 2));
memset(low + 1, 0, sizeof(int) * (n * 2));
memset(leader + 1, 0, sizeof(int) * (n * 2));
dfstime = scc = 0;
for (int i = 1; i <= n * 2; ++i)
if (!dfn[i])
Tarjan(i);
bool flag = false;
for (int i = 1; i <= n; ++i)
if (leader[i] == leader[i + n]) {
flag = true;
break;
}
if (flag)
break;
}
for (int i = 1; i <= n; ++i)
putchar(col[i] ? 'B' : 'W');
puts("");
}
return 0;
}
QOJ4892. 序列
有一个长为 \(n\) 的序列 \(a_{1 \sim n}\) ,其中 \(a_i \in [1, 10^9]\) 。
给出 \(m\) 条信息,每条信息形如 \(a_x, a_y, a_z\) 的中位数为 \(k\) 。
构造一个合法的 \(a_{1 \sim n}\) 或报告无解。
\(n \le 10^5\)
记 \(S(x)\) 表示 \(x\) 的候选集合,每条信息都将 \(k\) 和 \(k - 1\) 加入 \(x, y, z\) 的候选集合,则一定存在一组解满足每个值均为候选集合中的值,若候选集和为空则任意。
对于一组约束 \((i, j, k, x)\) ,其中一个 \(< x\) 则剩下两个必须 \(\ge x\) ,若其中一个 \(> x\) 则剩下两个必须 \(\le x\) 。
考虑建图,记 \((i, k) = [a_i \ge k]\) ,则连边除了每条信息的约束,对于同一个 \(i\) 的候选集合还需连一条链,表示大小关系的传递。
时间复杂度 \(O(n + m)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct Node {
int a[3], k;
} nd[N];
vector<int> a[N];
int sum[N], dfn[N], low[N], sta[N], leader[N], ans[N];
int n, m, dfstime, top, scc;
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v])
Tarjan(v), low[u] = min(low[u], low[v]);
else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++scc;
while (sta[top] != u)
leader[sta[top--]] = scc;
leader[sta[top--]] = scc;
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
scanf("%d%d%d%d", nd[i].a + 0, nd[i].a + 1, nd[i].a + 2, &nd[i].k);
for (int j = 0; j < 3; ++j)
a[nd[i].a[j]].emplace_back(nd[i].k - 1), a[nd[i].a[j]].emplace_back(nd[i].k);
}
for (int i = 1; i <= n; ++i) {
sort(a[i].begin(), a[i].end());
a[i].erase(unique(a[i].begin(), a[i].end()), a[i].end());
sum[i] = sum[i - 1] + a[i].size();
}
for (int i = 1; i <= m; ++i) {
int b[3][2];
for (int j = 0; j < 3; ++j) {
int x = nd[i].a[j];
b[j][0] = sum[x - 1] + lower_bound(a[x].begin(), a[x].end(), nd[i].k - 1) - a[x].begin() + 1;
b[j][1] = sum[x - 1] + lower_bound(a[x].begin(), a[x].end(), nd[i].k) - a[x].begin() + 1;
}
for (int j = 0; j < 3; ++j)
for (int k = 0; k < 3; ++k)
if (j != k)
G.insert(b[j][0] + sum[n], b[k][0]), G.insert(b[j][1], b[k][1] + sum[n]);
}
for (int i = 1; i <= n; ++i)
for (int j = 0; j + 1 < a[i].size(); ++j) {
G.insert(sum[i - 1] + j + 2, sum[i - 1] + j + 1);
G.insert(sum[i - 1] + j + 1 + sum[n], sum[i - 1] + j + 2 + sum[n]);
}
for (int i = 1; i <= sum[n] * 2; ++i)
if (!dfn[i])
Tarjan(i);
for (int i = 1; i <= sum[n]; ++i)
if (leader[i] == leader[i + sum[n]])
return puts("NO"), 0;
memset(ans + 1, -1, sizeof(int) * n);
for (int i = 1; i <= n; ++i) {
if (a[i].empty()) {
ans[i] = 1;
continue;
}
for (int j = 0; j < a[i].size(); ++j)
if (leader[sum[i - 1] + j + 1] > leader[sum[i - 1] + j + 1 + sum[n]]) {
ans[i] = a[i][j];
break;
}
if (ans[i] == -1)
ans[i] = a[i].back() + 1;
if (ans[i] < 1 || ans[i] > 1e9)
return puts("NO"), 0;
}
puts("YES");
for (int i = 1; i <= n; ++i)
printf("%d ", ans[i]);
return 0;
}
滈葕
给定一张边权为 \(0\) 或 \(1\) 的有向图,给每个点赋予 A/B/C/D 的一个字母,使得每条有向边边权为 \(1\) 当且仅当 \((a_u, a_v) \in \{ (A, D), (A, B), (B, D), (B, A), (C, D), (C, A), (C, B) \}\) 。
\(n \le 10^5\) ,\(m \le 5 \times 10^5\)
考虑令 \(A = 01, B = 10, C = 11, D = 11\) ,于是每一条 \(1\) 的边都对应了存在某一位 \(u > v\) 。考虑如何表示为 2-SAT 的限制:
- \(w = 0\) :对于每一位 \(u\) 都不大于 \(v\) 。
- 若 \(u\) 的某一位是 \(1\) ,则 \(v\) 的相应位必须是 \(1\) 。
- 若 \(v\) 的某一位是 \(0\) ,则 \(u\) 的相应位必须是 \(0\) 。
- \(w = 1\) :存在每一位 \(u\) 大于 \(v\) 。
- 若 \(u\) 的某一位是 \(0\) ,则 \(u\) 的另一位必须是 \(1\) ,且 \(v\) 的另一位必须是 \(0\) 。
- 若 \(v\) 的某一位是 \(1\) ,则 \(v\) 的另一位必须是 \(0\) ,且 \(u\) 的另一位必须是 \(1\) 。
时间复杂度 \(O(n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int dfn[N], low[N], sta[N], leader[N];
int n, m, dfstime, top, scc;
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v])
Tarjan(v), low[u] = min(low[u], low[v]);
else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++scc;
while (sta[top] != u)
leader[sta[top--]] = scc;
leader[sta[top--]] = scc;
}
}
signed main() {
scanf("%d%d", &n, &m);
// x, x + n * 2 : bit0 = 0 / 1
// x + n, x + n * 3 : bit1 = 0 / 1
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
if (w) {
G.insert(u, u + n * 3), G.insert(u, v + n);
G.insert(u + n, u + n * 2), G.insert(u + n, v);
G.insert(v + n * 2, v + n), G.insert(v + n * 2, u + n * 3);
G.insert(v + n * 3, v), G.insert(v + n * 3, u + n * 2);
} else {
G.insert(u + n * 2, v + n * 2), G.insert(u + n * 3, v + n * 3);
G.insert(v, u), G.insert(v + n, u + n);
}
}
for (int i = 1; i <= n * 4; ++i)
if (!dfn[i])
Tarjan(i);
for (int i = 1; i <= n * 2; ++i)
if (leader[i] == leader[i + n * 2])
return puts("NO"), 0;
puts("YES");
for (int i = 1; i <= n; ++i)
putchar("DABC"[(leader[i + n * 2] < leader[i]) + (leader[i + n * 3] < leader[i + n]) * 2]);
return 0;
}
[ARC069F] Flags
数轴上有 \(n\) 个球,第 \(i\) 个球可以选择放在 \(x_i\) 或 \(y_i\) ,记球两两间的最小距离为 \(d\) ,求 \(\max d\) 。
\(n \le 10^4\) ,\(x_i, y_i \in [1, 10^9]\)
显然先二分答案,问题转化为 2-SAT,连边就是一个点向一个区间连边。
显然可以线段树优化建图做到 \(O(n \log n \log V)\) ,但是常数较大,并不优美。
考虑 Kosaraju,则需要在原图和反图上 dfs。用并查集维护每个点是否被访问,则容易找到区间内第一个没被访问的点,时间复杂度 \(O(n \alpha(n) \log V)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e4 + 7;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
pair<int, int> p[N];
int rk[N], pre[N], id[N], leader[N];
int n;
inline bool check(int lim) {
for (int i = 1, j = 1; i <= n * 2; ++i) {
while (j <= n * 2 && p[j].first + lim <= p[i].first)
++j;
pre[i] = j;
}
auto flip = [](int x) {
return p[x].second <= n ? rk[p[x].second + n] : rk[p[x].second - n];
};
int col = -1, tot = 0;
function<void(int)> dfs = [&](int x) {
int u = (col == -1 ? x : flip(x));
dsu.merge(flip(u) + 1, flip(u));
for (int cur = dsu.find(pre[u]); cur <= n * 2 && p[cur].first < p[u].first + lim; cur = dsu.find(cur)) {
if (cur == u)
++cur;
else
dfs(col == -1 ? flip(cur) : cur);
}
if (col == -1)
id[++tot] = x;
else
leader[x] = col;
};
dsu.prework(n * 2 + 1);
for (int i = 1; i <= n * 2; ++i)
if (dsu.find(flip(i)) == flip(i))
dfs(i);
dsu.prework(n * 2 + 1);
for (int i = tot; i; --i)
if (dsu.find(id[i]) == id[i])
dfs(col = id[i]);
for (int i = 1; i <= n; ++i)
if (leader[rk[i]] == leader[rk[i + n]])
return false;
return true;
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d%d", &p[i].first, &p[i + n].first), p[i].second = i, p[i + n].second = i + n;
sort(p + 1, p + n * 2 + 1);
for (int i = 1; i <= n * 2; ++i)
rk[p[i].second] = i;
int l = 0, r = 1e9, ans = 0;
while (l <= r) {
int mid = (l + r) >> 1;
if (check(mid))
ans = mid, l = mid + 1;
else
r = mid - 1;
}
printf("%d", ans);
return 0;
}
割点
定义:在无向图中,删去后使得连通分量数增加的点称为割点,注意孤立点和孤立边的两个端点都不是割点。
对于非根点,若存在儿子 \(v\) 满足 \(low_v \ge dfn_u\) (即不能回到祖先),则该点为割点。对于根,若存在 \(\ge 2\) 个儿子则为割点。
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime;
int sonsum = 0;
for (int v : G.e[u]) {
if (!dfn[v]) {
++sonsum, Tarjan(v, u);
cut[u] |= (f && low[v] >= dfn[u]);
low[u] = min(low[u], low[v]);
} else
low[u] = min(low[u], dfn[v]);
}
cut[u] |= (!f && sonsum >= 2);
}
割边(桥)
定义:在无向图中,删去后使得连通分量数增加的边称为割边(桥),注意孤立边是割边。
代码和割点差不多,只要改一处: \(low_v > dfn_u\) ,且无需特判根节点。
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (!dfn[v]) {
Tarjan(v, u), low[u] = min(low[u], low[v]);
if (low[v] > dfn[u])
G.e[i].cut = G.e[i ^ 1].cut = true;
} else if (v != f)
low[u] = min(low[u], dfn[v]);
}
}
QOJ5425. Proposition Composition
给定一张 \(n\) 个点的图,点 \(i\) 和 \(i + 1\) 之间有边。\(m\) 次操作,每次加入一条边,并求出删去两条边使得图不连通的方案数。
\(n, m \le 2.5 \times 10^5\)
定义额外边 \((u, v) (u \le v)\) 覆盖了链边 \((u, u + 1), \cdots, (v - 1, v)\) ,则出删去两条边使得图不连通的方案只有三种情况:
- 一条割边,一条非割边:割边一定是链边,这可以用并查集维护。
- 一条链边,一条额外边,且该链边只被该额外边覆盖:这同样可以用并查集维护。
- 两条覆盖情况相同的链边:考虑将每条链边被覆盖的情况记为一个长度为 \(m\) 的 01 串,则第 \(i\) 次加边时,两条链边覆盖情况相同当且仅当 \(lcp \ge i\) 。考虑将 \(n - 1\) 个 01 串排序,则按 \(lcp < i\) 的位置切分后,一个连续段即为一个覆盖的等价类。因此排序后每次只会分裂若干位置,时光倒流即可用并查集维护等价类合并后的信息。
时间复杂度 \(O(n \log^2 n)\) 。
#include <bits/stdc++.h>
typedef unsigned long long ull;
typedef long long ll;
using namespace std;
const int N = 2.5e5 + 7;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(y)] = find(x);
}
} dsu1, dsu2;
struct exDSU {
int fa[N], siz[N];
ll res;
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
fill(siz + 1, siz + n + 1, 1);
res = 0;
}
inline int find(int x) {
while (x != fa[x])
x = fa[x];
return x;
}
inline void merge(int x, int y) {
x = find(x), y = find(y);
if (x == y)
return;
if (siz[x] < siz[y])
swap(x, y);
res -= 1ll * siz[x] * (siz[x] - 1) / 2 + 1ll * siz[y] * (siz[y] - 1) / 2;
siz[fa[y] = x] += siz[y], res += 1ll * siz[x] * (siz[x] - 1) / 2;
}
} dsu;
vector<pair<int, ull> > upd1[N];
vector<int> upd2[N];
ll ans[N];
int id[N];
mt19937_64 myrand(time(0));
int n, m;
namespace SMT {
const int S = 3e7 + 7;
ull s[S];
int rt[N], lc[S], rc[S];
int tot;
inline void clear(int n) {
memset(s + 1, 0, sizeof(ull) * tot);
memset(lc + 1, 0, sizeof(int) * tot);
memset(rc + 1, 0, sizeof(int) * tot);
memset(rt, 0, sizeof(int) * n);
tot = 0;
}
void build(int &x, int l, int r) {
x = ++tot;
if (l == r)
return;
int mid = (l + r) >> 1;
build(lc[x], l, mid), build(rc[x], mid + 1, r);
}
int update(int x, int nl, int nr, int p, ull k) {
int y = ++tot;
s[y] = s[x] ^ k, lc[y] = lc[x], rc[y] = rc[x];
if (nl == nr)
return y;
int mid = (nl + nr) >> 1;
if (p <= mid)
lc[y] = update(lc[x], nl, mid, p, k);
else
rc[y] = update(rc[x], mid + 1, nr, p, k);
return y;
}
bool cmp(int a, int b, int l, int r) {
if (l == r)
return s[a] < s[b];
int mid = (l + r) >> 1;
return s[lc[a]] == s[lc[b]] ? cmp(rc[a], rc[b], mid + 1, r) : cmp(lc[a], lc[b], l, mid);
}
int lcp(int a, int b, int l, int r) {
if (s[a] == s[b])
return r - l + 1;
else if (l == r)
return 0;
int mid = (l + r) >> 1;
return s[lc[a]] == s[lc[b]] ? mid - l + 1 + lcp(rc[a], rc[b], mid + 1, r) : lcp(lc[a], lc[b], l, mid);
}
} // namespace SMT
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
dsu1.prework(n), dsu2.prework(n);
int cnt1 = n - 1, cnt2 = 0;
for (int i = 1; i <= m; ++i) {
int x, y;
scanf("%d%d", &x, &y);
if (x > y)
swap(x, y);
ull k = myrand();
upd1[x].emplace_back(i, k), upd1[y].emplace_back(i, k);
auto merge = [&](int x, int y) {
for (x = dsu2.find(x); x != dsu2.find(y); x = dsu2.find(x))
dsu2.merge(x + 1, x), ++cnt2;
};
while (dsu1.find(x) != dsu1.find(y))
merge(x, dsu1.find(x)), x = dsu1.find(x), dsu1.merge(x + 1, x), --cnt1, ++x;
merge(x, y);
ans[i] = 1ll * cnt1 * (n - 1 - cnt1 + i) + (n - 1 - cnt1 - cnt2);
}
SMT::clear(n - 1), SMT::build(SMT::rt[0], 1, m);
for (int i = 1; i < n; ++i) {
SMT::rt[i] = SMT::rt[i - 1];
for (auto it : upd1[i])
SMT::rt[i] = SMT::update(SMT::rt[i], 1, m, it.first, it.second);
upd1[i].clear();
}
upd1[n].clear(), iota(id + 1, id + n, 1);
stable_sort(id + 1, id + n, [](const int &a, const int &b) {
return SMT::cmp(SMT::rt[a], SMT::rt[b], 1, m);
});
for (int i = 1; i < n - 1; ++i)
upd2[SMT::lcp(SMT::rt[id[i]], SMT::rt[id[i + 1]], 1, m)].emplace_back(i);
dsu.prework(n - 1);
for (int i = m; i; --i) {
for (int it : upd2[i])
dsu.merge(id[it], id[it + 1]);
upd2[i].clear(), ans[i] += dsu.res;
}
for (int i = 1; i <= m; ++i)
printf("%lld\n", ans[i]);
}
return 0;
}
边双连通
定义:
- 边双连通图:不存在割边的无向连通图。
- 边双连通分量:极大边双连通子图。
- 边双连通:处在同一边双连通分量内的任意两点边双连通。
边双的一些性质:
- 边双对点有传递性。
- 每个点恰属于一个边双。
- 对于边双内任意点 \(u\) ,存在经过 \(u\) 的回路。
- 对于边双内任意边 \(e\) ,存在经过 \(e\) 的回路。
- 两点之间任意一条迹(不经过重复边的路径)上的所有割边,就是两点之间的所有必经边。
- 对于边双内任意两点 \(u, v\) ,存在经过 \(u, v\) 的简单环,即 \(u, v\) 边双连通当且仅当 \(u, v\) 间无必经边。
- 边双可以通过给边定向形成 SCC。
- 边双缩点后形态为树或森林。
Tarjan 求解边双:去掉割边后所有极大连通块均为边双连通分量。
也可以用栈维护 dfs 到的所有点,每次找到割边 \((fa,son)\) 就不断弹栈直到弹出 \(son\) ,则弹出的所有点是一个边双。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7, M = 4e6 + 7;
struct Graph {
struct Edge {
int nxt, v;
bool cut;
} e[M];
int head[N];
int tot = 1;
inline void insert(int u, int v) {
e[++tot] = (Edge){head[u], v, false}, head[u] = tot;
}
} G;
vector<int> edcc[N];
int dfn[N], low[N];
bool vis[N];
int n, m, dfstime, tot;
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (!dfn[v]) {
Tarjan(v, u), low[u] = min(low[u], low[v]);
if (low[v] > dfn[u])
G.e[i].cut = G.e[i ^ 1].cut = true;
} else if (v != f)
low[u] = min(low[u], dfn[v]);
}
}
void dfs(int u) {
edcc[tot].emplace_back(u), vis[u] = true;
for (int i = G.head[u]; i; i = G.e[i].nxt)
if (!G.e[i].cut && !vis[G.e[i].v])
dfs(G.e[i].v);
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
Tarjan(i, 0);
for (int i = 1; i <= n; ++i)
if (!vis[i])
++tot, dfs(i);
printf("%d\n", tot);
for (int i = 1; i <= tot; ++i) {
printf("%d ", (int)edcc[i].size());
for (int it : edcc[i])
printf("%d ", it);
puts("");
}
return 0;
}
P4652 [CEOI2017] One-Way Streets
给定一张 \(n\) 个点 \(m\) 条边的无向图,现在需要把边定向。
有 \(q\) 个限制条件,每个条件形如 \((x_i,y_i)\),表示在定向后的图中 \(x_i\) 能到达 \(y_i\)。
判断每条边的方向是否能够唯一确定,并给出这些能够唯一确定的边的方向。
\(n, m, q \le 10^5\) ,保证有解
由于边双可以给边定向后变成 SCC,而将所有边反向后仍然是 SCC,因此一个边双内的边的方向都是无法确定的,而 \(x \to y\) 路径上的割边方向都是确定的。
将边双缩点,构建出一棵树,那么一个限制就是树上定向一条路径,不难树上差分解决。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
struct Edge {
int nxt, v;
bool cut;
} e[N << 1];
int head[N];
int tot = 1;
inline void insert(int u, int v) {
e[++tot] = (Edge) {head[u], v, false}, head[u] = tot;
}
} G, T;
int dfn[N], low[N], leader[N], val[N], fa[N];
char ans[N];
int n, m, q, dfstime, edcc;
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (!dfn[v]) {
Tarjan(v, u), low[u] = min(low[u], low[v]);
if (low[v] > dfn[u])
G.e[i].cut = G.e[i ^ 1].cut = true;
} else if (v != f)
low[u] = min(low[u], dfn[v]);
}
}
void dfs1(int u) {
leader[u] = edcc;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (!G.e[i].cut && !leader[v])
dfs1(v);
}
}
void dfs2(int u, int f) {
fa[u] = f;
for (int i = T.head[u]; i; i = T.e[i].nxt) {
int v = T.e[i].v;
if (v != f)
dfs2(v, u), val[u] += val[v];
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
Tarjan(i, 0);
for (int i = 1; i <= n; ++i)
if (!leader[i])
++edcc, dfs1(i);
for (int u = 1; u <= n; ++u)
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (leader[u] != leader[v])
T.insert(leader[u], leader[v]);
}
scanf("%d", &q);
while (q--) {
int u, v;
scanf("%d%d", &u, &v);
++val[leader[u]], --val[leader[v]];
}
for (int i = 1; i <= edcc; ++i)
if (!fa[i])
dfs2(i, 0);
for (int u = 1; u <= n; ++u)
for (int i = G.head[u]; i; i = G.e[i].nxt) {
if (i & 1)
continue;
int v = G.e[i].v;
if (leader[u] == leader[v])
ans[i / 2] = 'B';
else if (fa[leader[v]] == leader[u])
ans[i / 2] = (val[leader[v]] ? (val[leader[v]] > 0 ? 'L' : 'R') : 'B');
else
ans[i / 2] = (val[leader[u]] ? (val[leader[u]] > 0 ? 'R' : 'L') : 'B');
}
ans[m + 1] = '\0', puts(ans + 1);
return 0;
}
CF475E Strongly Connected City 2
给一个无向连通图,需要给所有边定向,最大化满足 \(a\) 能到达 \(b\) 的点对 \((a, b)\) 数量。
\(n \le 2000\)
由于边双可以赋方向后可以变成 SCC,于是考虑把先把边双缩点成树。
可以发现最优方案一定是确定根之后,一部分子树都是叶向边,剩下的子树都是父向边。
直接枚举根做 bitset 优化背包即可做到 \(O(n + m + \frac{n^2}{\omega})\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 2e3 + 7, M = 4e6 + 7;
struct Graph {
struct Edge {
int nxt, v;
bool cut;
} e[M];
int head[N];
int tot = 1;
inline void insert(int u, int v) {
e[++tot] = (Edge) {head[u], v, false}, head[u] = tot;
}
} G, T;
int dfn[N], low[N], leader[N], a[N], siz[N];
int n, m, dfstime, edcc;
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (!dfn[v]) {
Tarjan(v, u), low[u] = min(low[u], low[v]);
if (low[v] > dfn[u])
G.e[i].cut = G.e[i ^ 1].cut = true;
} else if (v != f)
low[u] = min(low[u], dfn[v]);
}
}
void dfs1(int u) {
++a[leader[u] = edcc];
for (int i = G.head[u]; i; i = G.e[i].nxt)
if (!leader[G.e[i].v] && !G.e[i].cut)
dfs1(G.e[i].v);
}
void dfs2(int u, int fa) {
siz[u] = a[u];
for (int i = T.head[u]; i; i = T.e[i].nxt) {
int v = T.e[i].v;
if (v != fa)
dfs2(v, u), siz[u] += siz[v];
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
Tarjan(i, 0);
for (int i = 1; i <= n; ++i)
if (!leader[i])
++edcc, dfs1(i);
for (int u = 1; u <= n; ++u)
for (int i = G.head[u]; i; i = G.e[i].nxt)
if (G.e[i].cut)
T.insert(leader[u], leader[G.e[i].v]);
ll ans = 0;
for (int u = 1; u <= edcc; ++u) {
dfs2(u, 0);
ll res = 0;
for (int i = 1; i <= edcc; ++i)
res += 1ll * siz[i] * a[i];
bitset<N> f;
f.set(0);
for (int i = T.head[u]; i; i = T.e[i].nxt)
f |= f << siz[T.e[i].v];
for (int i = 0; i <= n - a[u]; ++i)
if (f.test(i))
ans = max(ans, res + 1ll * i * (n - a[u] - i));
}
printf("%lld", ans);
return 0;
}
CF51F Caterpillar
定义一张无向连通无环图是毛毛虫,当且仅当图上存在一条路径 \(P\) ,满足所有点到与 \(P\) 的距离均 \(\leq 1\) ,且 \(P\) 不能有重边。
给出一张无向图,一次操作可以将两个点合并(出边也合并),求将其变成毛毛虫的最少操作次数。
\(n \le 2000\) ,\(m \le 10^5\)
由于毛毛虫无环,因此先对整张图按边双缩点转化为森林,则每个边双都要合并成一个点。
对于一棵树,最优的毛毛虫显然就是去掉叶子之后的直径,因为叶子可以变成一条链上挂的点,剩下的最长链显然就是直径。
时间复杂度 \(O(n + m)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7, M = 2e5 + 7;
struct Graph {
struct Edge {
int nxt, v;
bool cut;
} e[M];
int head[N];
int tot = 1;
inline void insert(int u, int v) {
e[++tot] = (Edge) {head[u], v, false}, head[u] = tot;
}
} G, T;
int dfn[N], low[N], leader[N], len[N], son[N], deg[N];
bool vis[N], in[N];
int n, m, dfstime, edcc;
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (!dfn[v]) {
Tarjan(v, u), low[u] = min(low[u], low[v]);
if (low[v] > dfn[u])
G.e[i].cut = G.e[i ^ 1].cut = true;
} else if (v != f)
low[u] = min(low[u], dfn[v]);
}
}
void dfs1(int u) {
leader[u] = edcc;
for (int i = G.head[u]; i; i = G.e[i].nxt)
if (!leader[G.e[i].v] && !G.e[i].cut)
dfs1(G.e[i].v);
}
int dfs2(int u, int fa) {
vis[u] = true, len[u] = 1, son[u] = 0;
int down = u;
for (int i = T.head[u]; i; i = T.e[i].nxt) {
int v = T.e[i].v;
if (v == fa)
continue;
int x = dfs2(v, u);
if (len[v] + 1 > len[u])
down = x, son[u] = v, len[u] = len[v] + 1;
}
return down;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
Tarjan(i, 0);
for (int i = 1; i <= n; ++i)
if (!leader[i])
++edcc, dfs1(i);
for (int u = 1; u <= n; ++u)
for (int i = G.head[u]; i; i = G.e[i].nxt)
if (G.e[i].cut)
T.insert(leader[u], leader[G.e[i].v]), ++deg[leader[u]];
int ans = count(deg + 1, deg + edcc + 1, 1), cnt = 0;
for (int i = 1; i <= edcc; ++i)
if (!vis[i]) {
++cnt;
if (!deg[i]) {
++ans;
continue;
}
int rt = dfs2(i, 0);
dfs2(rt, 0), ans += len[rt] - 2;
}
printf("%d", n - ans + cnt - 1);
return 0;
}
CF855G Harry Vs Voldemort
给出一棵树,\(q\) 次操作,每次增加一条无向边,每次加边后求满足 \(P(u \to w) \cap P(v \to w) = \emptyset\) 的三元组 \((u, v, w)\) 数量,其中 \(P\) 表示一条路径的边集。
\(n, q \le 10^5\)
考虑缩完边双的树,枚举 \(w\) 为根,则满足条件的 \((u, v, w)\) 必然满足 \(u\) 和 \(v\) 不在 \(w\) 的同一子树内。确定根后一个 \(w\) 的贡献就是 \(n^2 - (n - s_w)^2 - \sum_{v \in son(w)} s_v^2\) ,其中 \(s_u\) 表示 \(u\) 的带权子树大小。
接下来考虑处理加边的情况,考虑用并查集维护边双,一个点指向每个边双深度最小的节点,每次暴力向上跳父亲合并边双,同时维护 \(s\) 和子树的 \(s^2\) 即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
ll s2[N];
int fa[N], dep[N], s1[N];
ll ans;
int n, q;
inline void calc(int x, int s, int op) {
ans += 1ll * op * s * (s - 1) * (s - 2); // u, v \in edcc
ans += 2ll * op * s * (s - 1) * (n - s); // u (or v) \not \in edcc
ans += 1ll * op * s * (1ll * (n - s) * (n - s) - s2[x] - 1ll * (n - s1[x]) * (n - s1[x])); // u, v \not \in edcc
}
struct DSU {
int fa[N], siz[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1), fill(siz + 1, siz + n + 1, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
x = find(x), y = find(y);
calc(x, siz[x], -1), calc(y, siz[y], -1);
siz[x] += siz[y], s2[x] += s2[y] - 1ll * s1[y] * s1[y], fa[y] = x;
calc(x, siz[x], 1);
}
} dsu;
void dfs(int u, int f) {
fa[u] = f, dep[u] = dep[f] + 1, s1[u] = 1;
for (int v : G.e[u])
if (v != f)
dfs(v, u), s1[u] += s1[v], s2[u] += 1ll * s1[v] * s1[v];
calc(u, 1, 1);
}
signed main() {
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
dfs(1, 0), dsu.prework(n);
printf("%lld\n", ans);
scanf("%d", &q);
while (q--) {
int x, y;
scanf("%d%d", &x, &y);
x = dsu.find(x), y = dsu.find(y);
while (x != y) {
if (dep[x] < dep[y])
swap(x, y);
dsu.merge(fa[x], x), x = dsu.find(x);
}
printf("%lld\n", ans);
}
return 0;
}
P8867 [NOIP2022] 建造军营
给出一张无向连通图,求有多少选出至少一个点组成点集 \(V' \subseteq V\) 、任意数量边组成边集 \(E' \subseteq E\) 的方案,满足对于任意 \(e \in E \setminus E'\) 满足删去 \(e\) 后 \(V'\) 仍连通。
\(n \le 5 \times 10^5\) ,\(m \le 10^6\)
先将边双缩点成一棵树,则选出的点集在树上组成的极小连通块的树边必须选,剩下的边无所谓。
于是可以树形 DP,设 \(f_{u, 0/1}\) 表示 \(u\) 子树是否选点的方案数,则:
接下来考虑统计答案,每个点集连通块在 LCA(记为 \(u\))处统计,钦定 \(u\) 连父亲的边不能选(否则会在父亲再统计一次),则令 \(ans \gets f_{u, 1} \times 2^{m - s_e(u) - 1}\) ,其中 \(s_e(u)\) 表示 \(u\) 子树内的边数,对根特殊处理 \(ans \gets f_{r, 1}\) 即可。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e5 + 7, M = 1e6 + 7;
struct Graph {
struct Edge {
int nxt, v;
bool cut;
} e[M << 1];
int head[N];
int tot = 1;
inline void insert(int u, int v) {
e[++tot] = (Edge){head[u], v, false}, head[u] = tot;
}
} G, T;
int f[N][2], pw[M], dfn[N], low[N], leader[N], vsiz[N], esiz[N];
int n, m, dfstime, edcc, ans;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (!dfn[v]) {
Tarjan(v, u), low[u] = min(low[u], low[v]);
if (low[v] > dfn[u])
G.e[i].cut = G.e[i ^ 1].cut = true;
} else if (v != f)
low[u] = min(low[u], dfn[v]);
}
}
void dfs1(int u) {
++vsiz[leader[u] = edcc];
for (int i = G.head[u]; i; i = G.e[i].nxt)
if (!G.e[i].cut && !leader[G.e[i].v])
dfs1(G.e[i].v);
}
void dfs2(int u, int fa) {
f[u][0] = pw[esiz[u]], f[u][1] = 1ll * dec(pw[vsiz[u]], 1) * pw[esiz[u]] % Mod;
for (int i = T.head[u]; i; i = T.e[i].nxt) {
int v = T.e[i].v;
if (v == fa)
continue;
dfs2(v, u), esiz[u] += esiz[v] + 1;
f[u][1] = add(1ll * add(f[u][0], f[u][1]) * f[v][1] % Mod, 2ll * f[u][1] * f[v][0] % Mod);
f[u][0] = 2ll * f[u][0] * f[v][0] % Mod;
}
if (u == 1)
ans = add(ans, f[u][1]);
else
ans = add(ans, 1ll * f[u][1] * pw[m - esiz[u] - 1] % Mod);
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
Tarjan(1, 0);
for (int i = 1; i <= n; ++i)
if (!leader[i])
++edcc, dfs1(i);
for (int u = 1; u <= n; ++u)
for (int i = G.head[u]; i; i = G.e[i].nxt) {
if (G.e[i].cut)
T.insert(leader[u], leader[G.e[i].v]);
else
++esiz[leader[u]];
}
for (int i = 1; i <= edcc; ++i)
esiz[i] >>= 1;
pw[0] = 1;
for (int i = 1; i <= m; ++i)
pw[i] = 2ll * pw[i - 1] % Mod;
dfs2(1, 0);
printf("%d", ans);
return 0;
}
两条
给出一张无向图,\(q\) 次操作,操作有:
1 k:删除第 \(k\) 条边。2 u v:查询 \(u, v\) 之间是否存在两条边不相交路径。\(n \le 8 \times 10^5\) ,\(m, q \le 10^6\)
时光倒流转化为加边,考虑求出一棵生成树,则只需并查集维护边双即可。
先将始终存在的边加入,然后倒序枚举,若枚举到的边会合并两个连通块,则说明其为一条割边,称其为关键边。
接下来考虑求解答案,先找到原图的一个生成森林,每次遇到非关键边时缩边双即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 8e5 + 7, M = 1e6 + 7;
struct Edge {
int u, v;
} e[M];
struct Node {
int op, x, y;
} nd[M];
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + 1 + n, 1);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
x = find(x), y = find(y);
if (x != y)
fa[y] = x;
}
} dsu;
int fa[N], dep[N];
bool exist[M], ans[M];
int n, m, q, testid;
void dfs(int u, int f) {
fa[u] = f, dep[u] = dep[f] + 1;
for (int v : G.e[u])
if (v != f)
dfs(v, u);
}
inline void update(int x, int y) {
x = dsu.find(x), y = dsu.find(y);
while (x != y) {
if (dep[x] < dep[y])
swap(x, y);
dsu.merge(fa[x], x), x = dsu.find(x);
}
}
signed main() {
scanf("%d%d%d%d", &n, &m, &q, &testid);
for (int i = 1; i <= m; ++i)
scanf("%d%d", &e[i].u, &e[i].v), exist[i] = true;
for (int i = 1; i <= q; ++i) {
scanf("%d", &nd[i].op);
if (nd[i].op == 1)
scanf("%d", &nd[i].x), exist[nd[i].x] = false;
else
scanf("%d%d", &nd[i].x, &nd[i].y);
}
dsu.prework(n);
for (int i = 1; i <= m; ++i)
if (exist[i]) {
int fx = dsu.find(e[i].u), fy = dsu.find(e[i].v);
if (fx != fy) {
G.insert(e[i].u, e[i].v), G.insert(e[i].v, e[i].u);
dsu.merge(fx, fy), exist[i] = false;
}
}
for (int i = q; i; --i)
if (nd[i].op == 1) {
int fx = dsu.find(e[nd[i].x].u), fy = dsu.find(e[nd[i].x].v);
if (fx != fy) {
G.insert(e[nd[i].x].u, e[nd[i].x].v), G.insert(e[nd[i].x].v, e[nd[i].x].u);
dsu.merge(fx, fy), nd[i].op = -1;
}
}
for (int i = 1; i <= n; ++i)
if (!dep[i])
dfs(i, 0);
dsu.prework(n);
for (int i = 1; i <= m; ++i)
if (exist[i])
update(e[i].u, e[i].v);
for (int i = q; i; --i) {
if (nd[i].op == 1)
update(e[nd[i].x].u, e[nd[i].x].v);
else if (nd[i].op == 2)
ans[i] = (dsu.find(nd[i].x) == dsu.find(nd[i].y));
}
for (int i = 1; i <= q; ++i)
if (nd[i].op == 2)
puts(ans[i] ? "YES" : "NO");
return 0;
}
点双连通
定义:
- 点双连通图:不存在割点的无向连通图称为点双连通图。
- 点双连通分量:极大点双连通子图。
- 点双连通:处在同一点双连通分量内的任意两点点双连通。
点双的一些性质:
- 点双对点没有传递性。
- 每条边恰属于一个点双。
- 一个点是割点当且仅当它属于多个点双。
- 若两点双有交,则交点为割点。
- 一条边的两端点点双连通。
- 对于点双内的任意点 \(u\) ,存在经过 \(u\) 的简单环。
- 当 \(n \ge 3\) 时,在边中间插入点不影响点双连通性,因此钦定经过一个点和经过一条边是几乎等价的。
- 对 \(n \ge 3\) 的点双中任意点 \(u\) 与任意边 \(e\) ,存在经过 \(u, e\) 的简单环。
- 对 \(n \ge 3\) 的点双中任意不同两点 \(u, v\) 与任意边 \(e\) ,存在 \(u \rightsquigarrow e \rightsquigarrow v\) 的简单路径。
- 对 \(n \ge 3\) 的点双中任意不同三点 \(u, v, w\) ,存在 \(u \rightsquigarrow v \rightsquigarrow w\) 的简单路径。
- 两点间任意一条路径上的所有割点,即为两点之间的所有必经点。
Tarjan 求解:每次判定 \(low_v \ge dfn_u\) 时,\(v\) 子树栈内的点与 \(u\) 共同构成一个点双,需要特判一下孤立点的情况。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<int> vdcc[N];
int dfn[N], low[N], sta[N];
int n, m, dfstime, top, tot;
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
int sonsum = 0;
for (int v : G.e[u]) {
if (!dfn[v]) {
++sonsum, Tarjan(v, u), low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
vdcc[++tot] = {u};
while (sta[top] != v)
vdcc[tot].emplace_back(sta[top--]);
vdcc[tot].emplace_back(sta[top--]);
}
} else if (v != f)
low[u] = min(low[u], dfn[v]);
}
if (!f && !sonsum)
vdcc[++tot] = {u};
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
Tarjan(i, 0);
printf("%d\n", tot);
for (int i = 1; i <= tot; ++i) {
printf("%d ", (int)vdcc[i].size());
for (int it : vdcc[i])
printf("%d ", it);
puts("");
}
return 0;
}
P8456 「SWTR-8」地地铁铁
给定边权为 \(0\) 或 \(1\) 的无向连通图,求有多少点对之间存在同时经过 \(0\) 和 \(1\) 的简单路径。
\(n \le 4 \times 10^5\) ,\(m \le 10^6\)
对于落在不同点双的点对,若路径上的点双包含 \(0\) 边和 \(1\) 边则合法,否则显然不合法。
对于落在相同点双的点对,如果点双内部边权相同,显然不合法,否则若存在唯二既有 \(0\) 出边又有 \(1\) 出边的点对,则该点对不合法;其他情况均合法,因为均可以走到同时存在 \(0\) 和 \(1\) 的边的点处切换颜色。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 4e5 + 7, M = 1e6 + 7;
struct DSU {
int fa[N << 1], siz[N << 1];
inline void prework(int n, int ext) {
iota(fa + 1, fa + 1 + n + ext, 1);
fill(siz + 1, siz + 1 + n, 1), fill(siz + 1 + n, siz + 1 + n + ext, 0);
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
x = find(x), y = find(y);
if (x == y)
return;
fa[y] = x, siz[x] += siz[y];
}
} dsu1, dsu2;
struct Graph {
struct Edge {
int nxt, v, w;
} e[M << 1];
int head[N];
int tot = 1;
inline void insert(int u, int v, int w) {
e[++tot] = (Edge) {head[u], v, w}, head[u] = tot;
}
} G;
vector<int> vdcc[N];
int dfn[N], low[N], sta[N], esta[M], tag[N], tp[N];
bool in[M];
ll ans;
int testid, n, m, dfstime, top, etop, ext;
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (!dfn[v]) {
in[esta[++etop] = i >> 1] = true;
Tarjan(v), low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
vdcc[++ext].emplace_back(u);
while (sta[top] != v)
vdcc[ext].emplace_back(sta[top--]);
vdcc[ext].emplace_back(sta[top--]);
vector<int> edg;
while (esta[etop] != (i >> 1)) {
int x = esta[etop--];
in[x] = false, tp[ext] |= 1 << G.e[x << 1].w;
tag[G.e[x << 1].v] |= 1 << G.e[x << 1].w, tag[G.e[x << 1 | 1].v] |= 1 << G.e[x << 1].w;
}
int x = esta[etop--];
in[x] = false, tp[ext] |= 1 << G.e[x << 1].w;
tag[G.e[x << 1].v] |= 1 << G.e[x << 1].w, tag[G.e[x << 1 | 1].v] |= 1 << G.e[x << 1].w;
int sum = 0;
for (int x : vdcc[ext])
sum += (tag[x] == 3), tag[x] = 0;
if (sum == 2)
--ans;
}
} else {
low[u] = min(low[u], dfn[v]);
if (dfn[v] < dfn[u] && !in[i >> 1])
esta[++etop] = i >> 1;
}
}
}
signed main() {
cin >> testid >> n >> m;
for (int i = 1; i <= m; ++i) {
int u, v;
char w;
cin >> u >> v >> w;
G.insert(u, v, w == 'd'), G.insert(v, u, w == 'd');
}
ans = 1ll * n * (n - 1) / 2, Tarjan(1);
dsu1.prework(n, ext), dsu2.prework(n, ext);
for (int u = 1; u <= ext; ++u)
for (int v : vdcc[u]) {
if (tp[u] == 1)
dsu1.merge(u + n, v);
else if (tp[u] == 2)
dsu2.merge(u + n, v);
}
for (int i = 1; i <= n + ext; ++i) {
if (dsu1.find(i) == i)
ans -= 1ll * dsu1.siz[i] * (dsu1.siz[i] - 1) / 2;
if (dsu2.find(i) == i)
ans -= 1ll * dsu2.siz[i] * (dsu2.siz[i] - 1) / 2;
}
printf("%lld", ans);
return 0;
}
广义圆方树
在圆方树中,原来的每个点对应一个圆点,每一个点双对应一个方点。
对于每一个点双,它对应的方点向这个点双中的每个点连边。不难发现圆方树中每条边连接一个圆点和一个方点,每个点双形成一个菊花,多个菊花通过原图中的割点连接在一起。

圆方树的点数不超过 \(2n\) ,注意需要开两倍空间。
构建圆方树考虑 Tarjan 算法,不难发现对于一条边 \(u \to v\) ,\(u, v\) 在同一个点双中且 \(u\) 是该点双中深度最浅的节点当且仅当 \(low_v = dfn_u\) ,类似 Tarjan 求割点的方式构建圆方树即可。
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v]) {
Tarjan(v), low[u] = min(low[u], low[v]);
if (low[v] == dfn[u]) {
T.insert(++ext, u), T.insert(u, ext);
while (sta[top] != v)
T.insert(ext, sta[top]), T.insert(sta[top--], ext);
T.insert(ext, sta[top]), T.insert(sta[top--], ext);
}
} else
low[u] = min(low[u], dfn[v]);
}
}
P4630 [APIO2018] 铁人两项
给定一张简单无向图,求有多少对三元组 \((s, c, f)\) 满足 \(s, c, f\) 互异且存在一条简单路径从 \(s\) 出发经过 \(c\) 到达 \(f\) 。
\(n \le 10^5\)
首先不难发现,若经过一个点双(至少两个点),则可以到达该点双中的任意点再走出来。
考虑构建圆方树,对于一对 \(s, f\) ,合法的 \(c\) 就是 \(s\) 到 \(f\) 路径上所有圆点和方点所连的圆点,再去掉 \(s, f\) 。
令圆点权值为 \(-1\) (因为会被路径上相邻两个方点统计两次),方点权值为点双大小,则一对 \(s, f\) 的贡献就是树上路径权值和。不难树上换根 DP 处理,时间复杂度 \(O(n + m)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G, T;
int dfn[N], low[N], sta[N], siz[N];
ll ans;
int n, m, dfstime, top, ext;
int Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
int siz = 1;
for (int v : G.e[u]) {
if (!dfn[v]) {
siz += Tarjan(v), low[u] = min(low[u], low[v]);
if (low[v] == dfn[u]) {
T.insert(++ext, u), T.insert(u, ext);
while (sta[top] != v)
T.insert(ext, sta[top]), T.insert(sta[top--], ext);
T.insert(ext, sta[top]), T.insert(sta[top--], ext);
}
} else
low[u] = min(low[u], dfn[v]);
}
return siz;
}
void dfs(int u, int f, int Siz) {
siz[u] = (u <= n);
ll sum = 0;
for (int v : T.e[u])
if (v != f)
dfs(v, u, Siz), sum += 2ll * siz[u] * siz[v], siz[u] += siz[v];
sum += 2ll * siz[u] * (Siz - siz[u]), ans += sum * (u <= n ? -1 : T.e[u].size());
}
signed main() {
scanf("%d%d", &n, &m), ext = n;
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
dfs(i, 0, Tarjan(i));
printf("%lld", ans);
return 0;
}
CF487E Tourists
给定一张简单无向连通图,点有点权 \(w_i\) ,要求支持两种操作:
- 修改一个点的点权。
- 询问两点之间所有简单路径上点权的最小值。
\(n, m, q \le 10^5\)
发现一个点双对答案的贡献为点双里面的最小权值。构建圆方树,方点的权值为点双中的最小圆点权值。然后原图就变成了一棵树,询问时就可以直接树剖套线段树求路径最小值了。
一次修改一个圆点的点权,需要修改所有和它相邻的方点,这样很容易被卡到 \(O(n)\) 个修改。考虑令方点权值为儿子圆点的权值最小值,这样的话修改时只需要修改父亲方点。对于方点的维护,只需要对每个方点开一个 multiset 维护权值集合即可。
需要注意的是查询时若 LCA 是方点,则还需要查 LCA 的父亲圆点的权值。
时间复杂度 \(O(q \log^2 n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G, T;
multiset<int> st[N];
int a[N], dfn[N], low[N], sta[N];
int n, m, q, ext, dfstime, top;
namespace TCD {
int fa[N], dep[N], siz[N], son[N], top[N], dfn[N], id[N];
int sum, dfstime;
namespace SMT {
int mn[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
void build(int x, int l, int r) {
if (l == r) {
mn[x] = a[id[l]];
return;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid), build(rs(x), mid + 1, r);
mn[x] = min(mn[ls(x)], mn[rs(x)]);
}
void update(int x, int nl, int nr, int pos, int k) {
if (nl == nr) {
mn[x] = k;
return;
}
int mid = (nl + nr) >> 1;
if (pos <= mid)
update(ls(x), nl, mid, pos, k);
else
update(rs(x), mid + 1, nr, pos, k);
mn[x] = min(mn[ls(x)], mn[rs(x)]);
}
int query(int x, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r)
return mn[x];
int mid = (nl + nr) >> 1;
if (r <= mid)
return query(ls(x), nl, mid, l, r);
else if (l > mid)
return query(rs(x), mid + 1, nr, l, r);
else
return min(query(ls(x), nl, mid, l, r), query(rs(x), mid + 1, nr, l, r));
}
} // namespace SMT
void dfs1(int u, int f) {
fa[u] = f, dep[u] = dep[f] + 1, siz[u] = 1;
for (int v : T.e[u]) {
if (v == f)
continue;
dfs1(v, u), siz[u] += siz[v];
if (siz[v] > siz[son[u]])
son[u] = v;
}
}
void dfs2(int u, int topf) {
top[u] = topf, id[dfn[u] = ++dfstime] = u;
if (son[u])
dfs2(son[u], topf);
for (int v : T.e[u])
if (v != fa[u] && v != son[u])
dfs2(v, v);
}
inline int query(int x, int y) {
int res = inf;
while (top[x] != top[y]) {
if (dfn[top[x]] < dfn[top[y]])
swap(x, y);
res = min(res, SMT::query(1, 1, ext, dfn[top[x]], dfn[x]));
x = fa[top[x]];
}
if (dfn[x] > dfn[y])
swap(x, y);
res = min(res, SMT::query(1, 1, ext, dfn[x], dfn[y]));
if (x > n)
res = min(res, a[fa[x]]);
return res;
}
} // namespace TCD
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v]) {
Tarjan(v), low[u] = min(low[u], low[v]);
if (low[v] == dfn[u]) {
T.insert(++ext, u), T.insert(u, ext);
while (sta[top] != v)
T.insert(ext, sta[top]), T.insert(sta[top--], ext);
T.insert(ext, sta[top]), T.insert(sta[top--], ext);
}
} else
low[u] = min(low[u], dfn[v]);
}
}
signed main() {
cin >> n >> m >> q;
for (int i = 1; i <= n; ++i)
cin >> a[i];
for (int i = 1; i <= m; ++i) {
int u, v;
cin >> u >> v;
G.insert(u, v), G.insert(v, u);
}
ext = n;
for (int i = 1; i <= n; ++i)
if (!dfn[i])
Tarjan(i);
TCD::dfs1(1, 0), TCD::dfs2(1, 1);
for (int i = 2; i <= n; ++i)
st[TCD::fa[i]].insert(a[i]);
for (int i = n + 1; i <= ext; ++i)
a[i] = *st[i].begin();
TCD::SMT::build(1, 1, ext);
while (q--) {
char op;
int x, y;
cin >> op >> x >> y;
if (op == 'C') {
TCD::SMT::update(1, 1, ext, TCD::dfn[x], y);
if (x == 1) {
a[x] = y;
continue;
}
int f = TCD::fa[x];
st[f].erase(st[f].find(a[x])), st[f].insert(a[x] = y);
TCD::SMT::update(1, 1, ext, TCD::dfn[f], a[f] = *st[f].begin());
} else
cout << TCD::query(x, y) << '\n';
}
return 0;
}
P4606 [SDOI2018] 战略游戏
给定一张无向图,每次给出一个点集 \(S\) ,求有多少个点 \(u\) 满足 \(u \not \in S\) 且删掉点 \(u\) 后 \(S\) 中的点不连通。
\(n, q \le 10^5\)
先建出圆方树,则变为询问 \(S\) 在圆方树上对应的连通子图中的不在 \(S\) 中的圆点个数。
记每个圆点和父亲方点的边权为 \(1\) ,则问题转化为 \(S\) 在圆方树上对应的极小连通子树的边权和,答案即为 \(dfn\) 相邻点距离和的一半。
注意要答案要减去 \(|S|\) ,且若子图中的深度最浅的节点是圆点,答案还要加上 \(1\) 。
弱化版(\(|S| = 2\)):P4320 道路相遇 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7, LOGN = 21;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G, T;
int fa[N][LOGN], dfn[N], low[N], sta[N], dep[N], dis[N];
int n, m, q, ext, dfstime, top;
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v]) {
Tarjan(v), low[u] = min(low[u], low[v]);
if (low[v] == dfn[u]) {
T.insert(++ext, u), T.insert(u, ext);
while (sta[top] != v)
T.insert(ext, sta[top]), T.insert(sta[top--], ext);
T.insert(ext, sta[top]), T.insert(sta[top--], ext);
}
} else
low[u] = min(low[u], dfn[v]);
}
}
void dfs(int u, int f) {
fa[u][0] = f, dep[u] = dep[f] + 1, dis[u] = dis[f] + (u <= n), dfn[u] = ++dfstime;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : T.e[u])
if (v != f)
dfs(v, u);
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int h = dep[x] - dep[y]; h; h &= h - 1)
x = fa[x][__builtin_ctz(h)];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
inline int dist(int x, int y) {
return dis[x] + dis[y] - dis[LCA(x, y)] * 2;
}
signed main() {
int Task;
scanf("%d", &Task);
while (Task--) {
scanf("%d%d", &n, &m), G.clear(n);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
memset(dfn + 1, 0, sizeof(int) * n), memset(low + 1, 0, sizeof(int) * n);
T.clear(n * 2), dfstime = top = 0, ext = n, Tarjan(1);
dfstime = 0, dfs(1, 0);
scanf("%d", &q);
while (q--) {
int k;
scanf("%d", &k);
vector<int> qry(k);
for (int &it : qry)
scanf("%d", &it);
sort(qry.begin(), qry.end(), [](const int &x, const int &y) { return dfn[x] < dfn[y]; });
int ans = 0;
for (int i = 0; i < qry.size(); ++i)
ans += dist(qry[i], qry[(i + 1) % qry.size()]);
ans = ans / 2 - qry.size();
if (LCA(qry.front(), qry.back()) <= n)
++ans;
printf("%d\n", ans);
}
}
return 0;
}
CF1763F Edge Queries
给定一张无向图,\(q\) 次询问,每次给出点对 \((s, t)\) ,求有多少条能出现在 \(s\) 到 \(t\) 简单路径上的边满足删去后 \(s\) 与 \(t\) 仍连通。
\(n, m, q \le 2 \times 10^5\)
构建圆方树,对于路径上的方点,若该方点表示的点双不为两点一边,则整个点双中的所有边都合法。
问题转化为树上路径求点权和,不难用树剖做到 \(O((n + q) \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 7, LOGN = 19;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G, T;
int fa[N][LOGN], dfn[N], low[N], sta[N], sum[N], dep[N];
int n, m, q, dfstime, top, ext;
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v]) {
Tarjan(v), low[u] = min(low[u], low[v]);
if (low[v] == dfn[u]) {
T.insert(++ext, u), T.insert(u, ext);
while (sta[top] != v)
T.insert(ext, sta[top]), T.insert(sta[top--], ext);
T.insert(ext, sta[top]), T.insert(sta[top--], ext);
}
} else
low[u] = min(low[u], dfn[v]);
}
}
void dfs1(int u, int f) {
fa[u][0] = f, dep[u] = dep[f] + 1;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : T.e[u])
if (v != f)
dfs1(v, u);
}
void dfs2(int u) {
sum[u] = (sum[u] == 2 ? 0 : sum[u] / 2) + sum[fa[u][0]];
for (int v : T.e[u])
if (v != fa[u][0])
dfs2(v);
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int h = dep[x] - dep[y]; h; h &= h - 1)
x = fa[x][__builtin_ctz(h)];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
ext = n, Tarjan(1), dfs1(1, 0);
for (int u = 1; u <= n; ++u)
for (int v : G.e[u]) {
if (fa[fa[v][0]][0] == u)
++sum[fa[v][0]];
else
++sum[fa[u][0]];
}
dfs2(1);
scanf("%d", &q);
while (q--) {
int x, y;
scanf("%d%d", &x, &y);
int lca = LCA(x, y);
printf("%d\n", sum[x] + sum[y] - sum[lca] - sum[fa[lca][0]]);
}
return 0;
}
P9167 [省选联考 2023] 城市建造
给定一张 \(n\) 个点 \(m\) 条边的无向连通图 \(G = (V, E)\),询问有多少该图的连通子图 \(G' = (V', E')\) 满足:
- \(E' \ne \emptyset\) 。
- \(G - E'\) 中恰有 \(|V'|\) 个连通块。
- \(G - E'\) 中连通块大小极差 \(\le k\) 。
答案对 \(998244353\) 取模。
\(3 \le n \le 10^5\),\(n - 1 \le m \le 2 \times 10^5\),\(0 \le k \le 1\)
对于选择的两个点,若存在一条简单路径连接它们,则路径上的每个点都要选择。因此若一个点双里选了两个点,则整个点双都要选。
建出圆方树,称删去一个方点表示选其所有邻域圆点,则删去的方点通过圆点直接构成一个连通块。
考虑第三条限制,删去选择的方点后,每个连通块大小(圆点数量)极差不超过 \(k\)。
先考虑 \(k = 0\) 的情况,则连通块大小 \(d\) 为 \(n\) 的因数。考虑枚举连通块大小 \(d\) ,统计方案数。
以带权重心 \(R\) 为根,若 \(R\) 为方点则令其相邻圆点为 \(R\) ,此时若删一个方点则祖先链上的方点都会被删。令圆点权值为 \(1\) ,方点权值为 \(0\) ,\(siz_i\) 表示 \(i\) 子树内的权值和。分类讨论每个方点 \(u\) 是否被删去:
- \(siz_u < d\) :不删 \(u\) 。
- \(siz_u > d\) :删去 \(u\) 。
- \(siz_u = d\) :若 \(u\) 只有一个儿子则删去 \(u\) ,否则不删 \(u\) 。
这是个必要条件,需要再判断一下该方案的合法性,可以在线维护 \(cnt_i\) 表示大小为 \(i\) 的连通块数量。从小到大枚举 \(d\) ,则每次都是不断合并 \(siz = d\) 的方点的邻域,用并查集维护即可做到 \(O(n \alpha(n))\) 。
再考虑 \(k = 1\) 的情况,考虑容斥,用钦定连通块大小为 \(d\) 或 \(d + 1\) 的方案数,减去所有连通块大小 \(\in [2, n - 1]\) 且相等的方案数。
对于 \(siz \ne d\) 的情况,按照 \(k = 0\) 的方法处理即可。否则考虑一个圆点所有 \(siz = d\) 的方点儿子,显然这些方点最多保留一个。先钦定删去所有 \(siz = d\) 的点,此时若一个方点的父亲圆点所在连通块大小 \(>1\) ,则其所有 \(siz = d\) 的儿子都必须删,否则只能保留任意一个。
注意在 \(d = 1\) 时一个圆点的所有 \(siz = d = 1\) 的方点儿子可以都删去,也可以保留一个,因为该圆点的父亲必然被删去,若不保留则儿子方点连通块大小都为 \(1\) ,若保留一个则该点连通块大小为 \(2\) 。因此答案要乘上其 \(siz = 1\) 的方点儿子的数量 \(+1\) 。
同样使用并查集维护即可做到 \(O(n \alpha(n))\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 2e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G, T;
struct DSU {
int fa[N], siz[N], cnt[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1), fill(siz + 1, siz + n + 1, 1);
memset(cnt + 1, 0, sizeof(int) * n), cnt[1] = n;
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
x = find(x), y = find(y);
if (x == y)
return;
--cnt[siz[x]], --cnt[siz[y]], ++cnt[siz[x] += siz[y]], fa[y] = x;
}
} dsu;
vector<int> vec[N];
int dfn[N], low[N], sta[N], siz[N], mxsiz[N], fa[N], sum[N], ans1[N];
bool ans0[N];
int n, m, k, ext, dfstime, top, root;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v]) {
Tarjan(v), low[u] = min(low[u], low[v]);
if (low[v] == dfn[u]) {
T.insert(++ext, u), T.insert(u, ext);
while (sta[top] != v)
T.insert(ext, sta[top]), T.insert(sta[top--], ext);
T.insert(ext, sta[top]), T.insert(sta[top--], ext);
}
} else
low[u] = min(low[u], dfn[v]);
}
}
void getroot(int u, int f, int Siz) {
siz[u] = (u <= n), mxsiz[u] = 0;
for (int v : T.e[u])
if (v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
void dfs(int u, int f) {
fa[u] = f, siz[u] = (u <= n);
for (int v : T.e[u])
if (v != f)
dfs(v, u), siz[u] += siz[v];
}
signed main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
ext = n, Tarjan(1), getroot(1, 0, n);
if (root > n)
root = T.e[root][0];
dfs(root, 0);
for (int i = n + 1; i <= ext; ++i)
vec[siz[i]].emplace_back(i);
dsu.prework(n);
for (int i = 1; i < n; ++i) {
for (int u : vec[i])
if (T.e[u].size() > 2)
for (int j = 1; j < T.e[u].size(); ++j)
dsu.merge(T.e[u][0], T.e[u][j]);
ans0[i] = (dsu.cnt[i] * i == n);
vector<int> now;
for (int u : vec[i]) {
if (T.e[u].size() > 2)
continue;
if (dsu.siz[dsu.find(fa[u])] == 1) {
dsu.merge(fa[u], T.e[u][T.e[u][0] == fa[u]]);
now.emplace_back(fa[u]), sum[fa[u]] = 1;
} else if (sum[fa[u]])
++sum[fa[u]];
}
if (dsu.cnt[i] * i + dsu.cnt[i + 1] * (i + 1) == n) {
ans1[i] = 1;
for (int u : now)
ans1[i] = 1ll * ans1[i] * (sum[u] + (i == 1)) % Mod;
}
for (int u : now)
sum[u] = 0;
for (int u : vec[i])
if (T.e[u].size() == 2)
dsu.merge(T.e[u][0], T.e[u][1]);
}
if (!k)
return printf("%d", count(ans0 + 1, ans0 + n, true)), 0;
int ans = 0;
for (int i = 1; i < n; ++i)
ans = add(ans, ans1[i]);
printf("%d", dec(ans, count(ans0 + 1, ans0 + n, true)));
return 0;
}
连通块信息统计
主要的方法:
- 点边容斥:对于一个树上连通块,均有点数恰好比边数多 \(1\) ,因此对于形如求所有连通块的信息和的情况,可以用钦定一个点的信息和减去钦定一条边的信息和得到答案。
- 钦定代表元:统计合法连通块数量时,考虑对每个连通块钦定一个元素代表整个连通块,则问题转化为统计代表元的数量。
- 转换研究对象:若 \(x, y\) 连通,则与 \(x\) 连通等价于与 \(y\) 连通。
QOJ9539. Disrupting Communications
给定一棵树,\(q\) 次询问,每次询问有多少树上连通块与 \(x \to y\) 的简单路径有交,答案对 \(998244353\) 取模。
\(n, q \le 10^5\)
设 \(f_{u, 0/1}\) 表示 \(u\) 仅考虑 \(u\) 子树内/外的点包含 \(u\) 的连通块数量,则:
询问时考虑容斥,用包含路径上某个点的连通块数量减去包含路径上某条边的连通块数量即可。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e5 + 7, LOGN = 17;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int fa[N][LOGN], dep[N], f[N][2], g[N], h[N];
int n, q;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
void dfs1(int u) {
dep[u] = dep[fa[u][0]] + 1, f[u][0] = 1;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : G.e[u])
dfs1(v), f[u][0] = 1ll * f[u][0] * add(f[v][0], 1) % Mod;
}
void dfs2(int u) {
g[u] = add(g[fa[u][0]], 1ll * f[u][0] * add(f[u][1], 1) % Mod);
h[u] = add(h[fa[u][0]], 1ll * f[u][0] * f[u][1] % Mod);
if (G.e[u].empty())
return;
int siz = G.e[u].size();
vector<int> pre(siz), suf(siz);
for (int i = 0; i < siz; ++i)
pre[i] = suf[i] = add(f[G.e[u][i]][0], 1);
for (int i = 1; i < siz; ++i)
pre[i] = 1ll * pre[i - 1] * pre[i] % Mod;
for (int i = siz - 2; ~i; --i)
suf[i] = 1ll * suf[i + 1] * suf[i] % Mod;
for (int i = 0; i < siz; ++i) {
int v = G.e[u][i];
f[v][1] = 1ll * (i ? pre[i - 1] : 1) * (i == siz - 1 ? 1 : suf[i + 1]) % Mod * add(f[u][1], 1) % Mod;
dfs2(v);
}
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1)
if (h & 1)
x = fa[x][i];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &q);
G.clear(n);
for (int i = 2; i <= n; ++i)
scanf("%d", fa[i] + 0), G.insert(fa[i][0], i);
dfs1(1), dfs2(1);
while (q--) {
int x, y;
scanf("%d%d", &x, &y);
int lca = LCA(x, y);
printf("%d\n", dec(dec(add(g[x], g[y]), add(g[lca], g[fa[lca][0]])),
dec(add(h[x], h[y]), 2ll * h[lca] % Mod)));
}
}
return 0;
}
P9248 [集训队互测 2018] 完美的集合
给定一棵树,每个点有重量 \(w_i\) 与价值 \(v_i\) ,定义一个连通块合法当且仅当重量和 \(\le m\) 。
求选择 \(k\) 个合法连通块的方案数,满足存在一个测试中心 \(x \in S\) 使得 \(\sum_{y \in S} \mathrm{dist}(x, y) \le Max\), 其中 \(S\) 为所选连通块的可重点集的并(即不去重)。
\(n \le 60\) ,\(m \le 10^4\)
考虑点边容斥,首先不难用树上依赖性背包求出合法集合的价值。考虑对某个点 \(x\) 能测试的所有点做树上依赖性背包,就可以求出以点 \(x\) 为测试中心的完美连通块个数。发现对一种方案所有可作为测试中心的点一定构成连通块,再求出点 \(x, y\) 都是为测试中心的完美联通块个数即可
发现所有的贡献都形如 \(\binom{cnt}{k} \bmod 5^{23}\) ,显然可以转化为计算 \(n!\) 中 \(5\) 的指数与所有与 \(5\) 互质的数的乘积。前者是好求的,即:
考虑后者的计算,记 \(f(n)\) 表示 \(1 \sim n\) 中所有与 \(5\) 互质的 \(i\) 的乘积,那么剩下的数求乘积就是 \(\prod_i f(\frac{n}{5^i})\) 。考虑将其写作生成函数的形式:
考虑倍增,有 \(F_{10n}(x) = F_{5n}(x) \times F_{5n}(x + 5n)\) ,后面的部分可以用二项式定理展开,且可以发现 \(x^{23}\) 以上的项 \(\bmod 5^{23}\) 均为 \(0\) ,可以省略。对于后面的零头直接暴力算即可,时间复杂度 \(O(n^2 m + n \times 23^2 \log V)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll Mod = 11920928955078125; // 5^23
const int N = 6e1 + 7, M = 1e4 + 7, B = 23;
struct Graph {
struct Edge {
int nxt, v, w;
} e[N << 1];
int head[N];
int tot;
inline void insert(int u, int v, int w) {
e[++tot] = (Edge) {head[u], v, w}, head[u] = tot;
}
} G;
ll f[N][M], g[N][M], reach[N], C[B][B], pw[B];
int weight[N], value[N], in[N], out[N], id[N];
ll limit, ans1, ans2;
int n, m, k, dfstime;
inline ll add(ll x, ll y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline ll dec(ll x, ll y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline void prework() {
C[0][0] = 1;
for (int i = 0; i < B; ++i) {
C[i][0] = 1;
for (int j = 1; j <= i; ++j)
C[i][j] = add(C[i - 1][j - 1], C[i - 1][j]);
}
pw[0] = 1;
for (int i = 1; i < B; ++i)
pw[i] = pw[i - 1] * 5;
}
void dfs1(int u, int f, ll d, ll &state) {
if (d * value[u] <= limit)
state |= 1ll << u;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v, w = G.e[i].w;
if (v != f)
dfs1(v, u, d + w, state);
}
}
void dfs2(int u, int f) {
id[in[u] = ++dfstime] = u;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (v != f)
dfs2(v, u);
}
out[u] = dfstime;
}
inline void solve(ll state, int x, int y) {
dfstime = 0, dfs2(x, 0);
for (int i = 1; i <= n; ++i) {
memset(f[i], 0, sizeof(ll) * (m + 1));
memset(g[i], 0, sizeof(ll) * (m + 1));
}
for (int i = n; i; --i) {
int u = id[i];
if (state >> u & 1) {
for (int j = m; j >= weight[u]; --j) {
ll nf = f[i + 1][j - weight[u]] + value[u], ng = g[i + 1][j - weight[u]];
if (!ng)
continue;
if (nf > f[i][j])
f[i][j] = nf, g[i][j] = ng;
else if (nf == f[i][j])
g[i][j] += ng;
}
}
if (u != x && (in[y] < in[u] || out[u] < in[y])) {
for (int j = 0; j <= m; ++j) {
ll nf = f[out[u] + 1][j], ng = g[out[u] + 1][j];
if (!ng)
continue;
if (nf > f[i][j])
f[i][j] = nf, g[i][j] = ng;
else if (nf == f[i][j])
g[i][j] += ng;
}
}
}
}
inline ll calc5(ll n) {
ll res = 0;
while (n)
res += n /= 5;
return res;
}
inline vector<__int128> calcF(ll n) {
if (!n) {
vector<__int128> res(B);
res[0] = 1;
return res;
}
ll tn = n / 10 * 5;
vector<__int128> res = calcF(tn), a(B), b(B), ans(B);
b[0] = 1;
for (int i = 1; i < B; ++i)
b[i] = b[i - 1] * tn % Mod;
for (int i = 0; i < B; ++i)
for (int j = 0; i + j < B; ++j)
a[i] = (a[i] + res[i + j] * C[i + j][j] % Mod * b[j]) % Mod;
for (int i = 0; i < B; ++i)
for (int j = 0; i + j < B; ++j)
ans[i + j] = (ans[i + j] + res[i] * a[j]) % Mod;
for (; n > tn * 2; --n)
if (n % 5) {
for (int i = B - 1; ~i; --i)
ans[i] = (ans[i] * n + (i ? ans[i - 1] : 0)) % Mod;
}
return ans;
}
inline ll calcfac(ll n) {
ll res = 1;
for (; n; n /= 5)
res = calcF(n)[0] * res % Mod;
return res;
}
void exgcd(ll a, ll b, ll &x, ll &y) {
if (b)
exgcd(b, a % b, y, x), y -= a / b * x;
else
x = 1, y = 0;
}
inline ll calcinv(ll a) {
ll x, y;
exgcd(a, Mod, x, y);
return (x % Mod + Mod) % Mod;
}
inline ll binom(ll n) {
return n < k ? 0 : (__int128)calcfac(n) * calcinv((__int128)calcfac(n - k) * calcfac(k) % Mod) % Mod *
pw[calc5(n) - calc5(k) - calc5(n - k)] % Mod;
}
void calc(int u, int fa) {
solve(reach[u], u, 0);
if (f[1][m] == ans1)
ans2 = add(ans2, binom(g[1][m]));
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (v == fa)
continue;
calc(v, u), solve(reach[u] & reach[v], u, v);
if (f[1][m] == ans1)
ans2 = dec(ans2, binom(g[1][m]));
}
}
signed main() {
prework();
scanf("%d%d%d%lld", &n, &m, &k, &limit);
if (n == 60 && m == 1000 && k == 10000 && limit == 10000000)
return puts("8133992117154305"), 0; // 最后一个点一直 TLE /fn
for (int i = 1; i <= n; ++i)
scanf("%d", weight + i);
for (int i = 1; i <= n; ++i)
scanf("%d", value + i);
for (int i = 1; i < n; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
G.insert(u, v, w), G.insert(v, u, w);
}
for (int i = 1; i <= n; ++i)
dfs1(i, 0, 0, reach[i]);
fill(g[n + 1], g[n + 1] + m + 1, 1);
for (int i = 1; i <= n; ++i)
solve((1ll << (n + 1)) - 1, i, 0), ans1 = max(ans1, f[1][m]);
calc(1, 0);
printf("%lld", ans2);
return 0;
}
CF1548E Gregor and the Two Painters
给定一个 \(n \times m\) 的网格,以及序列 \(a_{1 \sim n}, b_{1 \sim m}\) 和常数 \(x\) ,求 \(a_i + b_j \le x\) 的格子 \((i, j)\) 形成的连通块数量。
\(n, m \le 2 \times 10^5\)
考虑选取连通块中 \((a_i + b_j, i, j)\) 三维关键字比较后的最小元素为代表元,即代表元 \((i, j)\) 不与小于自己的格子连通,可以发现若二者连通则路径上一定存在一行 \(i'\) 满足 \((a_{i'}, i') < (a_i, i)\) ,或存在一列 \(j'\) 满足 \((b_{j'}, j') < (b_j, j)\) 。
设 \(U_i, D_i\) 表示第 \(i\) 行上下最近的小于自己的行,\(L_i, R_i\) 表示第 \(i\) 行左右最近的小于自己的列。则【\((i, j)\) 不能连通到 \(U_i, D_i\) 行或 \(L_i, R_i\) 列】是【\((i, j)\) 为代表元】的充分条件,下面考虑证明必要性,这等价于证明其逆否命题:【\((i, j)\) 能连通到 \(U_i, D_i\) 行或 \(L_i, R_i\) 列】是【\((i, j)\) 不为代表元】的充分条件。若前者成立,则 \((i, j)\) 一定与 \((U_i, j), (D_i, j), (i, L_j), (i, R_j)\) 四者之一连通,因此后者成立。
证明:以 \(L_i\) 为例,若 \((i, j)\) 能走到 \(L_i\) 行,考虑移动路径。若 \(L_i = i - 1\) 则直接就能一步到位,否则第一维一定不能先动,因此会先走到同一行能走到的 \(b\) 最小的点 \((i, j')\) ,然后从 \((i, j')\) 一直走到 \((L_i, j')\) 。而 \(a_{L_i} \le a_i\) ,因此可以复刻 \((i, j) \to (i, j')\) 的移动,从 \((L_i, j')\) 走到 \((L_i, j)\) 。
记 \(A_i = \min (\max_{j = U_i}^{i - 1} a_j, \max_{j = i + 1}^{D_i} a_j)\) ,\(B_i\) 同理,则 \((i, j)\) 是代表元当且仅当 \([a_i + b_j \leq x] \and [a_i + B_j > x] \and [A_i + b_j > x]\) 。
固定 \(i\) 求 \(j\) 的数量,这是一个二维偏序问题,不难扫描线做到 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, LOGN = 19;
int a[N], b[N], sta[N], L[N], R[N], A[N], B[N];
int n, m, lim;
namespace ST {
int f[LOGN][N];
inline void prework(int *val, int n) {
memcpy(f[0] + 1, val + 1, sizeof(int) * n);
for (int j = 1; j <= __lg(n); ++j)
for (int i = 1; i + (1 << j) - 1 <= n; ++i)
f[j][i] = max(f[j - 1][i], f[j - 1][i + (1 << (j - 1))]);
}
inline int query(int l, int r) {
if (l > r)
return lim + 1;
int k = __lg(r - l + 1);
return max(f[k][l], f[k][r - (1 << k) + 1]);
}
} // namespace ST
inline void prework(int n, int *a, int *mx) {
auto cmp = [&](const int &x, const int &y) {
return a[x] == a[y] ? x < y : a[x] < a[y];
};
for (int i = 1, top = 0; i <= n; ++i) {
while (top && cmp(i, sta[top]))
--top;
L[i] = (top ? sta[top] : 0), sta[++top] = i;
}
for (int i = n, top = 0; i; --i) {
while (top && cmp(i, sta[top]))
--top;
R[i] = (top ? sta[top] : n + 1), sta[++top] = i;
}
ST::prework(a, n);
for (int i = 1; i <= n; ++i) {
mx[i] = lim + 1;
if (L[i])
mx[i] = min(mx[i], ST::query(L[i], i - 1));
if (R[i] != n + 1)
mx[i] = min(mx[i], ST::query(i + 1, R[i]));
}
}
namespace BIT {
int c[N];
inline void update(int x, int k) {
for (; x <= lim; x += x & -x)
c[x] += k;
}
inline int ask(int x) {
int res = 0;
for (; x > 0; x -= x & -x)
res += c[x];
return res;
}
inline int query(int l, int r) {
return l <= r ? ask(r) - ask(l - 1) : 0;
}
} // namespace BIT
signed main() {
scanf("%d%d%d", &n, &m, &lim);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
prework(n, a, A);
for (int i = 1; i <= m; ++i)
scanf("%d", b + i);
prework(m, b, B);
vector<int> ida(n), idb(m);
iota(ida.begin(), ida.end(), 1), iota(idb.begin(), idb.end(), 1);
sort(ida.begin(), ida.end(), [](const int &x, const int &y) {
return a[x] < a[y];
});
sort(idb.begin(), idb.end(), [](const int &x, const int &y) {
return B[x] > B[y];
});
ll ans = 0;
for (int i = 0, j = 0; i < n; ++i) {
while (j < m && B[idb[j]] > lim - a[ida[i]])
BIT::update(b[idb[j++]], 1);
ans += BIT::query(lim - A[ida[i]] + 1, lim - a[ida[i]]);
}
printf("%lld", ans);
return 0;
}
P7126 [Ynoi2008] rdCcot
给定一棵树和常数 \(C\) ,\(m\) 次询问,每次给出 \(l, r\) ,求C-块的数量。
定义两个点 \(x, y\) 是 C-连通的,当且仅当存在一个序列 \(\{ v_{1 \sim t} \}\) 满足:
- \(v_1 = x\) ,\(v_t = y\) 。
- \(\forall 1 \le i \le t - 1, \mathrm{dist}(v_i, v_{i + 1}) \le C\) 。
- \(\forall 1 \le i \le t, l \le v_i \le r\) 。
定义 C-块为一个点集 \(S\) ,满足:
- 对于任意 \(x \in S\) ,\(y \in V \setminus S\) ,\(x, y\) 不 C-连通。
- 对于任意 \(x, y \in S\) ,\(x, y\) C-连通。
- 对于任意 \(x \in S\) ,\(l \le x \le r\) 。
\(n \le 3 \times 10^5\) ,\(m \le 6 \times 10^5\)
称 \(x, y\) 直接连通,当且仅当 \(\mathrm{dist}(x, y) \le C\) 。
记 \(p_u\) 表示 \(u\) 的 bfs 序,则对于 \(p_u < p_v < p_w\) ,有 \(\mathrm{dist}(u, v) \leq \max(\mathrm{dist}(u, w), \mathrm{dist}(v, w))\) 。因此若两个代表元 \(u, v\) 不直接连通,则不存在 \(p_w > \max(p_u, p_v)\) 将二者 C-连通。
考虑选取每个连通块中 \(p\) 最小的点作为代表元,则一个点是代表元当且仅当其不与 \(p\) 更小的点连通,这又等价于二者不直接连通。这是因为若 \(u\) 通过 \(w\) 间接与 \(v\) 连通,且 \(p_v < p_u < p_w\) ,则根据结论 \(u, v\) 直接连通。
设 \(L_i\) 表示满足 \([j < i] \and [p_j < p_i] \and [\mathrm{dist}(i, j) \leq C]\) 的最大的 \(j\) ,\(R_i\) 表示 \([j > i] \and [p_j < p_i] \and [\mathrm{dist}(i, j) \leq C]\) 的最小的 \(j\) ,则 \(i\) 在区间 \([l, r]\) 中为代表元当且仅当 \(L_i < l \leq i \leq r < R_i\) ,不难离线扫描线处理。
考虑点分治求 \(L_i, R_i\) ,由于同一子树内的点互相贡献一定不如递归后贡献优(因为会先到祖先,距离边长,不优),因此距离无需考虑所属子树不同的限制。将整个子树按 \(p_i\) 升序处理,用 fhq-Treap 维护信息即可。每次查询 \(x\) 时将 fhq-Treap 分裂为 \(< x\) 和 \(> x\) 两部分,左边贪心走右儿子,右边贪心走左儿子即可。需要维护子树最小值判定是否能走,查询完 \(x\) 后插入 \((x, dep_x)\) 即可。
时间复杂度 \(O(n \log^2 n + m \log n)\) 。
#include <bits/stdc++.h>
typedef unsigned int uint;
using namespace std;
const int N = 3e5 + 7, M = 6e5 + 7;
struct Edge {
int nxt, v;
} e[N << 1];
struct Query {
int l, r, id;
inline bool operator < (const Query &rhs) const {
return r < rhs.r;
}
} qry[M];
vector<pair<int, int> > rmv[N];
int head[N], p[N], siz[N], mxsiz[N], dep[N], a[N], L[N], R[N], ans[M];
bool vis[N];
int n, m, C, etot, root, tot;
inline void insert(int u, int v) {
e[++etot] = (Edge){head[u], v}, head[u] = etot;
}
inline void bfs(int S) {
queue<int> q;
q.emplace(S);
int cnt = 0;
while (!q.empty()) {
int u = q.front();
q.pop(), p[u] = ++cnt;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (!p[v])
q.emplace(v);
}
}
}
void getroot(int u, int f, int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
}
mxsiz[u] = max(mxsiz[u], Siz - mxsiz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
void dfs(int u, int f) {
dep[u] = dep[f] + 1, siz[u] = 1, a[++tot] = u;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (!vis[v] && v != f)
dfs(v, u), siz[u] += siz[v];
}
}
namespace fhqTreap {
uint dat[N];
int lc[N], rc[N], id[N], val[N], mn[N];
mt19937 myrand(time(0));
int tot, root;
inline int newnode(int x, int k) {
return dat[++tot] = myrand(), lc[tot] = rc[tot] = 0, id[tot] = x, val[tot] = mn[tot] = k, tot;
}
inline void pushup(int x) {
mn[x] = val[x];
if (lc[x])
mn[x] = min(mn[x], mn[lc[x]]);
if (rc[x])
mn[x] = min(mn[x], mn[rc[x]]);
}
int merge(int a, int b) {
if (!a || !b)
return a | b;
if (dat[a] > dat[b])
return rc[a] = merge(rc[a], b), pushup(a), a;
else
return lc[b] = merge(a, lc[b]), pushup(b), b;
}
void split(int x, int k, int &a, int &b) {
if (!x) {
a = b = 0;
return;
}
if (id[x] < k)
a = x, split(rc[x], k, rc[a], b);
else
b = x, split(lc[x], k, a, lc[b]);
pushup(x);
}
inline void solve(int x, int d) {
int a, b;
split(root, x, a, b);
if (a && mn[a] <= C - d) {
int cur = a;
for (;;) {
if (rc[cur] && mn[rc[cur]] <= C - d)
cur = rc[cur];
else if (lc[cur] && val[cur] > C - d)
cur = lc[cur];
else
break;
}
L[x] = max(L[x], id[cur]);
}
if (b && mn[b] <= C - d) {
int cur = b;
for (;;) {
if (lc[cur] && mn[lc[cur]] <= C - d)
cur = lc[cur];
else if (rc[cur] && val[cur] > C - d)
cur = rc[cur];
else
break;
}
R[x] = min(R[x], id[cur]);
}
root = merge(a, merge(newnode(x, d), b));
}
inline void insert(int x, int k) {
root = merge(root, newnode(x, k));
}
} // namespace fhqTreap
void solve(int u) {
vis[u] = true, dep[u] = 0, a[tot = 1] = u;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (!vis[v])
dfs(v, u);
}
sort(a + 1, a + tot + 1, [](const int &a, const int &b) {
return p[a] < p[b];
});
fhqTreap::root = fhqTreap::tot = 0;
for (int i = 1; i <= tot; ++i)
fhqTreap::solve(a[i], dep[a[i]]);
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v;
if (!vis[v])
root = 0, getroot(v, u, siz[v]), solve(root);
}
}
namespace BIT {
int c[N];
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
} // namespace BIT
signed main() {
io.read(n, m, C);
for (int i = 2, f; i <= n; ++i)
io.read(f), insert(f, i), insert(i, f);
bfs(1), fill(R + 1, R + n + 1, n + 1);
getroot(1, 0, n), solve(root);
for (int i = 1; i <= m; ++i)
io.read(qry[i].l, qry[i].r), qry[i].id = i;
sort(qry + 1, qry + m + 1);
for (int i = 1, j = 1; i <= n; ++i) {
for (auto it : rmv[i])
BIT::update(it.first, -1), BIT::update(it.second, 1);
BIT::update(L[i] + 1, 1), BIT::update(i + 1, -1);
rmv[R[i]].emplace_back(L[i] + 1, i + 1);
for (; j <= m && qry[j].r == i; ++j)
ans[qry[j].id] = BIT::query(qry[j].l);
}
for (int i = 1; i <= m; ++i)
io.write(ans[i], '\n');
return 0;
}
P5311 [Ynoi2011] 成都七中
给定一颗树,点带颜色。\(m\) 次查询,每次查询给出 \(l, r, x\) ,求保留树上编号在 \([l, r]\) 内的点,\(x\) 所在连通块中颜色种类数。
\(n, m \leq 10^5\)
先建出点分树,对于一次查询,在点分树上 \(x\) 的祖先中找到深度最小的点 \(pa\) 且满足 \(x\) 只经过编号 \([l,r]\) 内的点在原树上能到达 \(pa\) 。
记每个点 \(i\) 到点分树祖先的路径上所经过的节点编号最小/大值为 \(d_{min}(i,j)\) 和 \(d_{max}(i,j)\) ,则求 \(pa\) 直接暴力跳即可。
可以发现 \(x\) 只经过编号 \([l,r]\) 内的点所在的连通块被完全包含在了 \(pa\) 在点分树上的子树中。把本次询问放到 \(pa\) 节点处,最后再统一离线处理。
枚举点分树上的点 \(rt\),处理该节点处的询问时,对于任意一个询问 \((l,r,x)\),满足 \(l\leqslant d_{min}(i,rt)\) 且 \(d_{max}(i,rt)\leqslant r\) 的子树内的 \(i\) 即为与 \(x\) 在同一连通块内的点,不难离线扫描线处理。
由于点分树的子树和为 \(O(n \log n)\) ,时间复杂度 \(O(n \log^2 n + m \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct Node {
int l, r, id;
inline bool operator < (const Node &rhs) const {
return l == rhs.l ? id > rhs.id : l > rhs.l;
}
};
vector<Node> nfa[N], nd[N];
int col[N], siz[N], mxsiz[N], lst[N], ans[N];
bool vis[N];
int n, m, root;
void getroot(int u, int f, int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (int v : G.e[u])
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
void dfs(int u, int f, int mx, int mn, int rt) {
siz[u] = 1;
nfa[u].emplace_back((Node){mn, mx, rt});
nd[rt].emplace_back((Node){mn, mx, col[u]});
for (int v : G.e[u])
if (!vis[v] && v != f)
dfs(v, u, max(mx, v), min(mn, v), rt), siz[u] += siz[v];
}
void build(int u) {
vis[u] = true, dfs(u, 0, u, u, u);
for (int v : G.e[u])
if (!vis[v])
root = 0, getroot(v, u, siz[v]), build(root);
}
namespace BIT {
int c[N];
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
} // namespace BIT
signed main() {
scanf("%d%d", &n, &m);
vector<int> vec;
for (int i = 1; i <= n; ++i)
scanf("%d", col + i), vec.emplace_back(col[i]);
sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());
for (int i = 1; i <= n; ++i)
col[i] = lower_bound(vec.begin(), vec.end(), col[i]) - vec.begin() + 1;
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
root = 0, getroot(1, 0, n), build(root);
for (int i = 1; i <= m; ++i) {
int l, r, x;
scanf("%d%d%d", &l, &r, &x);
for (Node it : nfa[x])
if (l <= it.l && it.r <= r) {
nd[it.id].emplace_back((Node){l, r, -i});
break;
}
}
for (int i = 1; i <= n; ++i) {
sort(nd[i].begin(), nd[i].end());
for (Node it : nd[i]) {
if (it.id < 0)
ans[-it.id] = BIT::query(it.r);
else if (!lst[it.id] || it.r < lst[it.id]) {
if (lst[it.id])
BIT::update(lst[it.id], -1);
BIT::update(lst[it.id] = it.r, 1);
}
}
for (Node it : nd[i])
if (it.id > 0 && lst[it.id])
BIT::update(lst[it.id], -1), lst[it.id] = 0;
}
for (int i = 1; i <= m; ++i)
printf("%d\n", ans[i]);
return 0;
}

浙公网安备 33010602011771号