BCCs y 2CCs
| Fuente | Recurso | Notas |
|---|---|---|
| CF | DFS Tree + Bridges | |
| CP2 | 4.2.8 - Articulation Points & Bridges |
Componentes 2-arista-conexas
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| YS | Two-Edge-Connected Components | Fácil | 2CC | en el módulo |
Implementación
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 3e5;
int timer; // Tiempo de entrada en el nodo
int scc; // Número de componentes fuertemente conexas
int id[MAX_N];
int low[MAX_N]; // ID más bajo en el subárbol del nodo en el árbol DFS
vector<int> neighbors[MAX_N];
vector<int> two_edge_components[MAX_N];
stack<int> st; // Lleva el registro del camino en nuestro DFS
void dfs(int node, int parent = -1) {
id[node] = low[node] = ++timer;
st.push(node);
bool multiple_edges = false;
for (int child : neighbors[node]) {
if (child == parent && !multiple_edges) {
multiple_edges = true;
continue;
}
if (!id[child]) {
dfs(child, node);
low[node] = min(low[node], low[child]);
} else {
low[node] = min(low[node], id[child]);
}
}
if (low[node] == id[node]) {
while (st.top() != node) {
two_edge_components[scc].push_back(st.top());
st.pop();
}
two_edge_components[scc++].push_back(st.top());
st.pop();
}
}
int main() {
int n, m;
cin >> n >> m;
for (int i = 0; i < m; i++) {
int x, y;
cin >> x >> y;
neighbors[x].push_back(y);
neighbors[y].push_back(x);
}
for (int node = 0; node < n; node++) {
if (id[node] == 0) { dfs(node); }
}
cout << scc << '\n';
for (int i = 0; i < scc; i++) {
cout << two_edge_components[i].size() << ' ';
for (int node : two_edge_components[i]) { cout << node << ' '; }
cout << '\n';
}
}Con DSU
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| Platinum | Disruption | Normal | Merging | Solución |
El análisis del problema de arriba menciona una solución . Aunque esto no es un problema de componentes 2-conexas, de hecho podemos usar DSU para generar componentes 2-conexas.
Las operaciones de DSU toman en lugar de porque el DSU no usa unión por tamaño, pero es fácil cambiar esto.
struct TwoEdgeCC {
struct {
vi e;
void init(int n) { e = vi(n, -1); }
int get(int x) { return e[x] < 0 ? x : e[x] = get(e[x]); }
bool unite(int x, int y) { // set par[y] = x
x = get(x), y = get(y);
if (x == y) return 0;
e[x] += e[y];
e[y] = x;
return 1;
}
} DSU;
int N;
vector<vi> adj;
vi depth, par;
vpi extra;
void init(int _N) {
N = _N;
DSU.init(N);
adj.rsz(N), depth.rsz(N), par = vi(N, -1);
}
void dfs(int x) {
trav(t, adj[x]) if (t != par[x]) par[t] = x, depth[t] = depth[x] + 1, dfs(t);
}
void ae(int a, int b) {
if (DSU.unite(a, b)) adj[a].pb(b), adj[b].pb(a); // edge of forest
else extra.pb({a, b}); // extra edge
}
void ad(int a, int b) {
while (1) {
a = DSU.get(a), b = DSU.get(b);
if (a == b) return;
if (depth[a] < depth[b]) swap(a, b);
assert(par[a] != -1 && DSU.unite(par[a], a));
}
}
void gen() {
F0R(i, N)
if (par[i] == -1) dfs(i); // independently for each connected component
DSU.init(N);
trav(t, extra) ad(t.f, t.s); // add non-spanning edges
};Problemas
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CEOI | 2017 - One-Way Streets | Fácil | BCC | — |
- SRM 787 1000
Componentes biconexas
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CSES | Forbidden Cities | Normal | BCC | en el módulo |
Explicación
Una observación importante es que si no se puede ir del nodo al nodo sin pasar por el nodo , el nodo es un nodo crítico (punto de articulación). El nodo puede separar y en 2 componentes distintas si se elimina. Esto nos hace pensar en componentes biconexas.
Ahora quedan dos casos. Si el nodo no es crítico, un camino de a puede evitar el nodo. En caso contrario, si el nodo es crítico, hay que comprobar si está en el camino de a . Aquí hay un poco de truco: en un grafo simple no es tan fácil de comprobar; en cambio, comprobarlo en un árbol puede ser mucho más fácil. Para ello, transformamos el grafo en un árbol block-cut .
En un árbol block-cut, cada articulación y cada componente biconexa representa un nodo. Ahora que convertimos nuestro grafo en un árbol, ¿cómo comprobamos si el camino de a pasa por ? Para esto usamos LCA. Se puede leer más aquí .
Implementación
#include <bits/stdc++.h>
using namespace std;
/** @return the block-cut tree of a graph */
vector<vector<int>> biconnected_components(vector<vector<int>> &g,
vector<bool> &is_cutpoint, vector<int> &id) {
int n = (int)g.size();
vector<vector<int>> comps;
vector<int> stk;
vector<int> num(n);
vector<int> low(n);
is_cutpoint.resize(n);
// Halla las componentes biconexas
function<void(int, int, int &)> dfs = [&](int node, int parent, int &timer) {
num[node] = low[node] = ++timer;
stk.push_back(node);
for (int son : g[node]) {
if (son == parent) { continue; }
if (num[son]) {
low[node] = min(low[node], num[son]);
} else {
dfs(son, node, timer);
low[node] = min(low[node], low[son]);
if (low[son] >= num[node]) {
is_cutpoint[node] = (num[node] > 1 || num[son] > 2);
comps.push_back({node});
while (comps.back().back() != son) {
comps.back().push_back(stk.back());
stk.pop_back();
}
}
}
}
};
int timer = 0;
dfs(0, -1, timer);
id.resize(n);
// Construir el árbol block-cut
function<vector<vector<int>>()> build_tree = [&]() {
vector<vector<int>> t(1);
int node_id = 0;
for (int node = 0; node < n; node++) {
if (is_cutpoint[node]) {
id[node] = node_id++;
t.push_back({});
}
}
for (auto &comp : comps) {
int node = node_id++;
t.push_back({});
for (int u : comp)
if (!is_cutpoint[u]) {
id[u] = node;
} else {
t[node].push_back(id[u]);
t[id[u]].push_back(node);
}
}
return t;
};
return build_tree();
}
int main() {
int n, m, q;
cin >> n >> m >> q;
const int LOGMAX = (int)ceil(log2(n));
vector<vector<int>> g;
g.resize(n);
for (int i = 0; i < m; i++) {
int x, y;
cin >> x >> y;
x--, y--;
g[x].push_back(y);
g[y].push_back(x);
}
vector<int> id, depth(3 * n, 0);
vector<bool> is_cutpoint;
vector<vector<int>> up(3 * n, vector<int>(LOGMAX));
vector<vector<int>> blockcut_tree = biconnected_components(g, is_cutpoint, id);
// Calcular el LCA
function<void(int, int)> dfs = [&](int node, int parent) {
if (parent != -1) {
depth[node] = depth[parent] + 1;
up[node][0] = parent;
for (int j = 1; j < LOGMAX; j++) {
up[node][j] = up[up[node][j - 1]][j - 1];
}
}
for (auto son : blockcut_tree[node]) {
if (son == parent) continue;
dfs(son, node);
}
};
auto find_lca = [&](int x, int y) {
if (depth[x] < depth[y]) { swap(x, y); }
int diff = depth[x] - depth[y];
for (int i = LOGMAX; i >= 0; i--) {
if ((1 << i) & diff) { x = up[x][i]; }
}
if (x == y) { return x; }
for (int i = LOGMAX; i >= 0; i--) {
if (up[x][i] != up[y][i]) {
x = up[x][i];
y = up[y][i];
}
}
return up[x][0];
};
// Comprobar si el nodo z está en el camino (x, y)
auto on_path = [&](int x, int y, int z) {
int lca = find_lca(x, y);
int lca1 = find_lca(x, z);
int lca2 = find_lca(y, z);
if (lca == z || (lca1 == lca && lca2 == z) || (lca2 == lca && lca1 == z)) {
return true;
}
return false;
};
dfs(0, -1);
for (int i = 0; i < q; i++) {
int a, b, c;
cin >> a >> b >> c;
a--, b--, c--;
// El camino no existe en dos casos:
// 1) a == c o b == c
// 2) c es un punto de corte y está en el camino entre a y b en el
// árbol block-cut
if (a == c || b == c || (is_cutpoint[c] && on_path(id[a], id[b], id[c]))) {
std::cout << "NO\n";
} else {
cout << "YES" << '\n';
}
}
}Puntos de articulación
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| SPOJ | SUBMERGE - Submerging Islands | Normal | BCC, Articulation Points | en el módulo |
| Fuente | Recurso | Notas |
|---|---|---|
| CP2 | Articulation Points (aka Cut Vertices) | quizá no del todo correcto |
Implementación
#include <bits/stdc++.h>
using namespace std;
const int NMAX = 2e4;
int timer;
vector<int> low, id;
vector<bool> visited, ap;
vector<vector<int>> g(NMAX);
void dfs(int node, int parent) {
visited[node] = true;
id[node] = low[node] = timer++;
int children = 0;
for (int son : g[node]) {
if (son == parent) { continue; }
if (!visited[son]) {
dfs(son, node);
low[node] = min(low[node], low[son]);
children++;
// Comprobar si el nodo actual es un punto de articulación
if (low[son] >= id[node] && parent != -1) { ap[node] = true; }
} else {
low[node] = min(low[node], id[son]);
}
}
if (parent == -1 && children > 1) { ap[node] = true; }
}
int main() {
int n, m;
while (cin >> n >> m && n && m) {
id.assign(n, 0);
ap.assign(n, 0);
low.assign(n, 0);
visited.assign(n, 0);
for (int i = 0; i < n; i++) { g[i].clear(); }
timer = 0;
for (int i = 0; i < m; i++) {
int x, y;
cin >> x >> y;
g[--x].push_back(--y);
g[y].push_back(x);
}
dfs(0, -1);
int articualation_points = 0;
for (int i = 0; i < n; i++) {
if (ap[i]) { articualation_points++; }
}
cout << articualation_points << '\n';
}
}Problemas
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| CSES | Strongly Connected Edges | Fácil | BCC | Solución | |
| POI | 2008 - Blockade | Normal | BCC | Solución | |
| APIO | 2018 - Duathlon | Normal | BCC | Solución | |
| POI | 2016 - Amusing Journeys | Normal | BCC | — | |
| TLE | Investment | Difícil | BCC | — | |
| CEOI | 2015 - Pipes | Difícil | BCC | Solución | |
| Platinum | Push a Box | Muy difícil | BCC | — |