连通性相关
连通性相关
强连通分量
有向强连通图:任意两个点可以互相到达的有向图。
强连通分量(SCC):极大的强连通子图。
Tarjan 算法
维护一个栈存储搜索到的还未确定强连通分量的点,定义:
- \(dfn_u\) :节点 \(u\) 被搜索到的次序。
- \(low_u\) :\(u\) 子树中能回溯到的最小的 \(dfn\) 。
对于 \(u\) 的出点 \(v\) ,分类讨论:
-
\(v\) 未被访问过:继续 dfs ,并用 \(low_v\) 更新 \(low_u\) ,这是因为 \(v\) 可以回溯到的栈中点 \(u\) 一定可以回溯到。
-
\(v\) 被访问过:
-
已在栈中:根据 \(low\) 的定义,用 \(dfn_v\) 更新 \(low_u\) 。
-
不在栈中:说明 \(v\) 已搜索完毕,其所在的连通分量已被处理,不用管它。
-
对于一个强连通分量,不难发现只有一个 \(u\) 满足 \(dfn_u = low_u\) ,其一定是这个强连通分量的根(dfs 树上的最浅点)。因此回溯过程中,若 \(dfn_u = low_u\) ,则新增一个强连通分量。
时间复杂度 \(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 的时间,这样就可以直接线段树合并回答询问了。
考虑整体二分,假设目前已知 \([L, R]\) 内的边合并 SCC 的时间 \(\in [l, r]\) ,初始时每条边的时间 \(t\) 设为加入时间。记 \(mid = \lfloor \frac{l + r}{2} \rfloor\) ,拿出所有 \(t \le mid\) 的边跑一边 Tarjan,此时两端不在同一 SCC 的边均有 \(t > mid\) ,由此可以递归处理。
求出每条边合并 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, tl = 0, tr = 0;
for (int i = L; i <= R; ++i) {
if (e[i].t <= mid)
el[++tl] = e[i];
else
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();
for (int i = L; i < L + tl; ++i)
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);
for (int i = L + tl - 1; i >= L; --i)
if (!leader[e[i].u] || !leader[e[i].v] || leader[e[i].u] != leader[e[i].v])
e[i].t = mid + 1;
tl = 0, tr = 0;
for (int i = L; i <= R; ++i) {
if (e[i].t <= mid)
el[++tl] = e[i];
else
er[++tr] = e[i];
}
memcpy(e + L, el + 1, sizeof(Edge) * tl), memcpy(e + L + tl, er + 1, sizeof(Edge) * tr);
for (int i = L + tl; i <= R; ++i) {
if (leader[e[i].u])
e[i].u = leader[e[i].u];
if (leader[e[i].v])
e[i].v = leader[e[i].v];
}
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\) 的候选集合,则一定存在一组解满足每个值均为候选集合中的值,若候选集和为空则任意。
三元组的约束不好处理,考虑转化为二元组。
对于一组约束 \((x, y, z, k)\) ,若 \(a_i < x\) 则 \(a_j, a_k \ge x\) ,剩余同理,而这些限制就足以表示原约束。
考虑建图,记 \((i, k) = [a_i \ge k]\) ,则连边除了每条信息的约束,对于同一个 \(i\) 的候选集和还需连一条链,表示若 \(k_1 \ge k_2\) 且 \(a_i \ge k_1\) 则 \(a_i \ge k_2\) 。
时间复杂度 \(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]);
}
}
边双连通
定义:
- 边双连通图:不存在割边的无向连通图。
- 边双连通分量:极大边双连通子图。
- 边双连通:处在同一边双连通分量内的任意两点边双连通。
边双的一些性质:
- 边双对点有传递性。
- 每个点恰属于一个边双。
- 对于边双内任意点 \(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'\) 中任意两个连通块大小之差不超过 \(k\) 。
答案对 \(998244353\) 取模。
\(3 \le n \le 10^5\),\(n - 1 \le m \le 2 \times 10^5\),\(0 \le k \le 1\)
由于 \(G - E'\) 恰有 \(|V'|\) 个连通块,故 \(V'\) 中的每个点在 \(G - E'\) 分别属于不同的连通块,且仅通过 \(E'\) 能使得 \(V'\) 连通。
对于选择的两个点,若存在一条简单路径连接它们,则路径上的每个点都要选择。进一步的,若一个点双里面选了两个点,则整个点双都要选。
建出圆方树,称删去一个方点表示选其所有邻域圆点,则删去的方点通过圆点直接构成一个连通块。
考虑第三条限制,删去选择的方点后,每个连通块大小(圆点数量)相差不超过 \(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\) 的方案数减去连通块大小均为 \(d\) 的方案数。
对于 \(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;
}