Tarjan 杂题选讲
写 CF 652E 的时候碰到了 DCC 相关内容于是恶补一发。
思维路径
- 必经点(割点),必经边(割边)
- 缩点后转化为树上问题或 DAG 上 DP
- DCC、SCC 自身的性质
洛谷P3388 【模板】割点(割顶)
Tarjan 求 v-DCC 的基本思路就是利用一个充要条件:如果 u 是 dfs 树根节点,那么它有不少于两个儿子;否则存在一个 v 满足 low[v] >= dfn[u]
。
后面这个东西可以很直观地理解一下,如果你这棵子树内所有的点都没法在 u 以外的路径上走到 u 上面,那么它一定是割点。
还有一个小细节就是 dfn[v] != 0
时 low[u] = std::min(low[u], dfn[v])
,这里用 dfn[v]
更新是因为使用 low[v]
可能会跳到当前环上面去,而在有向图中使用 low[v]
也是对的。
const int MAXN = 2e4 + 10;
const int MAXM = 1e5 + 10;
int n, m;
struct Edge { int v, nxt; } edge[MAXM << 1]; int head[MAXN], cnt;
void addEdge(int u, int v) {
edge[++cnt] = {v, head[u]}; head[u] = cnt;
}
int dfn[MAXN], low[MAXN], ts;
bool iscut[MAXN];
int fa[MAXN];
void Tarjan(int u) {
dfn[u] = low[u] = ++ts;
int childs = 0;
for (int e = head[u]; e; e = edge[e].nxt) {
int v = edge[e].v;
if (!dfn[v]) {
fa[v] = u;
Tarjan(v);
// 对是否为 dfs 树根进行讨论
++childs;
low[u] = std::min(low[u], low[v]);
if (fa[u] != u && low[v] >= dfn[u]) iscut[u] = true;
} else if (v != fa[u]) low[u] = std::min(low[u], dfn[v]);
}
if (fa[u] == u && childs >= 2) iscut[u] = true;
}
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
n = read(); m = read();
rep (i, 1, m) {
int u = read(); int v = read();
addEdge(u, v); addEdge(v, u);
}
rep (i, 1, n) {
if (!dfn[i]) {
fa[i] = i; Tarjan(i);
}
}
std::vector<int> ans;
rep (i, 1, n) {
if (iscut[i]) ans.push_back(i);
}
printf("%d\n", (int) ans.size());
for (auto v : ans) printf("%d ", v);
puts("");
return 0;
}
A. CodeForces 652E Pursuit For Artifacts
将图里所有的 e-DCC 缩点,最终图会变成一棵树,每个点走出去了就不能再走进去了,每条边也只能走一次。
e-DCC 保证了这个分量内每两个点之间都存在至少两条不相交路径,也就是说,你走进去这个节点,如果里面有 artifact 那么一定能拿到并顺利走出来。
所以只需要看目标点之间的树上路径里有没有 artifact 就可以了。
const int MAXN = 3e5 + 10;
int n, m;
struct REdge { int u, v, w; } redges[MAXN];
struct Edge { int v, w, nxt; } edge[MAXN << 1]; int head[MAXN], cnt = 1;
void addEdge(int u, int v, int w) {
edge[++cnt] = {v, w, head[u]}; head[u] = cnt;
}
int dfn[MAXN], low[MAXN], ts; bool iscut[MAXN << 1];
void Tarjan(int u, int fa) {
dfn[u] = low[u] = ++ts;
for (int e = head[u]; e; e = edge[e].nxt) {
int v = edge[e].v;
if (v == fa) continue;
if (!dfn[v]) {
Tarjan(v, u);
low[u] = std::min(low[u], low[v]);
if (low[v] > dfn[u]) {
iscut[e] = iscut[e ^ 1] = true;
}
} low[u] = std::min(low[u], dfn[v]);
}
}
int bel[MAXN], blkcnt;
bool has[MAXN];
void GetBlock(int u) {
// DEBUG(blkcnt); DEBUG(u);
bel[u] = blkcnt;
for (int e = head[u]; e; e = edge[e].nxt) {
int v = edge[e].v;
if (iscut[e]) continue;
has[blkcnt] |= edge[e].w;
if (bel[v]) continue;
GetBlock(v);
}
}
Edge nedge[MAXN << 1]; int nhead[MAXN], ncnt;
void addNEdge(int u, int v, int w) {
nedge[++ncnt] = {v, w, nhead[u]};
nhead[u] = ncnt;
}
void rebuild() {
rep (i, 1, m) {
int u = redges[i].u, v = redges[i].v, w = redges[i].w;
if (bel[u] != bel[v]) {
addNEdge(bel[u], bel[v], w);
addNEdge(bel[v], bel[u], w);
}
}
}
int dep[MAXN], faedge[MAXN];
void ndfs(int u, int fa) {
dep[u] = dep[fa] + 1;
for (int e = nhead[u]; e; e = nedge[e].nxt) {
int v = nedge[e].v;
if (v == fa) { faedge[u] = e; continue; }
ndfs(v, u);
}
}
bool Solve() {
int x = bel[read()]; int y = bel[read()];
if (dep[x] < dep[y]) std::swap(x, y);
bool ans = false;
while (dep[x] > dep[y]) {
ans |= has[x];
ans |= nedge[faedge[x]].w;
x = nedge[faedge[x]].v;
}
if (x == y) return ans | has[x];
while (x != y) {
ans |= has[y];
ans |= nedge[faedge[y]].w;
y = nedge[faedge[y]].v;
ans |= has[x];
ans |= nedge[faedge[x]].w;
x = nedge[faedge[x]].v;
}
return ans | has[x];
}
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
n = read(); m = read();
rep (i, 1, m) {
int u = read(); int v = read(); int w = read();
redges[i] = {u, v, w};
addEdge(u, v, w); addEdge(v, u, w);
}
Tarjan(1, 0);
rep (i, 1, n) if (!bel[i]) { ++blkcnt; GetBlock(i); }
rebuild();
// rep (i, 1, n) printf("(%d -> %d) ", i, bel[i]);
// puts("");
ndfs(1, 0);
printf("%s\n", Solve() ? "YES" : "NO");
return 0;
}
B. 洛谷P2746 [USACO5.3]校园网Network of Schools
SCC 缩点板子题。
任务 1 就是入度为 0 的点的个数,任务 2 就是入度为 0 和出度为 0 点个数的较大值。
const int MAXN = 100 + 10;
int n;
std::vector<int> G[MAXN];
int dfn[MAXN], low[MAXN], ts, stk[MAXN], top, instk[MAXN];
int bel[MAXN], blkcnt;
void Tarjan(int u) {
dfn[u] = low[u] = ++ts; stk[++top] = u; instk[u] = true;
forall (G[u], i) {
int v = G[u][i];
if (!dfn[v]) {
Tarjan(v);
low[u] = std::min(low[u], low[v]);
} else if (instk[v]) low[u] = std::min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++blkcnt;
do {
bel[stk[top]] = blkcnt; instk[stk[top]] = false; --top;
} while (stk[top + 1] != u);
}
}
int ind[MAXN], oud[MAXN];
void rebuild() {
rep (u, 1, n) {
for (auto v : G[u]) {
if (bel[u] != bel[v]) {
++ind[bel[v]]; ++oud[bel[u]];
}
}
}
}
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
n = read();
rep (i, 1, n) {
int x = read();
while (x) {
G[i].push_back(x); x = read();
}
}
rep (i, 1, n) if (!dfn[i]) Tarjan(i);
rebuild();
int ans1 = 0; rep (i, 1, blkcnt) ans1 += (ind[i] == 0);
int ans2 = 0; rep (i, 1, blkcnt) ans2 += (oud[i] == 0);
printf("%d\n%d\n", ans1, blkcnt == 1 ? 0 : std::max(ans1, ans2));
return 0;
}
C. [国家集训队]稳定婚姻
好离谱一题
声明:以下场景皆为假设,没有任何对女性的不尊重,本题也并不假定 \(B\) 和 \(G\) 为人类性别。
首先注意到戴绿帽这个过程是:\(B \rightarrow G \rightarrow B \rightarrow G \dots\) 或者 \(G \rightarrow B \rightarrow G \rightarrow B \dots\),也就是 \(B, G\) 交替进行,最终一定会形成一个环,否则最后一个人会落单。
然后继续仔细分析戴绿帽的过程:
比如说 \(G_7\) 出轨去找 \(B_6\),此时 \(G_6\) 会去找 \(B_5\),\(G_5\) 会去找 \(B_4\)……一直到 \(G_1\),如果能找到 \(B_7\),那么这 14 个人又形成了 7 对,也就是 \(B_7 \longleftrightarrow G_7\) 是不安全的。
这个过程中,每一对情侣 \(G_7 \rightarrow B_6\),\(G_6 \rightarrow B_5\)……都是从 \(G\) 导向 \(B\),上一次出轨导致拆散的夫妻 \(B_6 \rightarrow G_6\),\(B_5 \rightarrow G_5\) 都是从 \(B\) 导向 \(G\),最终形成了一个环。
所以考虑对于每一对夫妻连有向边 \(B \rightarrow G\),对于每一对情侣连有向边 \(B \leftarrow G\),之后 Tarjan 缩环,如果一对夫妻属于一个环就是不安全的。
关于本题为什么不能建无向图然后跑 e-DCC,可以看看 这个。
const int MAXN = 8000 + 10;
int n, m;
std::vector<int> G[MAXN];
std::map<std::string, int> idx; int cnt;
int dfn[MAXN], low[MAXN], ts, stk[MAXN], top, instk[MAXN];
int bel[MAXN], blkcnt;
void Tarjan(int u) {
dfn[u] = low[u] = ++ts; stk[++top] = u; instk[u] = true;
forall (G[u], i) {
int v = G[u][i];
if (!dfn[v]) {
Tarjan(v);
low[u] = std::min(low[u], low[v]);
} else if (instk[v]) low[u] = std::min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++blkcnt;
do {
bel[stk[top]] = blkcnt;
instk[stk[top]] = false; --top;
} while (stk[top + 1] != u);
}
}
struct E {int u, v;} es[MAXN];
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n;
rep (i, 1, n) {
std::string bb, gg; cin >> bb >> gg;
idx[bb] = ++cnt; idx[gg] = ++cnt;
es[i] = {cnt - 1, cnt};
G[idx[bb]].push_back(idx[gg]);
}
cin >> m;
rep (i, 1, m) {
std::string bb, gg; cin >> bb >> gg;
G[idx[gg]].push_back(idx[bb]);
}
rep (i, 1, cnt) if (!dfn[i]) Tarjan(i);
rep (i, 1, n) {
int u = es[i].u, v = es[i].v;
cout << ((bel[u] == bel[v]) ? "Unsafe" : "Safe") << endl;
}
return 0;
}
D. [ZJOI2007]最大半连通子图
半联通分量我们不大会求,但是我们会求强联通分量,显然一个强联通分量一定是半联通分量。所以先把原图 SCC 缩点。
考虑两个相邻的强联通分量,发现它们合起来也是一个半联通分量,因为前一个里面所有点可以到达后一个里面所有点。
所以题目转化成了 DAG 上最长链。
const int MAXN = 1e5 + 10;
int n, m, x;
std::vector<int> G[MAXN];
int dfn[MAXN], low[MAXN], ts, stk[MAXN], top, instk[MAXN];
int bel[MAXN], blk, siz[MAXN];
void Tarjan(int u) {
dfn[u] = low[u] = ++ts; stk[++top] = u; instk[u] = true;
forall (G[u], i) {
int v = G[u][i];
if (!dfn[v]) {
Tarjan(v);
low[u] = std::min(low[u], low[v]);
} else if (instk[v]) low[u] = std::min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++blk;
do {
++siz[blk];
bel[stk[top]] = blk; instk[stk[top]] = false;
--top;
} while (stk[top + 1] != u);
}
}
int nn;
std::set<int> F[MAXN]; int ww[MAXN];
int ind[MAXN];
void rebuild() {
nn = blk;
rep (u, 1, n) {
for (auto v : G[u]) {
if (bel[u] != bel[v]) {
if (!F[bel[u]].count(bel[v])) ++ind[bel[v]];
F[bel[u]].insert(bel[v]);
}
}
}
rep (i, 1, nn) ww[i] = siz[i];
}
int dp[MAXN], cnt[MAXN];
void DP() {
std::queue<int> q;
for (int i = 1; i <= nn; ++i) if (!ind[i]) {
dp[i] = ww[i]; cnt[i] = 1 % x;
q.push(i);
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (auto v : F[u]) {
// dp[v] = std::max(dp[v], dp[u] + ww[v]);
if (dp[u] + ww[v] > dp[v]) {
dp[v] = dp[u] + ww[v];
cnt[v] = cnt[u] % x;
} else if (dp[u] + ww[v] == dp[v]) {
(cnt[v] += cnt[u]) %= x;
}
--ind[v];
if (!ind[v]) q.push(v);
}
}
}
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
n = read(); m = read(); x = read();
rep (i, 1, m) {
int u = read(); int v = read();
G[u].push_back(v);
}
rep (i, 1, n) if (!dfn[i]) Tarjan(i);
rebuild(); DP();
int maxdp = 0, maxcnt = 0;
for (int i = 1; i <= nn; ++i) {
if (maxdp < dp[i]) {
maxdp = dp[i]; maxcnt = cnt[i];
} else if (maxdp == dp[i]) {
(maxcnt += cnt[i]) %= x;
}
}
printf("%d\n%d\n", maxdp, maxcnt);
return 0;
}
E. [HNOI2012]矿场搭建
如果某个点到逃生通道的必经点塌了就不好玩了。所以这题肯定和“必经点”——割点有关。于是先一遍 Tarjan 求出所有割点。
之后对每个 DCC 分类讨论:
- 如果这个 DCC 有不少于两个割点,那么这个 DCC 里不需要建任何逃生通道,如果一个割点塌了,那么就可以从其他割点逃出去;
- 如果这个 DCC 只有一个割点,也就是割点上挂了个联通块,那么这个 DCC 里需要建一个逃生通道,如果割点塌了就要从自己的通道出去;
- 如果这个 DCC 没有割点,也就是一整个连通分量,需要建两个逃生通道互相备份,防止另一个塌了。
方案数就是从一些点中选两个 / 选一个的方案数。总方案数就是每个 DCC 的方案数乘起来。
const int MAXN = 1000 + 10;
int n, m;
std::vector<int> G[MAXN];
int dfn[MAXN], low[MAXN], ts;
bool iscut[MAXN];
int fa[MAXN];
void Tarjan(int u) {
dfn[u] = low[u] = ++ts;
int childs = 0;
for (auto v : G[u]) {
if (!dfn[v]) {
fa[v] = u;
Tarjan(v);
// 对是否为 dfs 树根进行讨论
++childs;
low[u] = std::min(low[u], low[v]);
if (fa[u] != u && low[v] >= dfn[u]) iscut[u] = true;
} else if (v != fa[u]) low[u] = std::min(low[u], dfn[v]);
}
if (fa[u] == u && childs >= 2) iscut[u] = true;
}
int blk, col[MAXN];
int cuts, siz;
void dfs(int u) {
col[u] = blk;
++siz;
for (auto v : G[u]) {
if (col[v] != blk && iscut[v]) {
col[v] = blk; ++cuts;
}
if (!col[v] && !iscut[v]) {
dfs(v);
}
}
}
void cleanup() {
rep (i, 1, n) {
G[i].clear();
dfn[i] = low[i] = iscut[i] = col[i] = fa[i] = 0;
}
n = 0; m = 0;
blk = ts = 0;
}
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int cas = 0;
while (scanf("%d", &m) != EOF) {
if (m == 0) break;
++cas;
for (int i = 1; i <= m; ++i) {
int u, v; scanf("%d %d", &u, &v);
G[u].push_back(v); G[v].push_back(u);
n = std::max({n, u, v});
}
rep (i, 1, n) if (!dfn[i]) { fa[i] = i; Tarjan(i); }
unsigned long long int ans = 0, cnt = 1;
rep (i, 1, n) {
cuts = siz = 0;
if (col[i] || iscut[i]) continue;
++blk;
dfs(i);
if (cuts == 0) ans += 2, cnt *= 1ull * (siz - 1) * siz / 2;
else if (cuts == 1) ans += 1, cnt *= 1ull * siz;
}
printf("Case %d: %llu %llu\n", cas, ans, cnt);
cleanup();
}
return 0;
}
F. [ZJOI2004]嗅探器
必经点。显然答案集合是图中的某些(不一定是全部)割点,还要保证这个割点在两个中心服务器 \(a, b\) 的路径上。
所以一种方法是缩点之后在树上跑一遍 DFS,但是我写挂了。
另一种方法是考虑从一个中心服务器 \(a\) 开始 Tarjan,在每一个割点 \(u\) 出现时判断它是否符合要求,“符合要求”意味着另一个中心服务器 \(b\) 在 DFS 树上应该在 \(u\) 的子树中。
考虑一个割点 \(u\) 在遍历到 \((u, v)\) 这条边时被设置为割点,那么通过 dfn
,我们可以判断 \(b\) 是否在 \(u\) 的子树中:
dfn[u] > dfn[b]
,这种情况说明 \(b\) 可能是 \(u\) 的祖先或者在 \(u\) 的子树外,或者压根就没被遍历到;dfn[u] < dfn[b] < dfn[v]
,这种情况说明 \(b\) 和 \(v\) 是兄弟子树关系,然而无法判断 \(b\) 和 \(a\) 是否不在一个 DCC;dfn[b] > dfn[v]
,这种情况 \(b\) 一定在 \(u\) 的子树中,因为回溯到 \(v\) 点时,所有 dfn 大于它的一定在它子树里面。
所以就把 Tarjan 板子改改就过了。当然要注意本题根节点是不能作为割点的。
const int MAXN = 2e5 + 10;
int n;
int ss, tt;
std::vector<int> G[MAXN];
int dfn[MAXN], low[MAXN], ts;
int fa[MAXN];
bool iscut[MAXN];
int ans = (1 << 30);
void Tarjan(int u) {
// no need to check the root
dfn[u] = low[u] = ++ts;
int childcnt = 0;
for (auto v : G[u]) {
if (!dfn[v]) {
++childcnt;
fa[v] = u;
Tarjan(v);
low[u] = std::min(low[u], low[v]);
if (dfn[tt] >= dfn[v]) {
// if (u == fa[u] && childcnt >= 2) ans = std::min(ans, u);
if (u != fa[u] && low[v] >= dfn[u]) ans = std::min(ans, u);
}
} else if (v != fa[u]) low[u] = std::min(low[u], dfn[v]);
}
}
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
n = read();
while (true) {
int u = read(); int v = read();
if (u == 0 && v == 0) break;
G[u].push_back(v); G[v].push_back(u);
}
ss = read(); tt = read();
fa[ss] = ss; Tarjan(ss);
if (ans == (1 << 30)) puts("No solution");
else printf("%d\n", ans);
return 0;
}