Tác giả:
Reviewer:
Một số mảng quan trọng trong cây DFS :
Nhận xét : Các đỉnh có thứ tự thăm nằm trong khoảng từ $num[u]$ đến $tail[u]$ chính là các đỉnh nằm trong cây con gốc $u$ trong cây $DFS$.
Cách tính mảng low[], num[], tail[] :
int timeDfs = 0; // Thứ tự duyệt DFS
void dfs(int u, int pre) {
num[u] = low[u] = ++timeDfs;
for (int v : g[u]){
if (v == pre) continue;
if (!num[v]) {
dfs(v, u);
low[u] = min(low[u], low[v]);
}
else low[u] = min(low[u], num[v]);
}
tail[u] = timeDfs;
}
Ví dụ minh họa :
Mô tả quá trình :
Trong đồ thị vô hướng, một cạnh được gọi là cạnh cầu nếu như loại bỏ cạnh này ra khỏi đồ thị thì số thành phần liên thông của đồ thị tăng lên.
GRAPH_ - Tìm khớp và cầu (Cơ bản)
Xét đơn đồ thị vô hướng $G=(V, E)$ có $N$ $(1 \le N \le 10000)$ đỉnh và $M$ $(1 \le M \le 50000)$ cạnh. Người ta định nghĩa một đỉnh gọi là khớp nếu như xoá đỉnh đó sẽ làm tăng số thành phần liên thông của đồ thị. Tương tự như vậy, một cạnh được gọi là cầu nếu xoá cạnh đó sẽ làm tăng số thành phần liên thông của đồ thị.
Vấn đề đặt ra là cần phải đếm tất cả các khớp và cầu của đồ thị $G$.
Input
Output
Example
Input
10 12
1 10
10 2
10 3
2 4
4 5
5 2
3 6
6 7
7 3
7 8
8 9
9 7
Output
4 3
Note
Hoặc
Cấu trúc dữ liệu:
maxN = 10010
timeDfs
- Thứ tự $DFS$bridge
- Số lượng cạnh cầulow[], num[]
joint[]
- Đánh dấu đỉnh khớpg[]
- Danh sách cạnh kề của mỗi đỉnh#include <bits/stdc++.h>
using namespace std;
const int maxN = 10010;
int n, m;
bool joint[maxN];
int timeDfs = 0, bridge = 0;
int low[maxN], num[maxN];
vector <int> g[maxN];
void dfs(int u, int pre) {
int child = 0; // Số lượng con trực tiếp của đỉnh u trong cây DFS
num[u] = low[u] = ++timeDfs;
for (int v : g[u]) {
if (v == pre) continue;
if (!num[v]) {
dfs(v, u);
low[u] = min(low[u], low[v]);
if (low[v] == num[v]) bridge++;
child++;
if (u == pre) { // Nếu u là đỉnh gốc của cây DFS
if (child > 1) joint[u] = true;
}
else if (low[v] >= num[u]) joint[u] = true;
}
else low[u] = min(low[u], num[v]);
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
for (int i = 1; i <= n; i++)
if (!num[i]) dfs(i, i);
int cntJoint = 0;
for (int i = 1; i <= n; i++) cntJoint += joint[i];
cout << cntJoint << ' ' << bridge;
}
Để truy bắt tội phạm, cảnh sát xây dựng một hệ thống máy tính mới. Bản đồ khu vực bao gồm $N$ thành phố và $M$ đường nối $2$ chiều. Các thành phố được đánh số từ $1$ đến $N$.
Cảnh sát muốn bắt các tội phạm di chuyển từ thành phố này đến thành phố khác. Các điều tra viên, theo dõi bản đồ, phải xác định vị trí thiết lập trạm gác. Hệ thống máy tính mới phải trả lời được $2$ loại truy vấn sau:
Input
Dữ liệu được cho sao cho ban đầu luôn có cách di chuyển giữa hai thành phố bất kỳ.
Output
Example
Input
13 15
1 2
2 3
3 5
2 4
4 6
2 6
1 4
1 7
7 8
7 9
7 10
8 11
8 12
9 12
12 13
5
1 5 13 1 2
1 6 2 1 4
1 13 6 7 8
2 13 6 7
2 13 6 8
Output
yes
yes
yes
no
yes
Note
Ví dụ minh họa: Loại bỏ cạnh cầu $(1,7)$ (với đỉnh $7$ là con trực tiếp của đỉnh $1$)
Ví dụ minh họa: Loại bỏ đỉnh khớp $8$. Đỉnh $11$ và $12$ là các con trực tiếp của đỉnh $8$ trong cây $DFS$. Nhưng chỉ có cây con gốc $11$ là tách riêng ra thành $1$ thành phần liên thông riêng biệt. Còn cây con gốc $12$ thì có $1$ cung ngược nối lên đỉnh $7$ (tổ tiên của đỉnh $8$) nên số lượng thành phần liên thông của cả đồ thị chỉ tăng thêm $1$.
Cấu trúc dữ liệu:
maxN = 100010
timeDfs
- Thứ tự $DFS$low[], num[], tail[]
depth[]
- Lưu chiều sâu của mỗi đỉnh trong cây $DFS$p[][]
- Mảng ứng dụng $Sparse Table$ với $p[i][j]$ là tổ tiên thứ $2^j$ của $i$ trong cây $DFS$joint[]
- Đánh dấu đỉnh khớpg[]
- Danh sách cạnh kề của mỗi đỉnh#include <bits/stdc++.h>
using namespace std;
const int maxN = 100010;
int n, m, q;
int timeDfs = 0;
int low[maxN], num[maxN], tail[maxN];
int depth[maxN], p[maxN][20];
bool joint[maxN];
vector <int> g[maxN];
/* Tính mảng p */
void calP() {
p[1][0] = 1;
for (int j = 1; j <= 19; j++)
for (int i = 1; i <= n; i++)
p[i][j] = p[p[i][j - 1]][j - 1];
}
/* Tìm tổ tiên của đỉnh u là con trực tiếp của đỉnh par */
int findParent(int u, int par) {
for (int i = 19; i >= 0; i--)
if (depth[p[u][i]] > depth[par]) u = p[u][i];
return u;
}
/* Tìm khớp cầu */
void dfs(int u, int pre) {
int child = 0;
num[u] = low[u] = ++timeDfs;
for (int v : g[u]){
if (v == pre) continue;
if (!num[v]) {
child++;
p[v][0] = u;
depth[v] = depth[u] + 1;
dfs(v, u);
low[u] = min(low[u], low[v]);
if (u == pre) {
if (child > 1) joint[u] = true;
}
else if (low[v] >= num[u]) joint[u] = true;
}
else low[u] = min(low[u], num[v]);
}
tail[u] = timeDfs;
}
/* Kiểm tra xem đỉnh u có nằm trong cây con DFS gốc root hay không? */
bool checkInSubtree(int u, int root) {
return num[root] <= num[u] && num[u] <= tail[root];
}
/* Xử lí truy vấn 1 */
bool solve1(int a, int b, int g1, int g2) {
/* Vì ta coi g2 là con trực tiếp của g1 nên khi g1 là con của g2,
ta phải đổi chỗ 2 giá trị g1 và g2 cho nhau */
if (num[g1] > num[g2]) swap(g1, g2);
/* Kiểm tra nếu cạnh (g1, g2) không phải là cầu */
if (low[g2] != num[g2]) return true;
if (checkInSubtree(a, g2) && !checkInSubtree(b, g2)) return false;
if (checkInSubtree(b, g2) && !checkInSubtree(a, g2)) return false;
return true;
}
/* Xử lí truy vấn 2 */
bool solve2(int a, int b, int c) {
if (!joint[c]) return true;
int pa = 0, pb = 0;
if (checkInSubtree(a, c)) pa = findParent(a, c);
if (checkInSubtree(b, c)) pb = findParent(b, c);
if (!pa && !pb) return true;
if (pa == pb) return true;
if (!pa && low[pb] < num[c]) return true;
if (!pb && low[pa] < num[c]) return true;
if (pa && pb && low[pa] < num[c] && low[pb] < num[c]) return true;
return false;
}
int main() {
ios_base::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
depth[1] = 1;
dfs(1, 1);
calP();
cin >> q;
while (q--) {
int type, a, b, c, g1, g2;
cin >> type;
if (type == 1) {
cin >> a >> b >> g1 >> g2;
cout << (solve1(a, b, g1, g2) ? "yes\n" : "no\n");
}
else {
cin >> a >> b >> c;
cout << (solve2(a, b, c) ? "yes\n" : "no\n");
}
}
}
Cho $N$ hòn đảo và $N - 1$ cây cầu, mỗi cây cầu nối hai hòn đảo lại với nhau. Đảm bảo rằng từ một đảo bất kì luôn có thể đến được hết mọi đảo còn lại. Pirate đưa ra một lịch trình như sau: vào mỗi ngày sẽ đi kiểm tra mọi cây cầu trên đường đi từ đảo $a$ đến đảo $b$. Hỏi sau khi Pirate thực hiện xong lịch trình đó, thì còn có bao nhiêu cây cầu chưa được kiểm tra?
Input
$1 \le N, M \le 200000$
Output
Input
6
1 2
2 3
2 4
4 5
4 6
2
3 6
5 6
Output
1
Note
Vì đồ thị ban đầu liên thông và có $N - 1$ cạnh nên đây là đồ thị dạng cây.
Để đánh dấu các cạnh thuộc đường đi từ đỉnh $u$ đến đỉnh $v$ trên cây, thì ta có thể thêm một cạnh $(u,v)$ vào đồ thị. Khi đó, các cạnh thuộc đường đi từ $u \rightarrow v$ trên cây sẽ nằm trong $1$ chu trình. Từ đó, bài toán sẽ quy về thành bài toán đếm số lượng cạnh cầu của đồ thị.
Ví dụ minh họa: Để dánh dấu đường đi từ đỉnh $3 \rightarrow 6$ và đường đi từ đỉnh $5 \rightarrow 6$, ta thêm cạnh $(3, 6)$, $(5, 6)$ vào đồ thị. Khi đó, đồ thị có một cạnh cầu là cạnh $(1, 2)$.
Cấu trúc dữ liệu:
maxN = 200010
timeDfs
- Thứ tự $DFS$bridge
- Số lượng cạnh cầulow[], num[]
g[]
- Danh sách cạnh kề của mỗi đỉnh#include <bits/stdc++.h>
using namespace std;
const int maxN = 2e5 + 10;
int n, m;
int timeDfs = 0, bridge = 0;
int low[maxN], num[maxN];
vector <int> g[maxN];
void dfs(int u, int pre) {
num[u] = low[u] = ++timeDfs;
for (int v : g[u]) {
if (v == pre) continue;
if (!num[v]) {
dfs(v, u);
low[u] = min(low[u], low[v]);
if (low[v] == num[v]) bridge++;
}
else low[u] = min(low[u], num[v]);
}
}
int main() {
cin >> n;
for (int i = 1; i < n; i++) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
cin >> m;
while (m--) {
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
dfs(1, 1);
cout << bridge;
}
Định lý 1: Nếu $a$, $b$ là hai đỉnh thuộc thành phần liên thông mạnh $C$ thì với mọi đường đi từ $a$ tới $b$ cũng như từ $b$ tới $a$. Tất cả đỉnh trung gian trên đường đi đó đều phải thuộc $C$. Chứng minh: Nếu $a$ và $b$ là hai đỉnh thuộc $C$ thì tức là có một đường đi từ $a$ đến $b$ và một đường khác đi từ $b$ về $a$. Suy ra với một đỉnh $v$ nằm trên đường đi từ $a$ tới $b$ thì $a$ tới được $v$, $v$ tới được $b$, mà $b$ có đường tới $a$ nên $v$ cũng tới được $a$. Vậy $v$ nằm trong thành phần liên thông mạnh chứa $a$ tức là $v$ thuộc $C$. Tương tự với một đỉnh nằm trên đường đi từ $b$ tới $a$.
Định lý 2: Với một thành phần liên thông mạnh $C$ bất kỳ, tồn tại một đỉnh $r$ thuộc $C$ sao cho mọi đỉnh của $C$ đều thuộc cây con gốc $r$ trong cây $DFS$. Chứng minh: Trước hết, nhắc lại một thành phần liên thông mạnh là một đồ thị con liên thông mạnh của đồ thị ban đầu thoả mãn tính chất tối đại tức là việc thêm vào thành phần đó một tập hợp đỉnh khác sẽ làm mắt đi tính liên thông mạnh.
Trong số các đỉnh của $C$, chọn $r$ là đỉnh được thăm đầu tiên theo thuật toán tìm kiếm theo chiều sâu. Ta sẽ chứng minh $C$ nằm trong nhánh $DFS$ gốc $r$. Thật vậy, với một đỉnh $v$ bất kỳ của $C$, do $C$ liên thông mạnh nên phải tồn tại một đường đi từ $r$ tới $v$: $(r = x_0, x_1,…, x_k = v)$
Từ định lý $1$, tất cả các đỉnh $x_1, x_2,…, x_k$ đều thuộc $C$ nên chúng sẽ phải thăm sau đỉnh $r$. Khi thủ tục $DFS(r)$ được gọi thì tất cả các đỉnh $x_1, x_2,…, x_k$ đều chưa thăm; vì thủ tục $DFS(r)$ sẽ liệt kê tất cả những đỉnh chưa thăm đến được từ $r$ bằng cách xây dựng nhánh gốc $r$ của cây $DFS$, nên các đỉnh $x_1, x_2,…, x_k = v$ sẽ thuộc nhánh gốc $r$ của cây $DFS$. Bởi chọn $v$ là đỉnh bất kỳ trong $C$ nên ta có điều phải chứng minh.
Đỉnh $r$ trong chứng minh định lý - đỉnh thăm trước tất cả các đỉnh khác trong $C$ - gọi là chốt của thành phần $C$. Mỗi thành phần liên thông mạnh có duy nhất một chốt. Xét về vị trí trong cây tìm kiếm $DFS$, chốt của một thành phân liên thông là đỉnh nằm cao nhất so với các đỉnh khác thuộc thành phần đó, hay nói cách khác: là tiền bối của tất cả các đỉnh thuộc thành phần đó.
Định lý 3: Luôn tìm được đỉnh chốt $a$ thỏa mãn: Quá trình tìm kiếm theo chiều sâu bắt đầu từ $a$ không thăm được bất kỳ một chốt nào khác. (Tức là nhánh $DFS$ gốc $a$ không chứa một chốt nào ngoài $a$) chẳng hạn ta chọn $a$ là chốt được thăm sau cùng trong một dây chuyền đệ quy hoặc chọn $a$ là chốt thăm sau tất cả các chốt khác. Với chốt $a$ như vậy thì các đỉnh thuộc nhánh $DFS$ gốc $a$ chính là thành phần liên thông mạnh chứa $a$. Chứng minh: Với mọi đỉnh $v$ nằm trong nhánh $DFS$ gốc $a$, xét $b$ là chốt của thành phần liên thông mạnh chứa $v$. Ta sẽ chứng minh $a = b$. Thật vậy, theo định lý $2$, $v$ phải nằm trong nhánh $DFS$ gốc $b$. Vậy $v$ nằm trong cả nhánh $DFS$ gốc $a$ và nhánh $DFS$ gốc $b$. Giả sử phản chứng rằng $a$ khác $b$ thì sẽ có hai trường hợp xảy ra:
Theo định lý $2$, ta đã có thành phân liên thông mạnh chứa $a$ nằm trong nhánh $DFS$ gốc $a$, theo chứng minh trên ta lại có: Mọi đỉnh trong nhánh $DFS$ gốc $a$ nằm trong thành phân liên thông mạnh chứa $a$. Kết hợp lại được: Nhánh $DFS$ gốc $a$ chính là thành phần liên thông mạnh chứa $a$.
Cho đồ thị $G(V, E)$ có hướng $N$ $(1 \le N \le 10^4)$ đỉnh, $M$ $(1 \le M \le 10^5)$ cung. Hãy đếm số thành phần liên thông mạnh của $G$.
Input
Output
Examples
Input
3 2
1 2
2 3
Output
3
Input
3 3
1 2
2 3
3 1
Output
1
Thuật toán Tarjan được xây dựng dựa trên các dữ kiện sau:
Cấu trúc dữ liệu:
maxN = 100010
timeDfs
- Thứ tự $DFS$scc
- Số lượng thành phần liên thông mạnhlow[], num[]
deleted[]
- Đánh dấu các đỉnh đã bị xóag[]
- Danh sách cạnh kề của mỗi đỉnhst
- Lưu lại các đỉnh trong thành phần liên thông mạnh#include <bits/stdc++.h>
using namespace std;
const int maxN = 100010;
int n, m;
int timeDfs = 0, scc = 0;
int low[maxN], num[maxN];
bool deleted[maxN];
vector <int> g[maxN];
stack <int> st;
void dfs(int u) {
num[u] = low[u] = ++timeDfs;
st.push(u);
for (int v : g[u]) {
if (deleted[v]) continue;
if (!num[v]){
dfs(v);
low[u] = min(low[u], low[v]);
}
else low[u] = min(low[u], num[v]);
}
if (low[u] == num[u]) {
scc++;
int v;
do {
v = st.top();
st.pop();
deleted[v] = true;
}
while (v != u);
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
}
for (int i = 1; i <= n; i++)
if (!num[i]) dfs(i);
cout << scc;
}
Khu vườn của Pirate có hình chữ nhật, và được chia thành $M \cdot N$ ô vuông bằng nhau. Trong mỗi ô vuông có một cây thuộc một loại quả khác nhau, đánh số từ $0$ đến $9$. Những con số này thể hiện giá trị kinh tế của các loại cây. Tuy nhiên, nhìn mặt con Robot trái cây này có vẻ ngu ngu nên trong lần đầu tiên thử việc, Pirate muốn test AI của nó. Cụ thể là Robot phải tuân theo các quy định sau:
Xuất phát từ ô ở góc tây bắc của khu vườn, hãy giúp Robot trái cây xác định hành trình để đạt được lợi nhuận tối đa.
Input
Output
Example
Input
2 3
264
3WW
Output
15
Note
Cấu trúc dữ liệu:
maxN = 100010
INF = 1000000007
timeDfs
- Thứ tự $DFS$scc
- Số lượng thành phần liên thông mạnha[]
- Lưu các dữ liệu vào.val[]
- Lưu giá trị kinh tế của loại cây.totalScc[]
- Lưu tổng giá trị kinh tế của từng thành phần liên thông mạnh.root[]
- Lưu ô $(i, j)$ thuộc thành phần liên thông nào? Ta sẽ lấy thứ tự của thành phần liên thông làm đỉnh ảo trong đồ thị $DAG$.low[], num[]
deleted[]
- Đánh dấu các đỉnh đã bị xóaf[]
- Mảng quy hoạch độngg[]
- Lưu đồ thị ban đầu.h[]
- Lưu đồ thị mới (đồ thị DAG).#include <bits/stdc++.h>
using namespace std;
const int maxN = 100010;
const int INF = 1e9 + 7;
int dx[] = {0, -1, 0, 1, 0};
int dy[] = {0, 0, 1, 0,-1};
int m, n;
char a[maxN];
int val[maxN], totalScc[maxN];
/* Lưu đồ thị ban đầu*/
vector <int> g[maxN];
/* Lưu đồ thị mới*/
vector <int> h[maxN];
/* Kỹ thuật "Biến mảng 2 chiều thành mảng 1 chiều" */
int getId(int i, int j){
return (i - 1) * n + j;
}
/* Kiểm tra ô (i, j) có được đi vào không? */
bool check(int i, int j) {
if (a[getId(i, j)] == '#') return false;
return (i >= 1 && j >= 1 && i <= m && j <= n);
}
/* Tìm thành phần liên thông mạnh*/
int root[maxN];
int low[maxN], num[maxN];
bool deleted[maxN];
int timeDfs = 0, scc = 0;
stack <int> st;
void dfs(int u) {
low[u] = num[u] = ++timeDfs;
st.push(u);
for (int v : g[u]) {
if (deleted[v]) continue;
if (!num[v]) {
dfs(v);
low[u] = min(low[u], low[v]);
}
else low[u] = min(low[u], num[v]);
}
if (num[u] == low[u]) {
scc++;
int v;
do {
v = st.top();
st.pop();
deleted[v] = true;
/* Tính tổng giá trị kinh tế của thành phần liên thông */
totalScc[scc] += val[v];
/*Đỉnh scc sẽ là đỉnh ảo đại diện cho v trong đồ thị DAG*/
root[v] = scc;
} while (v != u);
}
}
/* Quy hoạch động trên đồ thị DAG */
int f[maxN];
int solve(int u) {
if (h[u].empty()) return totalScc[u];
if (f[u] != -1) return f[u];
int cur = -INF;
for (int v : h[u]) cur = max(cur, solve(v) + totalScc[u]);
return f[u] = cur;
}
int main() {
/* Xử lý dữ liệu đầu vào */
cin >> m >> n;
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= n; ++j) {
int u = getId(i, j);
cin >> a[u];
val[u] = (a[u] >= '0' && a[u] <= '9') ? a[u] - '0' : 0;
}
/* Xây dựng đồ thị ban đầu */
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
int u = getId(i, j);
if (a[u] == '#') continue;
if (check(i, j + 1)) g[u].push_back(getId(i, j + 1));
if (check(i + 1, j)) g[u].push_back(getId(i + 1, j));
if (a[u] == 'W' && check(i, j - 1))
g[u].push_back(getId(i, j - 1));
if (a[u] == 'N' && check(i - 1, j))
g[u].push_back(getId(i - 1, j));
}
}
/* Tìm thành phần liên thông mạnh*/
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= n; ++j){
int u = getId(i, j);
if (!num[u] && check(i, j)) dfs(u);
}
/* Xây dựng đồ thị mới */
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (!check(i, j)) continue;
int u = getId(i, j);
int ru = root[u];
for (int v : g[u]) {
int rv = root[v];
if (ru != rv) {
/* Có cung đi từ ru đến rv trên đồ thị mới do đỉnh
u trong TPLTM ru đi được tới đỉnh v trong TPLTM rv*/
h[ru].push_back(rv);
}
}
}
}
fill(f, f + m * n + 1, -1);
cout << solve(root[getId(1, 1)]);
}
CRITICAL - Thành phố trọng yếu
SAFENET2 - Mạng máy tính an toàn
REFORM - VOI 2015 Day 1 - Kế hoạch cải tổ