2024.3.23 笔记(Tarjan)
P3469 [POI2008] BLO-Blockade
根据割点的定义,若节点 \(i\) 不是割点,则把节点 \(i\) 关联的所有边去掉之后,只有 \(i\) 与其他 \(n - 1\) 个节点不连通,而其他 \(n - 1\) 个节点之间是连通的。注意:题目求的是有序点对,即 \((x, y)\) 和 \((y, x)\) 算不同的点对,故此时答案是 \(2 * (n - 1)\)
若节点 \(i\) 是割点,则把节点 \(i\) 关联的所有边去掉之后,图会分成若干个连通块,我们应该求出这些连通块的大小,两两相乘再相加。
假设节点 \(i\) 的子节点集合中,有 \(t\) 个子节点 \(s[k]\),满足 \(dfn[i] <= low[s[k]]\),那么删除节点 \(i\) 关联的所有边后,无向图最多分成 \(t + 2\) 个连通块,情况一共分为 $ 3$ 类:
-
- 节点 \(i\) 自身单独构成一个连通块
-
- 有 \(t\) 个连通块,分别由搜索树上以 \(s[k]\) 为根节点的子树中的节点构成
-
- 还可能有一个连通块,由除了上述节点之外的所有点构成
因此可以在 \(Tarjan\) 算法求割点的过程中顺便求一下 \(cnt[x]\),即以节点 \(x\) 为根节点的子树中的节点个数。
因此删掉割点 \(i\) 之后,不连通的有序点对数量为:
\(cnt[s[1]] * (n - cnt[s[1]]) + cnt[s[2]] * (n - cnt[s[2]]) + ... + cnt[s[n]] * (n - cnt[s[n]]) + 1 * (n - 1) + (n - 1 - sum(cnt[s[k]])) * (1 + sum(cnt[s[k]])) (1 <= k <= t)\)
int n, m, num;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], size[N];
int ans[N];
bool cut[N];
void add(int a, int b)
{
e[++idx] = b, ne[idx] = h[a], h[a] = idx;
}
void tarjan(int x)
{
dfn[x] = low[x] = ++num;
size[x] = 1;
int flag = 0, sum = 0;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!dfn[y])
{
tarjan(y);
size[x] += size[y];
low[x] = min(low[x], low[y]);
if (low[y] >= dfn[x])
{
flag++;
ans[x] += size[y] * (n - size[y]);
sum += size[y];
if (x != 1 || flag > 1) cut[x] = 1;
}
}
else
{
low[x] = min(low[x], dfn[y]);
}
}
if (cut[x])
ans[x] += (n - sum - 1) * (sum + 1) + (n - 1);
else
ans[x] = 2 * (n - 1);
}
signed main()
{
cin >> n >> m;
idx = 1;
for (rint i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
tarjan(1);
for (rint i = 1; i <= n; i++)
{
cout << ans[i] << endl;
}
return 0;
}
AcWing.364 网络
先求出所有边双连通分量,再对每个连通分量进行缩点,得到一颗新的树,最开始,树中边的数量就是桥的数量。
依次考虑每个添加边 \((x, y)\) 的操作,若 \(x, y\) 属于同一个连通分量,桥的数量不变。
设 \(c[x]\) 表示节点 \(x\) 所在的连通分量编号。
若不属于同一个连通分量,则 \(c[x]\) 与 \(c[y]\) 之间的路径上的每条边都不再是桥,因为他们都处在一个环内。
可以求出 \(p = lca(c[x], c[y])\),从 \(c[x]\) 不断走向父节点,直到 $ p$,把经过的边都标记成不再是桥。同样,从 \(c[y]\) 不断走向父节点,直到 \(p\),把经过的边都标记成不在是桥。途中若有 \(cnt\) 条边新获得印记,则把途中桥的总数减掉 \(cnt\)
这样这道题的时间复杂度就是 \(O(m + q * n)\),已经够了。当然这里还能再做一步优化。
用并查集对不在是桥的边进行路径压缩,时间复杂度降为 \(O(m + q * logn)\)
int h[N], e[M], ne[M], idx;
int hc[N], ec[M], nc[M], tc;
int dfn[N], low[N], c[N];
int n, m, q, num, dcc, ans, T;
int fa[N]/*并查集中的父节点*/, d[N], go[N]/*缩点之后的树上的父节点*/;
bool bridge[M];
void add(int a, int b)
{
e[++idx] = b, ne[idx] = h[a], h[a] = idx;
}
void add_c(int a, int b)
{
ec[++tc] = b, nc[tc] = hc[a], hc[a] = tc;
}
void tarjan(int x, int in_edge)
{
dfn[x] = low[x] = ++num;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!dfn[y])
{ // 树边
tarjan(y, i);
low[x] = min(low[x], low[y]);
if (dfn[x] < low[y]) bridge[i] = bridge[i ^ 1] = 1;
}
else if (i != (in_edge ^ 1))
{
low[x] = min(low[x], dfn[y]);
}
}
}
void dfs(int x)
{
c[x] = dcc;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!bridge[i] && !c[y])
{
dfs(y);
}
}
}
void dfs_c(int x)
{
for (rint i = hc[x]; i; i = nc[i])
{
int y = ec[i];
if (!d[y])
{
d[y] = d[x] + 1;
go[y] = x;
dfs_c(y);
}
}
}
int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void calc(int x, int y)
{
x = find(x);
y = find(y);
while (x != y)
{
if (d[x] < d[y]) swap(x, y);
if (x == 1) break;
// x到go[x]的边从桥边变为非桥边
fa[x] = find(go[x]);
ans--;
x = find(x);
}
}
signed main()
{
while (cin >> n >> m && n)
{
m(h), m(dfn), m(bridge), m(c), m(hc), m(d);
idx = tc = 1;
dcc = 0;
for (rint i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
tarjan(1, 0);
for (rint i = 1; i <= n; i++)
{
if (!c[i])
{
++dcc;
dfs(i);
}
}
for (rint i = 2; i <= idx; i++)
{
int x = e[i ^ 1], y = e[i];
if (c[x] == c[y]) continue;
add_c(c[x], c[y]);
add_c(c[y], c[x]);
}
ans = dcc - 1;
d[1] = 1;
dfs_c(1);
for (rint i = 1; i <= dcc; i++) fa[i] = i;
printf("Case %lld:\n", ++T);
cin >> q;
for (rint i = 1; i <= q; i++)
{
int a, b;
cin >> a >> b;
a = c[a], b = c[b];
calc(a, b);
cout << ans << endl;
}
puts("");
}
return 0;
}
SP2878 KNIGHTS
建立一个原图的补图,\(n\) 个节点代表 \(n\) 个骑士,若两名骑士没有憎恨关系,则两者之间连一条无向边。
根据题意,若干名骑士可以召开圆桌会议的条件是:他们对应的节点组成一个长度为奇数的简单环(简称奇环)。因此本题就是求多少个点不被任何奇环包含,这些点就是应该被踢掉的骑士。
若两个骑士属于两个不同的点双连通分量,则它们不可能一起出席会议(在同一个奇环中)
若某个点双连通分量中存在奇环,则这个点双连通分量中所有点都被至少一个奇环包含。
用 \(Tarjan\) 算法求出补图中所有的点双连通分量,判定每个点双连通分量中是否存在奇环即可。若存在奇环,则该点双连通分量中的所有骑士都可以参加会议(都在某个奇环中)。
奇环可以用染色法进行判断。一张无向图没有奇环,等价于它是一张二分图。
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], stk[N];
int c[N], v[N], able[N];
int n, m, num, top, cnt, now;
bool hate[N][N], flag;
vector<int> dcc[N];
void add(int a, int b)
{
e[++idx] = b, ne[idx] = h[a], h[a] = idx;
}
void tarjan(int x, int root)
{
dfn[x] = low[x] = ++num;
stk[++top] = x;
if (x == root && h[x] == 0)
{
dcc[++cnt].push_back(x);
return;
}
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!dfn[y])
{
tarjan(y, root);
low[x] = min(low[x], low[y]);
if (low[y] >= dfn[x])
{
cnt++;
int z;
do
{
z = stk[top--];
dcc[cnt].push_back(z);
} while (z != y);
dcc[cnt].push_back(x);
}
}
else
{
low[x] = min(low[x], dfn[y]);
}
}
}
void dfs(int x, int color)
{
c[x] = color;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (v[y] != now) continue;
if (c[y] && c[y] == color)
{
flag = 1;
return;
}
if (!c[y]) dfs(y, 3 - color);
}
}
signed main()
{
while (cin >> n >> m && n)
{
m(h), m(dfn), m(able), m(v);
for (rint i = 1; i <= n; i++) dcc[i].clear();
idx = 1;
num = top = cnt = 0;
for (rint i = 1; i <= n; i++)
for (rint j = 1; j <= n; j++)
hate[i][j] = 0;
for (rint i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
if (a == b) continue;
hate[a][b] = hate[b][a] = 1;
}
// 建补图
for (rint i = 1; i < n; i++)
for (rint j = i + 1; j <= n; j++)
if (!hate[i][j])
add(i, j), add(j, i);
// 求点双连通分量
for (rint i = 1; i <= n; i++)
if (!dfn[i])
tarjan(i, i);
// 判断每个点双是否包含奇环
for (rint i = 1; i <= cnt; i++)
{
now = i;
for (rint j = 0; j < (int)dcc[i].size(); j++)
{
v[dcc[i][j]] = now;
c[dcc[i][j]] = 0;
}
flag = 0;
dfs(dcc[i][0], 1);
if (flag)
for (rint j = 0; j < (int)dcc[i].size(); j++)
able[dcc[i][j]] = 1;
}
int ans = 0;
for (rint i = 1; i <= n; i++)
if (!able[i])
ans++;
cout << ans << endl;
}
return 0;
}
P6066 [USACO05JAN] Watchcow S
本题求的是一条每条边来回走两次的欧拉回路。只需要在求欧拉回路模板的基础上去掉边的判重标记即可。
因为本来就存储了正向边、反向边,判重是为了让一条边不来回走两次,如果不判重,那么就会将所有边都走一遍,即每条边来回走两次
由于欧拉回路的递归层数和边的数量有关,边数过大会造成栈溢出,一般采用另一个栈模拟机器的递归过程
int h[N], e[M], ne[M], idx;
int stk[N], ans[N];
bool vis[N];
int n, m, top, cnt;
void add(int a, int b)
{
e[++idx] = b, ne[idx] = h[a], h[a] = idx;
}
void euler()
{
stk[++top] = 1;
while (top > 0)
{
int x = stk[top], i = h[x];
// 找到一条尚未访问的边
while (i && vis[i]) i = ne[i];
// 沿着这条边模拟递归过程,标记该边,并更新表头
if (i)
{
stk[++top] = e[i];
h[x] = ne[i];
// vis[i] = vis[i ^ 1] = true;
}
// 与x相连的所有边均已访问,模拟回溯过程,并记录
else
{
top--;
ans[++cnt] = x;
}
}
}
signed main()
{
cin >> n >> m;
idx = 1;
for (rint i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
euler();
for (rint i = cnt; i; i--) cout << ans[i] << endl;
return 0;
}
P2746 [USACO5.3] 校园网
使用 \(Tarjan\) 算法求出所有强连通分量,并统计出所有强连通分量中入度和出度为
\(0\) 的个数,分别为 \(a\) 和 \(b\)。
第一问:入度为 \(0\) 强连通分量就是缩点后拓扑图的起点,有多少个起点就需要至少给多少个学校传递新软件,所以答案就是 \(a\)
第二问:答案为 \(max(a,b)\)。特判图中只有一个强连通分量时不需要加边,答案为 \(0\)
复杂度 \(O(n+m)\)
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], c[N], s[N];
int n, num, top, cnt;
int in[N], out[N];
bool ins[N];
void add(int a, int b)
{
e[++idx] = b, ne[idx] = h[a], h[a] = idx;
}
void tarjan(int x)
{
low[x] = dfn[x] = ++num;
s[++top] = x;
ins[x] = 1;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!dfn[y])
{
tarjan(y);
low[x] = min(low[x], low[y]);
}
else if (ins[y])
{
low[x] = min(low[x], dfn[y]);
}
}
if (dfn[x] == low[x])
{
cnt++; // 找到了一个SCC
int y;
do
{
y = s[top--];
ins[y] = false;
c[y] = cnt;
// scc[cnt].push_back(y);
} while (x != y);
}
}
signed main()
{
cin >> n;
for (rint i = 1; i <= n; i++)
{
int j;
while (scanf("%lld", &j), j) add(i, j);
}
for (rint i = 1; i <= n; i++)
if (!dfn[i])
tarjan(i);
for (rint x = 1; x <= n; x++)
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (c[x] == c[y]) continue;
// 缩点后从c[x]到c[y]的有向边
in[c[y]]++;
out[c[x]]++;
}
int zero_in = 0;
int zero_out = 0;
for (rint i = 1; i <= cnt; i++)
{
if (!in[i]) zero_in++;
if (!out[i]) zero_out++;
}
cout << zero_in << endl;
cout << (cnt == 1 ? 0 : max(zero_in, zero_out)) << endl;
return 0;
}
AcWing.368 银河
首先,会想到差分约束系统,这样,就可以在图中方便地表示关系了。
如果使用 \(spfa\),由于所有点的亮度最小是 \(1\)(需要进行初始化),并且所有的点并不一定连通(要全部添加队列),因此可以整一个“超级原点”,到所有的点都会有边,并且边的权值是 \(1\)
有一个问题就是数据太过于大,并不可以使用 spfa 来进行求解。
拓扑排序,但是拓扑排序是针对有向无环图的。
在题目中有关键的一点,如果要是有一个环,那么环上的一定是相等的,如果有一个不相等,那么就不符合条件。
所以可以采用求出强连通分量,并且缩点。如果强连通分量上的边的权值不是 \(0\),那么就不合法。
然后进行拓扑排序
复杂度 \(O(n+m)\)
int h[N], w[M], e[M], ne[M], idx;
int hc[N], wc[M], ec[M], nc[M], tc;
int dfn[N], low[N], c[N], stk[N];
int n, m, num, top, cnt;
int in[N], d[N];
bool ins[N];
queue<int> q;
void add(int a, int b, int c)
{
e[++idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx;
}
void add_c(int a, int b, int c)
{
ec[++tc] = b, wc[tc] = c, nc[tc] = hc[a], hc[a] = tc, in[b]++;
}
void tarjan(int x)
{
low[x] = dfn[x] = ++num;
stk[++top] = x;
ins[x] = 1;
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (!dfn[y])
{
tarjan(y);
low[x] = min(low[x], low[y]);
}
else if (ins[y])
{
low[x] = min(low[x], dfn[y]);
}
}
if (dfn[x] == low[x])
{
cnt++; // 找到了一个SCC
int y;
do
{
y = stk[top--];
ins[y] = 0;
c[y] = cnt;
} while (x != y);
}
}
void topsort()
{
q.push(c[0]);
while (!q.empty())
{
int x = q.front();
q.pop();
for (rint i = hc[x]; i; i = nc[i])
{
int y = ec[i];
d[y] = max(d[y], d[x] + wc[i]);
if (--in[y] == 0) q.push(y);
}
}
}
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= m; i++)
{
int z, x, y;
cin >> z >> x >> y;
if (z == 1) add(x, y, 0), add(y, x, 0);
else if (z == 2) add(x, y, 1);
else if (z == 3) add(y, x, 0);
else if (z == 4) add(y, x, 1);
else add(x, y, 0);
}
for (rint i = 1; i <= n; i++) add(0, i, 1);
for (rint i = 0; i <= n; i++)
if (!dfn[i])
tarjan(i);
for (rint x = 0; x <= n; x++)
{
for (rint i = h[x]; i; i = ne[i])
{
int y = e[i];
if (c[x] == c[y])
{
if (w[i] == 1)
{
puts("-1");
return 0;
}
continue;
}
// 缩点后从c[x]到c[y]的有向边
add_c(c[x], c[y], w[i]);
}
}
topsort();
int ans = 0;
for (rint i = 1; i <= n; i++) ans += d[c[i]];
cout << ans << endl;
return 0;
}
AcWing.369 北大ACM队的远足
本题求的是最小危险程度,即最小步行经过的桥的长度
首先一定会经过的桥就是有向图的必经桥,因此我们要让必经桥的长度和最小。
首先可以求出从 \(S\) 到 \(T\) 的最短路,然后考虑在最短路的什么地方搭车能让危险程度最小。
如果只有一个区间,那么可以用双指针的方式扫描得出最小危险程度,\(ds[i]\) 表示从 \(S\) 到最短路上的第 \(i\) 个节点只搭一次车
的最小危险程度。
然后我们可以反着再求一遍,\(dt[i]\) 表示从最短路上第 \(i\) 个节点到 \(T\) 只搭一次车的最小危险程度。
然后枚举每个点 \(i\),用 \(ds[i] + dt[i]\) 更新答案。
这里我们还漏了一种情况,就是当两个区间相连成为一个长度为 \(2q\) 的区间,这里可以直接用长度为 \(2q\) 的区间再扫描一遍更新答案。
const int N = 1e5 + 5;
const int M = 2e6 + 5;
const int mod = 1e9 + 7;
int n, m, s, t, bus;
int e[M], w[M], ne[M], h[N], idx;
int f[2][N], deg[2][N], d[N], pre[N];
bool bridge[M];
int a[N], b[N], cnt; // 长度、是不是桥
int sum[N], sum_bri[N], ds[N], dt[N], ds_min[N];
int occur[N], first_occur[N];
queue<int> q;
void add(int a, int b, int c)
{
e[++idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx;
}
void topsort(int s, int bit)
{
if (!bit)
{ // 只有正图需要求最短路
memset(d, 0x3f, sizeof d);
d[s] = 0;
}
f[bit][s] = 1;
for (rint i = 1; i <= n; i++)
if (!deg[bit][i])
q.push(i);
while (!q.empty())
{
int x = q.front();
q.pop();
for (rint i = h[x]; i; i = ne[i])
{
if ((i & 1) == bit)
{
int y = e[i];
f[bit][y] = (f[bit][y] + f[bit][x]) % mod; // 路径条数
if (bit == 0 && d[y] > d[x] + w[i])
{ // 最短路
d[y] = d[x] + w[i];
pre[y] = i;
}
if (--deg[bit][y] == 0) q.push(y);
}
}
}
}
signed main()
{
int T;
cin >> T;
while (T--)
{
m(h), m(deg), m(f), m(bridge), m(occur);
idx = 1;
cnt = 0;
cin >> n >> m >> s >> t >> bus;
s++;
t++;
for (rint i = 1; i <= m; i++)
{
int x, y, z;
cin >> x >> y >> z;
x++, y++;
add(x, y, z); // 偶数边是正边(邻接表2, 4, 6,...位置)
add(y, x, z); // 奇数边是反边
deg[0][y]++; // 入度
deg[1][x]++; // 出度
}
topsort(s, 0);
if (!f[0][t])
{
puts("-1");
continue;
}
topsort(t, 1);
for (rint i = 2; i <= idx; i += 2)
{
int x = e[i ^ 1], y = e[i];
if (f[0][x] * f[1][y] % mod == f[0][t])
{
bridge[i] = 1;
}
}
// O(M)判重边,用map可能超时
for (rint x = 1; x <= n; x++)
{
for (rint i = h[x]; i; i = ne[i])
{
if (i & 1) continue; // 只考虑正边
int y = e[i];
if (occur[y] == x)
{
bridge[i] = 0;
bridge[first_occur[y]] = 0;
}
else
{
occur[y] = x;
first_occur[y] = i;
}
}
}
while (t != s)
{
a[++cnt] = w[pre[t]];
b[cnt] = bridge[pre[t]];
t = e[pre[t] ^ 1];
}
// reverse(a + 1, a + cnt + 1); 不反过来也可以
// reverse(b + 1, b + cnt + 1);
for (rint i = 1; i <= cnt; i++)
{
sum[i] = sum[i - 1] + a[i]; // 以i这条边为结尾(包含i)的前缀总长度
sum_bri[i] = sum_bri[i - 1] + (b[i] ? a[i] : 0);
}
ds_min[0] = 1 << 30;
for (rint i = 1, j = 0; i <= cnt; i++)
{ // 恰好在i这条边的结尾处下车,前面的最小危险程度:ds[i]
// 双指针扫描,让j+1~i这些边乘车,j这条边有可能部分乘车
while (sum[i] - sum[j] > bus) j++;
ds[i] = sum_bri[j];
if (j > 0 && b[j]) ds[i] -= min(a[j], bus - (sum[i] - sum[j]));
ds_min[i] = min(ds[i], ds_min[i - 1] + (b[i] ? a[i] : 0)); // i之前搭一次车:ds_min[i],即书上的"ds[i]"
}
for (rint i = cnt, j = cnt + 1; i; i--)
{ // 恰好在i这条边的开头处上车,后面的最小危险程度:ds[i]
// 双指针扫描,让i~j-1这些边乘车,j这条边有可能部分乘车
while (sum[j - 1] - sum[i - 1] > bus) j--;
dt[i] = sum_bri[cnt] - sum_bri[j - 1];
if (j <= cnt && b[j]) dt[i] -= min(a[j], bus - (sum[j - 1] - sum[i - 1]));
}
// 两段乘车分开的情况
int ans = 1 << 30;
for (rint i = 1; i <= cnt; i++) ans = min(ans, dt[i] + ds_min[i - 1]);
// 两段乘车接在一起,2*bus覆盖一次的情况
for (rint i = 1, j = 0; i <= cnt; i++)
{
while (sum[i] - sum[j] > 2 * bus) j++;
int temp = sum_bri[j];
if (j > 0 && b[j]) temp -= min(a[j], 2 * bus - (sum[i] - sum[j]));
ans = min(ans, temp + sum_bri[cnt] - sum_bri[i]);
}
cout << ans << endl;
}
return 0;
}