特殊图
特殊图
竞赛图
\(n\) 个点的竞赛图定义为将 \(n\) 个点的无向完全图每条边任意定向所构成的有向图,通常规定 \(n \ge 3\) 。
性质:
-
竞赛图缩点后为链状偏序集,其有如下推论:
-
一个非平凡强连通竞赛图,其中任意点都被至少一个三元环包含。
-
\(x\) 能到达 \(y\) 当且仅当 \(\mathrm{deg^{out}}(x) > \mathrm{deg^{out}}(y)\) 或 \(x, y\) 强连通。
-
求解 SCC:将所有点的入度升序排序为 \(d_{1 \sim n}\) ,则该竞赛图上所有极大的 SCC 在 \(d\) 中分为若干个互不相交的区间,且一个 \(i\) 为一个区间的右端点当且仅当 \(\sum_{j \le i} d_j = \binom{i}{2}\) 。由此可以 \(O(n)\) 求出所有 SCC。
-
对于 \(n \ge 4\) ,\(n\) 阶强连通竞赛图存在 \(n - 1\) 阶强连通子图。
证明:考虑 \(G_n\) 的 \(1 \sim n - 1\) 组成的图 \(G_{n - 1}\) ,缩点后记为 \(a_{1 \sim k}\) ,分类讨论:
-
\(k = 1\) :此时 \(G_{n - 1}\) 已经强连通,得证。
-
\(k = 2\) :不难发现 \(G_n\) 中存在边 \((u, n)\) 和 \((n, v)\) ,其中 \(u \in a_2, v \in a_1\) 。由于 \(n \ge 4\) ,则 \(a_1, a_2\) 中至少存在一者大小 \(\ge 2\) ,不妨设为 \(a_2\) ,则只要保证 \(u\) 不被删去,则图中存在 \(a_2 \to n \to a_1\) 的路径,此时剩下的图强连通。
-
\(k \ge 3\)
由于 \(G_n\)强连通,因此 \(a_k\) 存在一条经过 \(n\) 的路径到 \(a_1\) 。
不难发现 \(G_n\) 中存在边 \((u, n)\) 和 \((n, v)\) ,其中 \(u \in a_k, v \in a_1\) 。
又因为 \(G\) 是竞赛图,因此对于任意 \(x \in a_i, y \in a_j, i < j\) ,均存在边 \(x \to y\) 。
于是考虑删去任意点 \(u \in a_{2 \sim k - 1}\) ,于是对于任意点对 \(x, y \not \in \{ u, n \}\) ,存在路径 \(x \to a_k \to n \to a_1 \to y\) ,因此去掉 \(u\) 即可得到 \(n - 1\) 阶强连通子图。
-
-
-
Redei 定理:竞赛图存在 Hamilton 路径。
构造:考虑增量法。对于一条已经有的 Hamilton 路径 \(s \to t\) ,现在要插入 \(u\) 。若存在一个 \(x\) 满足存在边 \(x \to u \to nxt_x\) 则在此插入 \(u\) ,否则在头或尾插入 \(u\) 即可。
-
Camion-Moon 定理:强连通竞赛图存在 Hamilton 回路。
构造:首先找到一条 Hamilton 路径,然后找到路径上第一个向起点有边的点,作为初始的环 \(s - t\) 。接着向后遍历 \(u\) ,其中 \(t \to u\) 。若 \(u \to s\) ,则令 \(t = u\) 即可。否则找到第一个 \(v\) 满足 \(u \to nxt_v\) ,则可以构成一个 \(nxt_v \to \cdots \to t \to s \to \cdots \to v \to u \to nxt_v\) 的 Hamilton 回路。
-
Landau 定理:定义一个竞赛图的比分序列是把每个点的出度升序排序得到的序列,一个长度为 \(n\) 的不降序列 \(d_{1 \sim n}\) 是合法的比分序列当且仅当 \(\forall k \in [1, n], \sum_{i = 1}^k d_i \ge \binom{k}{2}\) 。
P3561 [POI2017] Turysta
求竞赛图每个点开始的最长简单路径。
\(n \le 2 \times 10^3\)
考虑缩点后按照拓扑序构造 Hamilton 路径,在每个强连通分量内选一个哈密顿回路然后走到下一个强连通分量即可,时间复杂度 \(O(n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7;
vector<int> vec[N];
int in[N], leader[N], nxt[N];
bool e[N][N];
int n, scc;
inline void GetSCC() {
vector<int> id(n);
iota(id.begin(), id.end(), 1);
sort(id.begin(), id.end(), [](const int &a, const int &b) {
return in[a] < in[b];
});
for (int i = 0, sum = 0; i < n; sum += in[id[i++]]) {
if (sum == i * (i - 1) / 2)
++scc;
vec[leader[id[i]] = scc].emplace_back(id[i]);
}
}
inline int GetHamiltonPath(vector<int> &id) {
int s = id[0], t = s;
for (int i = 1; i < id.size(); ++i) {
int u = id[i];
if (e[t][u])
nxt[t] = u, t = u;
else if (e[u][s])
nxt[u] = s, s = u;
else {
for (int v = s; v != t; v = nxt[v])
if (e[v][u] && e[u][nxt[v]]) {
nxt[u] = nxt[v], nxt[v] = u;
break;
}
}
}
return s;
}
inline void GetHamiltonCircle(vector<int> &id) {
int s = GetHamiltonPath(id), t = 0;
for (int u = nxt[s]; u; u = nxt[u]) {
if (t) {
if (e[u][s])
t = u;
else {
for (int v = s; v != t; v = nxt[v])
if (e[u][nxt[v]]) {
int w = nxt[v];
nxt[v] = nxt[t], nxt[t] = s;
s = w, t = u;
break;
}
}
} else if (e[u][s])
t = u;
}
nxt[t] = s;
}
signed main() {
scanf("%d", &n);
for (int i = 2; i <= n; ++i)
for (int j = 1; j < i; ++j) {
int w;
scanf("%d", &w);
if (w)
e[j][i] = true, ++in[i];
else
e[i][j] = true, ++in[j];
}
GetSCC();
for (int i = 1; i <= scc; ++i)
GetHamiltonCircle(vec[i]);
for (int i = 1; i <= n; ++i) {
vector<int> path;
int u = i, bel = leader[i];
for (;;) {
path.emplace_back(u);
if (vec[bel].size() > 1) {
for (int x = nxt[u]; x != u; x = nxt[x])
path.emplace_back(x);
}
if (bel == scc)
break;
u = vec[++bel][0];
}
printf("%d ", path.size());
for (int it : path)
printf("%d ", it);
puts("");
}
return 0;
}
CF1498E Two Houses
这是一道交互题。
有一张 \(n\) 个点的竞赛图,并给出每个点的入度 \(k_i\) 。
可以通过交互询问从 \(u\) 能否到达 \(v\),但一旦回答了”是“,就不能再询问。
定义一个点对 \((u,v)\) 的价值是 \(|k_u - k_v|\) 。求所有双向可达的点对中价值最大的任意一对,或者输出无解。
\(n \le 500\)
直接求出 SCC 即可,无需询问。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e2 + 7;
int in[N], out[N], leader[N];
int n, scc;
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", in + i), out[i] = n - 1 - in[i];
vector<int> id(n);
iota(id.begin(), id.end(), 1);
sort(id.begin(), id.end(), [](const int &a, const int &b) {
return out[a] < out[b];
});
for (int i = 0, sum = 0; i < n; sum += out[id[i++]]) {
if (sum == i * (i - 1) / 2)
++scc;
leader[id[i]] = scc;
}
int ans = -1, ansx = 0, ansy = 0;
for (int i = 1; i <= n; ++i)
for (int j = i + 1; j <= n; ++j)
if (leader[i] == leader[j] && abs(in[i] - in[j]) > ans)
ans = abs(in[i] - in[j]), ansx = i, ansy = j;
printf("! %d %d", ansx, ansy);
return 0;
}
CF1268D Invertation in Tournament
给定一张 \(n\) 个点的竞赛图,定义一次操作为选取一个顶点 \(v\) 并翻转所有以 \(v\) 为端点的边的方向。
判断是否存在一种操作方案使得操作完成后这个图强连通。求出最小操作次数,以及使得操作次数达到最小的方案数。
\(3 \le n \le 2000\)
结论一:对于 \(n \ge 4\) 的强连通竞赛图,总可以翻转恰好一个点,使得它还是强连通的。
证明:由于 \(n \ge 4\) 阶强连通竞赛图存在 \(n - 1\) 阶强连通子图,只要翻转多出来的那个点即可。
结论二:\(n > 6\) 时,总可以通过翻转至多 \(1\) 个点使得原图变成强连通的。
证明:设原图 \(G\) 缩点并拓扑排序之后得到 \(a_{1 \sim k}\) 。
\(k = 1\) :不需要翻转。
\(k= 2\) :由于 \(n > 6\) ,则 \(a_1, a_2\) 中必存在至少一者点数 \(\ge 4\) ,不妨设为 \(a_1\) 。那么 \(a_1\) 中存在一点 \(u\) ,使得翻转 \(u\) 之后,\(a_1\) 仍强连通。翻转之后存在一条从 \(a_2\) 某一点指向 \(u\) 的边。故形成了一个包含 \(a_1, a_2\) 的环,于是整个图强连通。
\(k \ge 3\) :任取 \(u \in a_{2 \sim k - 1}\) 进行翻转,则对于任意顶点对 \((x, y)\) ,都有路径 \(\{ x, a_k, u, a_1, y \}\),从而翻转 \(u\) 后的图强连通。
于是对于 \(n>6\) 的情况只要枚举翻转的点判断即可。
对于 \(n \le 6\) 的情况,与 \(n > 6\) 时一样先枚举 \(0 / 1\) 次操作是否有解,接下来判断是否需要更多次操作或无解:
- \(n = 3\) :答案只能是 \(0\) 或 \(1\) ,不用特判。
- \(n = 4\) :一定无解,答案为 \(-1\) 。
- \(n = 5\) :此时只要操作有入度的单点即可,不用特判。
- \(n = 6\) :
- \(1\) 个三元环和 \(3\) 个单点,这个同 \(n = 5\) 。
- \(2\) 个三元环,此时需要分别对每个三元环的一个点操作 \(1\) 次,方案有 \(2 \times 3 \times 3 = 18\) 种。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7;
int out[N];
char str[N];
bool e[N][N];
int n;
inline bool check() {
vector<int> d(out + 1, out + n + 1);
sort(d.begin(), d.end());
for (int i = 0, sum = d[0]; i < n - 1; sum += d[++i])
if (sum == i * (i + 1) / 2)
return false;
return true;
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%s", str + 1);
for (int j = 1; j <= n; ++j)
if (str[j] == '1')
e[i][j] = true, ++out[i];
}
if (check())
return puts("0 1"), 0;
int ans = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (j == i)
continue;
if (e[i][j])
--out[i], ++out[j];
else
++out[i], --out[j];
}
if (check())
++ans;
for (int j = 1; j <= n; ++j) {
if (j == i)
continue;
if (e[i][j])
++out[i], --out[j];
else
--out[i], ++out[j];
}
}
if (ans)
printf("1 %d", ans);
else if (n == 6)
puts("2 18");
else
puts("-1");
return 0;
}
QOJ5407. 基础图论练习题
给定一张竞赛图,对于每对 \(1 \le i < j \le n\) ,求反转连接 \(i, j\) 的边后求新图有多少个极大 SCC 。
\(n \le 5 \times 10^3\)
容易发现反转一条边对从小到大排序后的度数序列的每个位置变化量 \(\Delta \in \{-1, 0, 1 \}\) ,于是直接预处理三个前缀和数组即可 \(O(1)\) 查询,时间复杂度 \(O(n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e3 + 7;
pair<int, int> a[N];
int pw2[N * N], out[N], d[N], pre1[N], pre2[N], pre3[N];
char str[N];
bool e[N][N];
int n;
signed main() {
pw2[0] = 1;
for (int i = 1; i < N * N; ++i)
pw2[i] = 2ll * pw2[i - 1] % Mod;
int T;
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
memset(out + 1, 0, sizeof(int) * n);
for (int i = 1; i <= n; ++i)
memset(e[i] + 1, false, sizeof(bool) * n);
for (int i = 2; i <= n; ++i) {
scanf("%s", str);
for (int l = 1, r; l < i; l += 4) {
r = min(l + 3, i - 1);
int res = str[(l - 1) >> 2] <= '9' ? str[(l - 1) >> 2] & 15 : str[(l - 1) >> 2] - 'A' + 10;
for (int j = l; j <= r; ++j) {
if (res >> (j - l) & 1)
e[i][j] = true, ++out[i];
else
e[j][i] = true, ++out[j];
}
}
}
memcpy(d + 1, out + 1, sizeof(int) * n);
sort(d + 1, d + 1 + n);
for (int l = 1, r; l <= n; l = r + 1) {
r = l;
while (r < n && d[r + 1] == d[l])
++r;
a[d[l]] = make_pair(l, r);
}
for (int i = 1, sum = d[1]; i <= n; sum += d[++i]) {
pre1[i] = pre1[i - 1] + (sum == i * (i - 1) / 2);
pre2[i] = pre2[i - 1] + (sum + 1 == i * (i - 1) / 2);
pre3[i] = pre3[i - 1] + (sum - 1 == i * (i - 1) / 2);
}
int ans = 0;
for (int i = 1; i <= n; ++i)
for (int j = i + 1; j <= n; ++j) {
int x = a[out[e[i][j] ? j : i]].second, y = a[out[e[i][j] ? i : j]].first;
if (x + 1 == y && d[x] + 1 == d[y])
ans = (ans + 1ll * pre1[n] * pw2[(j - 1) * (j - 2) / 2 + i - 1]) % Mod;
else if (x < y)
ans = (ans + 1ll * (pre1[x - 1] + pre1[n] - pre1[y - 1] + pre2[y - 1] - pre2[x - 1]) *
pw2[(j - 1) * (j - 2) / 2 + i - 1]) % Mod;
else
ans = (ans + 1ll * (pre1[y - 1] + pre1[n] - pre1[x - 1] + pre3[x - 1] - pre3[y - 1]) *
pw2[(j - 1) * (j - 2) / 2 + i - 1]) % Mod;
}
printf("%d\n", ans);
}
return 0;
}
CF1514E Baby Ehab's Hyper Apartment
这是一道交互题。
有一张 \(n\) 个点的竞赛图,可以进行两种询问:
- 询问边 \((x, y)\) 的方向,限制次数 \(9n\) 。
- 询问 \(x\) 有无到点集 \(S\) 中的边,限制次数 \(2n\) 。
最后对于所有 \((i, j)\) 判断是否存在 \(i\) 到 \(j\) 的路径。
\(n \le 100\)
考虑先求出原图的一个 Hamilton 路径,再在 Hamilton 路径上求解强连通分量。
考虑归并,假设现在要求 \([l, r]\) 点的 Hamilton 路径,已知 \([l, mid]\) 和 \([mid + 1, r]\) 的 Hamilton 路径,要将它们归并。设 \([l, mid]\) 的 Hamilton 路径起点为 \(u\) ,\([mid + 1, r]\) 的 Hamilton 路径起点为 \(v\) 。那么调用第一种询问,若 \(u \to v\) 则把 \(u\) 放在 \([l, r]\) 的 Hamilton 路径的第一个,否则把 \(v\) 放在第一个。这部分需要 \(O(n \log n)\) 次询问一,用 stable_sort
可以简洁实现。
最后倒序枚举 Hamilton 路径上的点,则每个点最前可以回到的点是单调的,于是用双指针即可做到 \(\leq 2n\) 次的询问二。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e2 + 7;
vector<int> path;
int leader[N];
bool ans[N][N];
int T, n;
inline bool query1(int x, int y) {
cout << "1 " << x << ' ' << y << endl;
bool res;
cin >> res;
return res;
}
inline bool query2(int x, int len) {
cout << "2 " << x << ' ' << len + 1;
for (int i = 0; i <= len; ++i)
cout << ' ' << path[i];
cout << endl;
bool res;
cin >> res;
return res;
}
signed main() {
cin >> T;
while (T--) {
cin >> n;
path.resize(n), iota(path.begin(), path.end(), 0);
stable_sort(path.begin(), path.end(), query1);
for (int i = 0; i < n; ++i)
memset(ans[i], true, sizeof(bool) * n);
for (int i = n - 1, j = n - 2; ~i; --i) {
if (i == j) {
for (int x = i + 1; x < n; ++x)
for (int y = 0; y <= i; ++y)
ans[path[x]][path[y]] = false;
--j;
}
while (query2(path[i], j))
--j;
}
cout << "3\n";
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j)
cout << ans[i][j];
cout << endl;
}
cin >> n;
}
return 0;
}
P9545 [湖北省选模拟 2023] 环山危路 / road
给定一个竞赛图,边的流量均为 \(1\) ,多次询问多源单汇最大流。
\(n \le 3000\) ,\(m \le 30000\)
考虑将最大流转化为最小割,那么会将原图分为两个集合 \(S, T\) ,割即 \(f(S, T) = \sum_{u \in S} \sum_{v \in T} e_{u, v}\) 。
注意到 \(\begin{cases} f(S, T) + f(T, S) = |S| \times |T| \\ f(S, T) - f(T, S) = \sum_{u \in S} out_u - in_u \end{cases}\) ,于是 \(f(S, T) = \frac{|S| \times |T| + \sum_{u \in S} out_u - in_u}{2}\) 。
固定了 \(|S|\) ,则 \(S\) 中的点将 \(out_u - in_u\) 排序后贪心即可,时间复杂度 \(O(nm)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 3e3 + 7;
int val[N];
char e[N][N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%s", e[i] + 1);
int out = count(e[i] + 1, e[i] + n + 1, '1');
val[i] = out - (n - 1 - out);
}
vector<int> id(n);
iota(id.begin(), id.end(), 1);
sort(id.begin(), id.end(), [](const int &a, const int &b) {
return val[a] < val[b];
});
while (m--) {
bitset<N> vis;
int s, t, res = 0;
scanf("%d%d", &t, &s);
vis.set(t);
for (int i = 1; i <= s; ++i) {
int x;
scanf("%d", &x);
vis.set(x), res += val[x];
}
int ans = res + s * (n - s);
for (int it : id) {
if (vis.test(it))
continue;
++s, res += val[it];
if (res + s * (n - s) <= ans)
ans = res + s * (n - s);
}
printf("%d\n", ans / 2);
}
return 0;
}
UOJ181. 【UR #12】密码锁
给出一张完全图,给出 \(m\) 条边的边权,其余边的边权均为 \(5000\) 。
对于一条边 \((u, v, w)\) ( \(u < v\) ),其有 \(\frac{w}{10^4}\) 的概率被定向为 \(u \to v\) ,有 \(1 - \frac{w}{10^4}\) 的概率被定向为 \(v \to u\) 。
求定向后的图中 SCC 数量的期望 \(\times (10^4)^{n(n - 1)} \bmod 998244353\) 。
\(n \le 38\) ,\(m \le 19\)
不难发现定向后的图是竞赛图,而竞赛图的 SCC 缩点后是一条链,那么 SCC 的数量等价于链的点数。考虑统计每个集合作为链上的点的概率,但是发现要钦定哪些点在前哪些点在后。于是考虑统计每个集合 \(S\) 作为链上的前缀的概率,记 \(U = \{ 1, 2, \cdots, n \}\) ,则 \(S\) 与 \(U \setminus S\) 之间的边方向均为 \(S\) 到 \(U \setminus S\) 。直接统计可以做到 \(O(n 2^n)\) 。
发现 \(m\) 比较小,考虑单独统计 \(m\) 条特殊边的贡献。若 \(S\) 与 \(U \setminus S\) 之间没有特殊边,则概率为 \(0.5^{|S| \times (n - |S|)}\) 。枚举所有特殊边的集合,那么这些边能对一个 \(S\) 产生影响当且仅当一端在 \(S\) 而一端不在 \(S\) 。
若只保留 \(m\) 条特殊边,则会形成若干连通块。连通块之间的边权均为 \(0.5\) ,可以等到最后考虑。注意到一个 \(m\) 条边的连通块最多只有 \(m + 1\) 个点,可以直接对每个连通块枚举哪些点在 \(S\) 中,并计算特殊边产生的贡献。
最后做一次背包 DP 合并每个连通块的信息即可做到 \(O(m 2^m + nm + n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353, inv2 = (Mod + 1) / 2;
const int N = 39;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct Edge {
int u, v, w;
} e[N];
vector<int> vec;
int bel[N], id[N], f[N], g[N];
int n, m, tot;
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;
}
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
void dfs(int u) {
bel[u] = tot, id[u] = vec.size(), vec.emplace_back(u);
for (int v : G.e[u])
if (!bel[v])
dfs(v);
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
e[i] = (Edge){--u, --v, 1ll * w * mi(1e4, Mod - 2) % Mod};
G.insert(u, v), G.insert(v, u);
}
f[0] = 1;
for (int i = 0; i < n; ++i)
if (!bel[i]) {
++tot, vec.clear(), dfs(i);
memset(g, 0, sizeof(g));
for (int s = 0; s < (1 << vec.size()); ++s) {
int res = 1;
for (int j = 0; j < m; ++j) {
int u = e[j].u, v = e[j].v, w = e[j].w;
if (bel[u] == tot && ((s >> id[u] & 1) ^ (s >> id[v] & 1)))
res = 2ll * res % Mod * (s >> id[u] & 1 ? w : dec(1, w)) % Mod;
}
g[__builtin_popcount(s)] = add(g[__builtin_popcount(s)], res);
}
for (int j = n; ~j; --j) {
f[j] = 1ll * f[j] * g[0] % Mod;
for (int k = 1; k <= min(j, (int)vec.size()); ++k)
f[j] = add(f[j], 1ll * f[j - k] * g[k] % Mod);
}
}
int ans = 0;
for (int i = 1; i <= n; ++i)
ans = add(ans, 1ll * f[i] * mi(inv2, i * (n - i)) % Mod);
printf("%d", 1ll * ans * mi(1e4, n * (n - 1)) % Mod);
return 0;
}
欧拉图
定义:
- 欧拉路径:通过图中每条边恰好一次的路径。
- 欧拉回路:通过图中每条边恰好一次的回路。
- 半欧拉图:具有欧拉路径但不具有欧拉回路的图。
- 欧拉图:具有欧拉回路的图。
判定:
- 无向图:
- 欧拉图:非零度点连通、不存在奇度点。
- 半欧拉图:非零度点连通、恰有两个奇度点(这两点作为起点和终点)。
- 有向图:
- 欧拉图:非零度点强连通、每个点的入度和出度相等。
- 半欧拉图:
- 非零度点弱连通。
- 恰有一个点 \(S\) 的入度比出度小 \(1\) (其作为欧拉路径的起点)。
- 恰有一个点 \(T\) 的出度比入度小 \(1\) (其作为欧拉路径的终点)。
- 其他顶点的入度和出度相等。
求解欧拉回路或路径常用 Hierholzer 算法,其自动寻找欧拉回路,在找不到欧拉回路的情况下会找到欧拉路径,需要指定起点。
流程:从起点开始递归,每次尝试寻找 \(u\) 的处边递归处理后删除,回溯时将 \(u\) 插入答案栈中,倒序输出(因为这样后面加入的环才可以在欧拉路径上显示为走到)。
时间复杂度 \(O(n + m)\) ,注意需要加当前弧优化不然复杂度会退化到平方级别。
若要按字典序输出答案,首先起点要选的尽量小,然后遍历时尽量走小的点即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
int cur[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int in[N], out[N];
int n, m;
inline int check() {
int S = 1, cnt_out = 0, cnt_in = 0;
for (int i = 1; i <= n; ++i) {
if (out[i] - in[i] == 1)
++cnt_out, S = i;
else if (in[i] - out[i] == 1)
++cnt_in;
else if (abs(in[i] - out[i]) > 1)
return -1;
}
return cnt_out <= 1 && cnt_in <= 1 ? S : -1;
}
void hierholzer(int u, stack<int> &ans) {
while (!G.e[u].empty()) {
int v = G.e[u].back();
G.e[u].pop_back(), hierholzer(v, ans);
}
ans.emplace(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), ++in[v], ++out[u];
}
for (int i = 1; i <= n; ++i)
sort(G.e[i].begin(), G.e[i].end(), greater<int>());
int S = check();
if (S == -1)
return puts("No"), 0;
stack<int> ans;
hierholzer(S, ans);
while (!ans.empty())
printf("%d ", ans.top()), ans.pop();
return 0;
}
P5921 [POI1999] 原始生物
给定若干限制 \((x, y)\) 表示存在一个 \(i\) 使得 \((a_i, a_{i + 1}) = (x, y)\) ,求一个最小长度满足存在该长度的一个序列 \(a\) 满足所有限制,保证没有形如 \((x, x)\) 的限制。
\(n \le 10^6\) ,\(x, y \le 10^3\)
对于一个限制 \((x, y)\) ,连边 \(x \to y\) ,则问题转化为加入最少的边使得图存在一条欧拉路径。
首先注意到若原图有 \(c\) 个连通块,则至少需要 \(c - 1\) 条边将它们连起来。然后考虑连通块内部的情况,则对于入度大于出度的点,只能保留一个且保留的点出入度差值为 \(1\) ,出度大于入度的点也是类似的。记所有入度大于出度的点的出入度差为 \(s\) ,则增加一条边就能使 \(s\) 小 \(1\) ,答案即为 \(s - 1\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 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;
int deg[N], cnt[N];
bool e[N][N], vis[N];
int n;
signed main() {
scanf("%d", &n), dsu.prework(1e3);
for (int i = 1, x, y; i <= n; ++i) {
scanf("%d%d", &x, &y);
if (e[x][y]) {
--i, --n;
continue;
}
e[x][y] = true, vis[x] = vis[y] = true;
dsu.merge(x, y), ++deg[x], --deg[y];
}
for (int i = 1; i < N; ++i)
if (deg[i] > 0)
cnt[dsu.find(i)] += deg[i];
int ans = n;
for (int i = 1; i < N; ++i)
if (vis[i] && dsu.find(i) == i)
ans += max(cnt[i], 1);
printf("%d", ans);
return 0;
}
P10777 BZOJ3706 反色刷
给出一张无向图,边有黑白两种颜色。每次可以选择一条回路,并将途径所有边颜色反转,\(q\) 次操作:
- 反转一条边的颜色。
- 求至少多少次操作才能使所有边变成白色。
\(n, m, q \le 10^6\)
不难发现黑边必须经过奇数次,白边必须经过偶数次。
先判掉无解的情况,有解的充要条件是黑边的导出子图每个点都是偶度点。
考虑求解最优方案,若一个连通块存在黑边,则需要一次操作处理,否则不需要。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
struct DSU {
int fa[N], num[N];
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1);
memset(num + 1, 0, sizeof(int) * 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)
num[x] += num[y], fa[y] = x;
}
} dsu;
struct Edge {
int u, v, w;
} e[N];
int d[N];
int n, m, q, odd, ans;
inline void insert(int u, int v, int w) {
if (w) {
if (!dsu.num[dsu.find(u)])
++ans;
++dsu.num[dsu.find(u)];
odd -= d[u] + d[v], odd += (d[u] ^= 1) + (d[v] ^= 1);
}
}
inline void remove(int u, int v, int w) {
if (w) {
--dsu.num[dsu.find(u)];
if (!dsu.num[dsu.find(u)])
--ans;
odd -= d[u] + d[v], odd += (d[u] ^= 1) + (d[v] ^= 1);
}
}
signed main() {
scanf("%d%d", &n, &m);
dsu.prework(n);
for (int i = 0; i < m; ++i)
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w), dsu.merge(e[i].u, e[i].v);
for (int i = 0; i < m; ++i)
insert(e[i].u, e[i].v, e[i].w);
scanf("%d", &q);
while (q--) {
int op;
scanf("%d", &op);
if (op == 1) {
int x;
scanf("%d", &x);
remove(e[x].u, e[x].v, e[x].w), insert(e[x].u, e[x].v, e[x].w ^= 1);
} else
printf("%d\n", odd ? -1 : ans);
}
return 0;
}
CF508D Tanya and Password
给出长度为 \(n+2\) 的字符串 \(s\) 的所有 \(n\) 个长度为 \(3\) 的子串,求一个合法的原串,或输出
NO
。字符集为大小写字母与数字,\(n \le 2\times 10^5\)。
对于给出的字符串 \(xyz\) ,看作有向边 \(xy \to yz\) ,跑欧拉路径即可。
事实上如果通过求解哈密顿路的方向思考也是类似的,但是求解哈密顿路没有什么优秀复杂度的做法。一个常见的转化方向是边和点互相转化,这样每个点经过一次就转化为每条边经过一次,而欧拉路径相关求解往往是更为简单的。
#include <bits/stdc++.h>
using namespace std;
const int N = 1 << 16 | 1;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<int> Answer;
int indeg[N], outdeg[N];
bool exist[N];
int n, S = -1;
void dfs(int u) {
while (!G.e[u].empty()) {
int v = G.e[u].back();
G.e[u].pop_back();
dfs(v);
}
Answer.emplace_back(u);
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
char str[7];
scanf("%s", str + 1);
int u = (str[1] << 8) + str[2], v = (str[2] << 8) + str[3];
G.insert(u, v), ++outdeg[u], ++indeg[v];
exist[u] = exist[v] = true;
}
int cnt = 0;
for (int i = 0; i < N; ++i)
if (exist[i]) {
if (outdeg[i] - indeg[i] == 1)
S = i;
if (abs(outdeg[i] - indeg[i]) > 1)
return puts("NO"), 0;
cnt += (outdeg[i] != indeg[i]);
}
if (cnt && cnt != 2)
return puts("NO"), 0;
if (S == -1) {
for (int i = 0; i < N; ++i)
if (exist[i]) {
S = i;
break;
}
}
dfs(S), reverse(Answer.begin(), Answer.end());
if (Answer.size() + 1 != n + 2)
return puts("NO"), 0;
puts("YES");
for (int x : Answer)
putchar(x >> 8);
putchar(Answer.back() & (1 << 8) - 1);
return 0;
}
CF1458D Flip and Reverse
给定 \(01\) 字符串 \(S\) ,每次可以将含 \(0\) 和 \(1\) 数量相同的子串取反后翻转,求字典序最小的结果。
\(\sum |S| \le 5 \times 10^5\)
将 \(0\) 看作 \(-1\) ,\(1\) 看作 \(1\) ,记前缀和为 \(s_i\) ,每次操作相当于翻转一段两端等高的折线。
将 \((s_{i - 1}, s_i)\) 连成无向图,操作不改变图的所有边,且任何可达字符串是 \(0\) 到 \(s_n\) 的欧拉路径,反之也是成立的。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
struct Graph {
vector<pair<int, int> > e[N];
int cur[N];
bool vis[N];
int tot;
inline void clear(int n) {
for (int i = 0; i <= n; ++i)
e[i].clear(), cur[i] = 0;
while (tot)
vis[tot--] = false;
}
inline void insert(int u, int v) {
e[u].emplace_back(v, ++tot), e[v].emplace_back(u, tot);
}
} G;
stack<int> ans;
char str[N];
int T, n;
void hierholzer(int u, int pre) {
for (int i = G.cur[u]++; i < G.e[u].size(); i = G.cur[u]++)
if (!G.vis[G.e[u][i].second])
G.vis[G.e[u][i].second] = true, hierholzer(G.e[u][i].first, u);
if (pre)
ans.emplace(u);
}
signed main() {
scanf("%d", &T);
while (T--) {
scanf("%s", str + 1);
n = strlen(str + 1), G.clear(n * 2);
for (int i = 1, sum = n; i <= n; sum += (str[i++] == '1' ? 1 : -1))
G.insert(sum, sum + (str[i] == '1' ? 1 : -1));
for (int i = 0; i <= n * 2; ++i)
sort(G.e[i].begin(), G.e[i].end());
hierholzer(n, 0);
for (int pre = n; !ans.empty(); pre = ans.top(), ans.pop())
putchar(ans.top() > pre ? '1' : '0');
puts("");
}
return 0;
}
P11762 [IAMOI R1] 走亲访友
给出一张无向图,构造一条路径满足:
- 起点为 \(S\) ,终点不限。
- 对于每一条边 \((u_i, v_i)\) ,需要确定 \(p_i \in \{ 0, 1 \}\) ,其中 \(p_i = 0\) 表示删去该边(之后不能再次经过这条边),\(p_i = 1\) 则保留该边(之后还可以被经过)。
- 路径长度 \(\le n + m\) 。
- 最后保留下来的边构成一棵 \(n\) 个点的树。
\(n \le 10^3\) ,\(m \le \frac{n(n - 1)}{2}\)
如果原图是欧拉图,那么只要随便钦定一棵生成树,其余边删掉即可。
考虑一般情况,由于一条边可以经过多次,那么将其视为重边,钦定每条边只能经过一次。
考虑将一般图加重边转化为欧拉图,一个简单的方式是先找到一个生成树,然后自底向上考虑。若当前点度数为奇数,则加一条与父亲之间的边。
不难发现总边数 \(\le m + n - 1\) ,满足限制,时间复杂度 \(O(n + m)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;
struct Graph {
struct Edge {
int nxt, v, w;
bool vis;
} e[N * N];
int head[N];
int tot = 1;
inline void insert(int u, int v, int w) {
e[++tot] = (Edge){head[u], v, w, false}, head[u] = tot;
}
} G;
vector<int> ans;
set<int> st;
int deg[N];
bool vis[N], flag[N * N];
int n, m, k, s;
void dfs(int u, int f) {
vis[u] = true;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v, w = G.e[i].w;
if (vis[v] || i > (m << 1 | 1))
continue;
flag[w] = true, dfs(v, u);
if (deg[v] & 1)
G.insert(u, v, w), G.insert(v, u, w), ++deg[u], ++deg[v];
}
}
void hierholzer(int u) {
for (int &i = G.head[u]; i; i = G.e[i].nxt) {
if (G.e[i].vis)
continue;
int id = G.e[i].w;
G.e[i].vis = G.e[i ^ 1].vis = true;
hierholzer(G.e[i].v), ans.emplace_back(id);
}
}
signed main() {
scanf("%d%d%d%d", &n, &m, &k, &s);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v, i), G.insert(v, u, i);
++deg[u], ++deg[v];
}
dfs(1, 0), hierholzer(s);
printf("%d\n", (int)ans.size());
for (int i = ans.size() - 1; ~i; --i)
printf("%d %d\n", ans[i], flag[ans[i]]);
return 0;
}
CF547D Mike and Fish
给定 \(n\) 个二维平面上的点,需要将每个点黑白染色,满足每行或每列两种颜色点的数量差值 \(\le 1\) ,构造一组方案。
\(n \le 2 \times 10^5\)
考虑将每个点转化为该行和该列连边,染色转化为给边定向,则限制条件转化为每个点的出入度差值 \(\le 1\) 。
新建一个虚点,将奇度点向虚点连边,求出一条欧拉回路即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;
struct Graph {
struct Edge {
int nxt, v, vis;
} e[N * 6];
int head[N << 1];
int tot = 1;
inline void insert(int u, int v) {
e[++tot] = (Edge){head[u], v, -1}, head[u] = tot;
}
} G;
int deg[N << 1];
int n;
void Hierholzer(int u) {
for (int &i = G.head[u]; i; i = G.e[i].nxt)
if (G.e[i].vis == -1)
G.e[i].vis = G.e[i ^ 1].vis = i & 1, Hierholzer(G.e[i].v);
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
int x, y;
scanf("%d%d", &x, &y);
G.insert(x, y + N), G.insert(y + N, x);
++deg[x], ++deg[y + N];
}
for (int i = 1; i < N * 2; ++i)
if (deg[i] & 1)
G.insert(0, i), G.insert(i, 0);
for (int i = 0; i < N * 2; ++i)
Hierholzer(i);
for (int i = 1; i <= n; ++i)
putchar(G.e[i << 1].vis ? 'r' : 'b');
return 0;
}
CF429E Points and Segments
给定数轴上 \(n\) 条线段,构造一组将这些线段红蓝染色的方案,满足对于任意点,其被红、蓝线段覆盖的数量差的绝对值 \(\le 1\) 。
\(n \le 10^5\)
考虑将红蓝线段赋上 \(\pm 1\) 的权值,则限制转化为任一点的权值和 \(\in \{ -1, 0, 1 \}\) ,其中被覆盖偶数次的点必然为 \(0\) ,被覆盖奇数次的点必然为 \(\pm 1\) 。
考虑额外加入若干条线段,将覆盖奇数次的点覆盖成偶数次,这样就要求每个点的权值为和 \(0\) 。
考虑转化为差分序列,每次将 \(c_l \to c_l + 1, c_{r + 1} \to c_{r + 1} - 1\) 或 \(c_l \to c_l - 1, c_{r + 1} \to c_{r + 1} + 1\) ,最后 \(c\) 必须为全 \(0\) 。对于每个区间 \([l, r]\) ,连边 \((l, r + 1)\) ,问题转化为给边定向后满足入度等于出度,直接用 Hierholzer 求解欧拉回路即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
struct Graph {
struct Edge {
int nxt, v, c;
} e[N];
int head[N];
int tot = 1;
inline void insert(int u, int v) {
e[++tot] = (Edge){head[u], v, -1}, head[u] = tot;
}
} G;
struct Line {
int l, r;
} p[N];
int a[N], b[N];
int n, m;
void Hierholzer(int u) {
for (int &i = G.head[u]; i; i = G.e[i].nxt)
if (G.e[i].c == -1)
G.e[i].c = G.e[i ^ 1].c = i & 1, Hierholzer(G.e[i].v);
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d%d", &p[i].l, &p[i].r), a[++m] = p[i].l, a[++m] = ++p[i].r;
sort(a + 1, a + m + 1), m = unique(a + 1, a + m + 1) - a - 1;
for (int i = 1; i <= n; ++i) {
p[i].l = lower_bound(a + 1, a + m + 1, p[i].l) - a;
p[i].r = lower_bound(a + 1, a + m + 1, p[i].r) - a;
b[p[i].l] ^= 1, b[p[i].r] ^= 1;
G.insert(p[i].l, p[i].r), G.insert(p[i].r, p[i].l);
}
for (int i = 1; i <= m + 1; ++i)
if (b[i] ^= b[i - 1])
G.insert(i, i + 1), G.insert(i + 1, i);
for (int i = 1; i <= m + 1; ++i)
Hierholzer(i);
for (int i = 1; i <= n; ++i)
printf("%d ", G.e[i << 1].c);
return 0;
}
P6628 [省选联考 2020 B 卷] 丁香之路
有一张 \(n\) 个点的无向完全图,\(i, j\) 之间的边权为 \(|i - j|\) 。
给出 \(m\) 条必经边,对于 \(t = 1, 2, \cdots, n\) ,求从给定的起点 \(s\) 开始,经过所有必经边至少一次,最后到达 \(t\) 的最小路径长度。
\(n \le 2500\) ,\(m \le \frac{n (n - 1)}{2}\)
只连上 \(m\) 条关键边,先考虑 \(s, t\) 和这 \(m\) 条边的端点形成一个连通块的情况。考虑拿出所有 \(\notin \{ s, t \}\) 的奇度点和 \(\in \{s, t \}\) 的偶度点,将其排序后两两配对连上边(不难发现这样连边是最优的),\(s\) 到 \(t\) 的欧拉回路长度(所有边的边权和)即为答案。
接下来考虑形成多个连通块的情况,同样将不合法的点拿出来排序后两两配对连上边,但是此时仍然可能不连通,不存在欧拉路径。
首先发现加入新边 \((x, y)\) 和加入新边组 \((x, x + 1), (x + 1, x + 2), \cdots, (y - 1, y)\) 是等价的,而后者遍历到了更多点,能够不花费代价地同时连接某些连通块。然后考虑再加入额外边连接不同连通块,不难发现取 MST 作为额外边,每条边遍历两次是最优的。
加入额外边的正确性:考虑加入额外边之前新连的两条边 \((a, b), (c, d)\) ,其中 \(a < b < c < d\) 。该方案给出的最优解为 \((b - a) + 2(c - b) + (d - c) = -a - b + c + d\) ,若改连 \((a, c), (b, d)\) ,则给出的解仍为 \(-a - b + c + d\) 。
由于边权 \(\le n\) ,使用基数排序即可优化 Kruskal 的复杂度。
时间复杂度 \(O(n^2 \alpha(n) + m)\) ,一开始先连上边 \((s, t)\) 最后再减去可以降低实现难度。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2.5e3 + 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);
}
} ori;
vector<int> edg[N];
int deg[N];
ll sum;
int n, m, s;
inline ll solve(int s, int t) {
DSU dsu = ori;
++deg[s], ++deg[t], dsu.merge(s, t);
ll ans = sum;
for (int i = 1, lst = 0; i <= n; ++i)
if (deg[i] & 1) {
if (lst) {
ans += i - lst;
for (int j = lst; j < i; ++j)
dsu.merge(j, j + 1);
lst = 0;
} else
lst = i;
}
for (int i = 1; i <= n; ++i)
edg[i].clear();
for (int i = 1, lst = 0; i <= n; ++i)
if (deg[i]) {
if (lst && dsu.find(i) != dsu.find(lst))
edg[i - lst].emplace_back(lst);
lst = i;
}
for (int i = 1; i <= n; ++i)
for (int u : edg[i])
if (dsu.find(u) != dsu.find(u + i))
dsu.merge(u, u + i), ans += i * 2;
return --deg[s], --deg[t], ans;
}
signed main() {
scanf("%d%d%d", &n, &m, &s);
ori.prework(n);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
++deg[u], ++deg[v];
ori.merge(u, v), sum += abs(u - v);
}
for (int i = 1; i <= n; ++i)
printf("%lld ", solve(s, i));
return 0;
}
平面图
若图 \(G\) 可以画在平面上且满足除顶点外没有边相交,则 \(G\) 是一个平面图。
整个平面会被 \(G\) 的边划分为若干区域,每个区域称为 \(G\) 的一个面。
其中面积无限的面称为无限面(或外部面),面积有限的称为有限面(或内部面),包围每个面的所有边组成的回路称为该面的边界。
一个常见的转化是:平面图最大流等于对偶图最短路。
欧拉公式:对于任意连通平面图 \(G\) ,设 \(n, m, r\) 分别为 \(G\) 的点数、边数、面数,则:
推论:若 \(G\) 有 \(p\) 个连通块,则:
推论:若 \(G\) 是 \(n \ge 3\) 阶 \(m\) 条边的简单平面图,则 \(m \le 3n - 6\) 。
CF811E Vladik and Entertaining Flags
给出一个 \(n \times m\) 的网格,每个格子有颜色。
\(q\) 次询问,每次询问只保留 \(l \sim r\) 列时有多少个四连通的颜色块。
\(n \le 10\) ,\(m, q \le 10^5\)
网格图是典型的平面图,因此考虑欧拉定理,只要求出 \(n - m + r - 1\) 即可。
点数即为 \((r - l + 1) \times n\) ,边数可以前缀和存横竖边做差求得,考虑面数的求法。
考虑建立一个新图,把四个格子的中心作为点,把原先网格图上没有的边(相邻格子异色)转化为格点之间的边,原来网格图上有边(相邻格子同色)则不连,则网格图的面数即为新图的连通块数量。
然后考虑并查集维护每个连通块的区间 \([l, r]\) ,则每次只要询问有多少连通块区间属于询问区间即可。因为如果没有被包含仅是有交的话这个面是无限面,不能被统计。
时间复杂度 \(O((nm + q) \log m)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 11, M = 1e5 + 7;
vector<pair<int, int> > qry[M];
vector<int> vec[M];
int a[N][M], s1[M], s2[M], ans[M];
int n, m, q;
inline int getid(int x, int y) {
return (x - 1) * (m - 1) + y;
}
struct DSU {
pair<int, int> domain[N * M];
int fa[N * M];
bool flag[N * M];
inline void prework() {
for (int i = 1; i < n; ++i)
for (int j = 1; j < m; ++j) {
int x = getid(i, j);
fa[x] = x, domain[x] = make_pair(j, j + 1);
}
for (int i = 1; i < n; ++i) {
if (a[i][1] != a[i + 1][1])
flag[getid(i, 1)] = true;
if (a[i][m] != a[i + 1][m])
flag[getid(i, m - 1)] = true;
}
for (int i = 1; i < m; ++i) {
if (a[1][i] != a[1][i + 1])
flag[getid(1, i)] = true;
if (a[n][i] != a[n][i + 1])
flag[getid(n - 1, i)] = true;
}
}
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) {
domain[x].first = min(domain[x].first, domain[y].first);
domain[x].second = max(domain[x].second, domain[y].second);
flag[x] |= flag[y], fa[y] = x;
}
}
} dsu;
struct BIT {
int c[M];
inline void update(int x, int k) {
for (; x <= m; x += x & -x)
c[x] += k;
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
} bit;
signed main() {
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
scanf("%d", a[i] + j);
for (int i = 1; i <= m; ++i) {
s1[i] = s1[i - 1], s2[i] = s2[i - 1];
for (int j = 1; j <= n; ++j) {
if (i < m && a[j][i] == a[j][i + 1])
++s1[i];
if (j < n && a[j][i] == a[j + 1][i])
++s2[i];
}
}
dsu.prework();
for (int i = 2; i < n; ++i)
for (int j = 1; j < m; ++j)
if (a[i][j] != a[i][j + 1])
dsu.merge(getid(i - 1, j), getid(i, j));
for (int i = 1; i < n; ++i)
for (int j = 2; j < m; ++j)
if (a[i][j] != a[i + 1][j])
dsu.merge(getid(i, j - 1), getid(i, j));
for (int i = 1; i <= (n - 1) * (m - 1); ++i)
if (dsu.fa[i] == i && !dsu.flag[i])
vec[dsu.domain[i].first].emplace_back(dsu.domain[i].second);
for (int i = 1; i <= q; ++i) {
int l, r;
scanf("%d%d", &l, &r);
qry[l].emplace_back(r, i);
ans[i] = (r - l + 1) * n - (s1[r - 1] - s1[l - 1]) - (s2[r] - s2[l - 1]);
}
for (int i = m; i; --i) {
for (int it : vec[i])
bit.update(it, 1);
for (auto it : qry[i])
ans[it.second] += bit.query(it.first);
}
for (int i = 1; i <= q; ++i)
printf("%d\n", ans[i]);
return 0;
}
P3776 [APIO2017] 斑斓之地
给定 \(r\) 行 \(c\) 列的空白网格,网格上有一条被染黑的长度为 \(m\) 的路径,\(q\) 次询问子矩阵内的白色连通块数量。
\(r, c \le 2 \times 10^5\) ,\(m, q \le 10^5\)
考虑平面图欧拉公式,只要求出点数、边数、面数即可:
- 点数:只要求矩阵内白色点数量即可,这等价于总点数减去矩阵内黑点数量。
- 边数:考虑容斥,用总边数减去两端存在黑色的边数即可。
- 左右相邻:对于黑点 \((x, y)\) ,同时加入 \((x - 1, y)\) 和 \((x, y)\) ,问题转化为统计矩形内黑点数量。
- 上下相邻:对于黑点 \((x, y)\) ,同时加入 \((x, y - 1)\) 和 \((x, y)\) ,问题转化为统计矩形内黑点数量。
- 面数:特判矩形包含了整个路径的情况,剩下就是是四个白点连成 \(2 \times 2\) 的矩形,这同样可以容斥,减去存在黑色的 \(2 \times 2\) 的矩形即可。对于黑点 \((x, y)\) ,同时加入 \((x - 1, y - 1)\) 、\((x - 1, y)\) 、\((x, y - 1)\) 、\((x, y)\) ,问题转化为统计矩形内黑点数量。
问题转化为二维数点,开四棵主席树,时间复杂度 \(O((r + m + q) \log c)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, S = N << 5;
vector<int> upd[N][4];
char str[N];
int r, c, m, q, bx, by;
struct SMT {
int rt[N], lc[S], rc[S], s[S];
int tot;
int update(int x, int nl, int nr, int p, int k) {
int y = ++tot;
lc[y] = lc[x], rc[y] = rc[x], s[y] = s[x] + k;
if (nl == nr)
return y;
int mid = (nl + nr) >> 1;
if (p <= mid)
lc[y] = update(lc[x], nl, mid, p, k);
else
rc[y] = update(rc[x], mid + 1, nr, p, k);
return y;
}
int query(int x, int y, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r)
return s[y] - s[x];
int mid = (nl + nr) >> 1;
if (r <= mid)
return query(lc[x], lc[y], nl, mid, l, r);
else if (l > mid)
return query(rc[x], rc[y], mid + 1, nr, l, r);
else
return query(lc[x], lc[y], nl, mid, l, r) + query(rc[x], rc[y], mid + 1, nr, l, r);
}
} smt[4];
signed main() {
scanf("%d%d%d%d%d%d", &r, &c, &m, &q, &bx, &by);
if (m)
scanf("%s", str + 1);
int lx = bx, rx = bx, ly = by, ry = by;
auto insert = [](int x, int y) {
upd[x][0].emplace_back(y);
upd[x - 1][1].emplace_back(y), upd[x][1].emplace_back(y);
upd[x][2].emplace_back(y - 1), upd[x][2].emplace_back(y);
upd[x - 1][3].emplace_back(y - 1), upd[x - 1][3].emplace_back(y);
upd[x][3].emplace_back(y - 1), upd[x][3].emplace_back(y);
};
insert(bx, by);
for (int i = 1; i <= m; ++i) {
if (str[i] == 'N')
lx = min(lx, --bx);
else if (str[i] == 'S')
rx = max(rx, ++bx);
else if (str[i] == 'W')
ly = min(ly, --by);
else
ry = max(ry, ++by);
insert(bx, by);
}
for (int i = 1; i <= r; ++i)
for (int j = 0; j < 4; ++j) {
sort(upd[i][j].begin(), upd[i][j].end());
upd[i][j].erase(unique(upd[i][j].begin(), upd[i][j].end()), upd[i][j].end());
smt[j].rt[i] = smt[j].rt[i - 1];
for (int it : upd[i][j])
smt[j].rt[i] = smt[j].update(smt[j].rt[i], 0, c, it, 1);
}
while (q--) {
int xl, xr, yl, yr;
scanf("%d%d%d%d", &xl, &yl, &xr, &yr);
ll ans = 1ll * (xr - xl + 1) * (yr - yl + 1) -
smt[0].query(smt[0].rt[xl - 1], smt[0].rt[xr], 0, c, yl, yr);
if (xl < xr)
ans -= 1ll * (xr - xl) * (yr - yl + 1) -
smt[1].query(smt[1].rt[xl - 1], smt[1].rt[xr - 1], 0, c, yl, yr);
if (yl < yr)
ans -= 1ll * (xr - xl + 1) * (yr - yl) -
smt[2].query(smt[2].rt[xl - 1], smt[2].rt[xr], 0, c, yl, yr - 1);
if (xl < xr && yl < yr)
ans += 1ll * (xr - xl) * (yr - yl) -
smt[3].query(smt[3].rt[xl - 1], smt[3].rt[xr - 1], 0, c, yl, yr - 1);
ans += (xl < lx && rx < xr && yl < ly && ry < yr);
printf("%lld\n", ans);
}
return 0;
}
仙人掌
定义:
- 仙人掌:是一个无向连通图,满足每条边最多在一个环内。
- 沙漠:由多棵仙人掌组成。
仙人掌的边数是 \(O(n)\) 的,一个上界为 \(n - 1 + \lfloor \frac{n}{3} \rfloor\) (连一堆三元环)。
常见的处理方式:
- dfs 树:常用于树上 DP,整体是树形 DP,过程中单独把每个环拿出来 DP,代码好写。
- 圆方树:将每个环拿出来建方点,普适性比较强,便于 DS 维护。
- 虚仙人掌:可以视为圆方树上的虚树。
P4244 [SHOI2008] 仙人掌图 II
给出一棵仙人掌,边权均为 \(1\) ,求直径长度(最短路径最长的点对即为直径端点)。
\(n \le 2 \times 10^4\)
考虑在 dfs 树上 DP,设 \(f_u\) 表示 \(u\) 为链顶的最长链长度:
- 若 \((u, v)\) 不在环上,即 \(low_v > dfn_u\) ,则显然可以更新直径 \(ans \gets \max(ans, f_u + f_v + 1)\) ,之后更新 \(f_u \gets \max(f_u, f_v + 1)\) 。
- 否则 \((u, v)\) 是一条返祖边,不妨设 \(u\) 为这个环的最浅点。在处理完 \(u\) 的所有树边后考虑拿出整个环,相当于用 \(\max (f_i + f_j + \min(|i - j|, len - |i - j|))\) 的更新直径,断环为链后用单调队列即可去掉后面的 \(\min\) 做到线性,最后更新 \(f_u \gets \max (f_v + \mathrm{dist}(u, v))\) ,其中 \(\mathrm{dist}(u, v)\) 为 \((u, v)\) 在环上的距离。
时间复杂度线性。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 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], fa[N], f[N];
int n, m, dfstime, ans;
void solve(int u, int v) {
vector<int> vec = {u};
for (int x = v; x != u; x = fa[x])
vec.emplace_back(x);
int len = vec.size();
for (int i = 0; i < len; ++i)
vec.emplace_back(vec[i]);
deque<int> q;
for (int i = 0; i < len * 2; ++i) {
while (!q.empty() && q.front() < i - len / 2)
q.pop_front();
if (!q.empty())
ans = max(ans, f[vec[i]] + f[vec[q.front()]] + i - q.front());
while (!q.empty() && f[vec[q.back()]] - q.back() < f[vec[i]] - i)
q.pop_back();
q.emplace_back(i);
}
for (int i = 1; i < len; ++i)
f[u] = max(f[u], f[vec[i]] + min(i, len - i));
}
void Tarjan(int u, int father) {
fa[u] = father, dfn[u] = low[u] = ++dfstime;
for (int v : G.e[u]) {
if (v == father)
continue;
if (!dfn[v])
Tarjan(v, u), low[u] = min(low[u], low[v]);
else
low[u] = min(low[u], dfn[v]);
if (low[v] > dfn[u])
ans = max(ans, f[u] + f[v] + 1), f[u] = max(f[u], f[v] + 1);
}
for (int v : G.e[u])
if (v != father && dfn[v] > dfn[u] && fa[v] != u)
solve(u, v);
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int k, u;
scanf("%d%d", &k, &u);
for (int i = 2; i <= k; ++i) {
int v;
scanf("%d", &v);
G.insert(u, v), G.insert(v, u), u = v;
}
}
Tarjan(1, 0);
printf("%d", ans);
return 0;
}
P10779 BZOJ4316 小 C 的独立集
给出一棵仙人掌,求最大独立集。
\(n \le 5 \times 10^4\) ,\(m \le 6 \times 10^4\)
树的情况是简单的,同样考虑拿出一个环单独 DP。设 \(u\) 为最浅点,\(v\) 为最深点,设 \(f_{i, 0/1, 0/1}\) 表示考虑到 \(i\) 、是否选择 \(i\) 、是否选择 \(v\) 的答案,转移不难,时间复杂度线性。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 5e4 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int fa[N], dfn[N], low[N], f[N][2];
int n, m, dfstime;
inline void solve(int u, int v) {
vector<int> vec;
for (int x = v; x != u; x = fa[x])
vec.emplace_back(x);
vector<vector<int> > g(vec.size(), vector<int>(4)); // 后两维二进制压缩成一维
g[0] = {f[v][0], -inf, -inf, f[v][1]};
for (int i = 1; i < vec.size(); ++i) {
g[i][0] = f[vec[i]][0] + max(g[i - 1][0], g[i - 1][2]), g[i][2] = f[vec[i]][1] + g[i - 1][0];
g[i][1] = f[vec[i]][0] + max(g[i - 1][1], g[i - 1][3]), g[i][3] = f[vec[i]][1] + g[i - 1][1];
}
f[u][0] += *max_element(g.back().begin(), g.back().end()), f[u][1] += g.back()[0];
}
void Tarjan(int u, int father) {
fa[u] = father, dfn[u] = low[u] = ++dfstime;
f[u][0] = 0, f[u][1] = 1;
for (int v : G.e[u]) {
if (v == father)
continue;
if (!dfn[v])
Tarjan(v, u), low[u] = min(low[u], low[v]);
else
low[u] = min(low[u], dfn[v]);
if (low[v] > dfn[u])
f[u][0] += max(f[v][0], f[v][1]), f[u][1] += f[v][0];
}
for (int v : G.e[u])
if (v != father && dfn[v] > dfn[u] && fa[v] != u)
solve(u, 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);
}
Tarjan(1, 0);
printf("%d", max(f[1][0], f[1][1]));
return 0;
}
P5236 【模板】静态仙人掌
给出一张仙人掌,边带边权,\(q\) 次询问两点间的最短路。
\(n, q \le 10^4\)
类比树上最短路,考虑建出圆方树后在 LCA 处考虑。定义一对父子边 \((f, u)\) 的权值为:
- \(f, u\) 均为圆点:边权为 \(1\) 。
- \(f\) 为方点,\(u\) 为圆点:边权为环上 \(u\) 到 \(fa_f\) 的最短路。
- \(f\) 为圆点,\(u\) 为方点:边权为 \(0\) 。
考虑对 LCA 分类讨论:
- LCA 为圆点:此时最短距离即为圆方树上的距离。
- LCA 为方点:此时需要考虑在环上是否需要经过最浅点,只要记录环的长度以及每个点在环中的前缀边权和即可,其余部分距离即为树上距离。
时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e4 + 7, LOGN = 15;
struct Graph {
vector<pair<int, ll> > e[N];
inline void insert(int u, int v, ll w) {
e[u].emplace_back(v, w);
}
} G, T;
ll cirlen[N], prelen[N], dis[N];
int fa[N][LOGN], dfn[N], low[N], sta[N], falen[N], lowlen[N], dep[N];
int n, m, q, ext, dfstime, top;
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (!dfn[v]) {
falen[v] = w, Tarjan(v, u), low[u] = min(low[u], low[v]);
if (low[v] == dfn[u]) {
cirlen[++ext] = lowlen[sta[top]];
T.insert(u, ext, 0), T.insert(ext, u, 0);
vector<int> vec;
while (sta[top] != v)
vec.emplace_back(sta[top]), cirlen[ext] += falen[sta[top--]];
vec.emplace_back(sta[top]), cirlen[ext] += falen[sta[top--]];
vec.emplace_back(u), reverse(vec.begin(), vec.end());
for (int i = 1; i < vec.size(); ++i) {
int x = vec[i];
prelen[x] = prelen[vec[i - 1]] + falen[x];
ll w = min(prelen[x], cirlen[ext] - prelen[x]);
T.insert(x, ext, w), T.insert(ext, x, w);
}
}
} else if (dfn[v] < low[u])
low[u] = dfn[v], lowlen[u] = w;
}
}
void dfs(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 (auto it : T.e[u]) {
int v = it.first;
ll w = it.second;
if (v != f)
dis[v] = dis[u] + w, dfs(v, u);
}
}
inline int jump(int x, int h) {
while (h) {
int d = __lg(h);
x = fa[x][d], h ^= 1 << d;
}
return x;
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
x = jump(x, dep[x] - dep[y]);
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 ll query(int x, int y) {
int lca = LCA(x, y);
if (lca <= n)
return dis[x] + dis[y] - dis[lca] * 2;
int px = jump(x, dep[x] - dep[lca] - 1), py = jump(y, dep[y] - dep[lca] - 1);
return dis[x] - dis[px] + dis[y] - dis[py] +
min(abs(prelen[px] - prelen[py]), cirlen[lca] - abs(prelen[px] - prelen[py]));
}
signed main() {
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
G.insert(u, v, w), G.insert(v, u, w);
}
ext = n, Tarjan(1, 0), dfs(1, 0);
while (q--) {
int x, y;
scanf("%d%d", &x, &y);
printf("%lld\n", query(x, y));
}
return 0;
}
UOJ87. mx的仙人掌
给出一张仙人掌,边带边权,\(q\) 次询问,每次给出点集 \(S\) ,求 \(S\) 中任意两点最短路的最大值。
\(n, \sum |S| \le 3 \times 10^5\)
考虑建出仙人掌的圆方树,定义一对父子边 \((f, u)\) 的权值为:
- \(f, u\) 均为圆点:边权为 \(1\) 。
- \(f\) 为方点,\(u\) 为圆点:边权为环上 \(u\) 到 \(fa_f\) 的最短路。
- \(f\) 为圆点,\(u\) 为方点:边权为 \(0\) 。
每次将点集在圆方树上的虚树拿出来 DP,不难做到 \(O(\sum |S| \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 6e5 + 7, LOGN = 21;
struct Graph1 {
vector<pair<int, ll> > e[N];
inline void insert(int u, int v, ll w) {
e[u].emplace_back(v, w);
}
} G, T;
struct Graph2 {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} nT;
ll cirlen[N], prelen[N], dis[N], f[N];
int fa[N][LOGN], dfn[N], low[N], sta[N], falen[N], lowlen[N], dep[N];
ll ans;
int n, m, q, ext, dfstime, top;
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (!dfn[v]) {
falen[v] = w, Tarjan(v, u), low[u] = min(low[u], low[v]);
if (low[v] == dfn[u]) {
cirlen[++ext] = lowlen[sta[top]];
T.insert(u, ext, 0), T.insert(ext, u, 0);
vector<int> vec;
while (sta[top] != v)
vec.emplace_back(sta[top]), cirlen[ext] += falen[sta[top--]];
vec.emplace_back(sta[top]), cirlen[ext] += falen[sta[top--]];
vec.emplace_back(u), reverse(vec.begin(), vec.end());
for (int i = 1; i < vec.size(); ++i) {
int x = vec[i];
prelen[x] = prelen[vec[i - 1]] + falen[x];
ll w = min(prelen[x], cirlen[ext] - prelen[x]);
T.insert(x, ext, w), T.insert(ext, x, w);
}
}
} else if (dfn[v] < low[u])
low[u] = dfn[v], lowlen[u] = w;
}
}
void dfs1(int u, int f) {
fa[u][0] = f, dep[u] = dep[f] + 1, dfn[u] = ++dfstime;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (auto it : T.e[u]) {
int v = it.first;
ll w = it.second;
if (v != f)
dis[v] = dis[u] + w, dfs1(v, u);
}
}
inline int jump(int x, int h) {
while (h) {
int d = __lg(h);
x = fa[x][d], h ^= 1 << d;
}
return x;
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
x = jump(x, dep[x] - dep[y]);
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 build(vector<int> &kp) {
sort(kp.begin(), kp.end(), [](const int &a, const int &b) {
return dfn[a] < dfn[b];
});
kp.erase(unique(kp.begin(), kp.end()), kp.end());
int top = 0, r = sta[++top] = LCA(kp[0], kp.back());
nT.e[r].clear();
for (int x : kp) {
if (x == r)
continue;
int lca = LCA(sta[top], x);
if (lca != sta[top]) {
for (; dfn[lca] < dfn[sta[top - 1]]; --top)
nT.insert(sta[top - 1], sta[top]);
if (sta[top - 1] == lca)
nT.insert(lca, sta[top--]);
else
nT.e[lca].clear(), nT.insert(lca, sta[top]), sta[top] = lca;
}
nT.e[x].clear(), sta[++top] = x;
}
for (; top > 1; --top)
nT.insert(sta[top - 1], sta[top]);
return r;
}
void dfs2(int u) {
f[u] = 0;
for (int v : nT.e[u]) {
dfs2(v);
if (u <= n)
ans = max(ans, f[u] + f[v] + dis[v] - dis[u]);
f[u] = max(f[u], f[v] + dis[v] - dis[u]);
}
if (u > n) {
vector<pair<int, int> > vec;
for (int v : nT.e[u])
vec.emplace_back(v, jump(v, dep[v] - dep[u] - 1));
sort(vec.begin(), vec.end(), [](const pair<int, int> &a, const pair<int, int> &b) {
return prelen[a.second] < prelen[b.second];
});
int len = vec.size();
vector<ll> val(len), pre(len);
for (int i = 0; i < len; ++i) {
vec.emplace_back(vec[i]), pre.emplace_back((pre[i] = prelen[vec[i].second]) + cirlen[u]);
val.emplace_back(val[i] = f[vec[i].first] + dis[vec[i].first] - dis[vec[i].second]);
}
deque<int> q;
for (int i = 0; i < len * 2; ++i) {
while (!q.empty() && pre[i] - pre[q.front()] > cirlen[u] / 2)
q.pop_front();
if (!q.empty())
ans = max(ans, val[i] + val[q.front()] + pre[i] - pre[q.front()]);
while (!q.empty() && val[i] - pre[i] > val[q.back()] - pre[q.back()])
q.pop_back();
q.emplace_back(i);
}
}
}
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), G.insert(v, u, w);
}
for (int i = 1; i <= n; ++i)
sort(G.e[i].begin(), G.e[i].end(), [](const pair<int, int> &a, const pair<int, int> &b) {
return a.second < b.second;
});
ext = n, Tarjan(1, 0);
dfstime = 0, dfs1(1, 0);
scanf("%d", &q);
while (q--) {
int k;
scanf("%d", &k);
vector<int> vec(k);
for (int &it : vec)
scanf("%d", &it);
ans = 0, dfs2(build(vec));
printf("%lld\n", ans);
}
return 0;
}
P3687 [ZJOI2017] 仙人掌
给出一张无向连通图,求有多少种加边的方案满足得到的图为简单仙人掌。
\(n \le 5 \times 10^5\) ,\(m \le 10^6\)
首先,如果原图不是仙人掌,答案为 \(0\) 。这个可以树上差分维护每条树边的覆盖次数,若 \(\ge 2\) 则不是仙人掌。
若原图是仙人掌,则显然不能在环上加边。因此可以去掉原图中的环,对森林中的每棵树分别求解,最后用乘法原理合并。下面讨论图是一棵树的情况。
考虑合法的加边条件,一条树边只能被一条非树边覆盖。若钦定可以有重边,则条件可以转化为一条树边必须被一条非树边覆盖。不难发现可以构造有重边和没有重边的双射,考虑对后者计数。
设 \(f_u\) 表示 \(u\) 子树以及 \(u\) 到父亲的边的覆盖方案数。对于 \(u\) 的所有出边,每条边要么和其他边匹配(连成一条链),要么不匹配(在 \(u\) 处结束)。
因此再设 \(g_i\) 表示 \(i\) 条出边的匹配方案数,则第 \(i\) 条边要么和前面的某条边匹配,要么不匹配,因此 \(g_i = g_{i - 1} + (i - 1) g_{i - 2}\) ,从而得到 \(f_u = g_{\mathrm{deg}(u)} \prod_{v \in son(u)} f_v\) 。
时间复杂度 \(O(n + m)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 5e5 + 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, nT;
int dfn[N], low[N], c[N], fa[N], f[N], g[N];
bool vis[N];
int n, m, dfstime, tot;
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 v : G.e[u]) {
if (!dfn[v])
fa[v] = u, T.insert(u, v), Tarjan(v, u), low[u] = min(low[u], low[v]);
else if (v != f) {
low[u] = min(low[u], dfn[v]);
if (dfn[v] < dfn[u])
++c[u], --c[v];
}
}
}
bool dfs1(int u) {
bool flag = true;
for (int v : T.e[u])
flag &= dfs1(v), c[u] += c[v];
return flag && c[u] <= 1;
}
int dfs2(int u) {
vis[u] = true, f[u] = g[nT.e[u].size()];
for (int v : nT.e[u])
if (!vis[v])
f[u] = 1ll * f[u] * dfs2(v) % Mod;
return f[u];
}
signed main() {
g[0] = g[1] = 1;
for (int i = 2; i < N; ++i)
g[i] = add(g[i - 1], 1ll * (i - 1) * g[i - 2] % Mod);
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), memset(c + 1, 0, sizeof(int) * n);
dfstime = 0, T.clear(n), Tarjan(1, 0);
if (!dfs1(1)) {
puts("0");
continue;
}
nT.clear(n);
for (int i = 2; i <= n; ++i)
if (!c[i])
nT.insert(fa[i], i), nT.insert(i, fa[i]);
memset(vis + 1, false, sizeof(bool) * n);
int ans = 1;
for (int i = 2; i <= n; ++i)
if (!vis[i])
ans = 1ll * ans * dfs2(i) % Mod;
printf("%d\n", ans);
}
return 0;
}
弦图
定义:
- 弦:连接环中不相邻两点的边。
- 弦图:任意长度 \(> 3\) 的环都有一个弦的图。
- 点割集:对于图 \(G\) 上的两点 \(u, v\) ,定义点割集为满足删去这一集合后 \(u, v\) 不连通的点集。若其不存在点割子集,则其为极小点割集。
- 单纯点:满足 \(\{ x \} \cup N(x)\) 的导出子图是一个团的点 \(x\) 。
- 完美消除序列:满足 \(v_i\) 在 \(\{ v_i, v_{i + 1}, \cdots, v_n \}\) 的导出子图中为单纯点的排列 \(v_{1 \sim n}\) 。
性质:
-
团数 \(\omega(G) \le \chi(G)\) 色数。
证明:最大团的导出子图色数至少为 \(\omega(G)\) ,而原图色数不小于它。
-
最大独立集数 \(\alpha(G) \le \kappa(G)\) 最小团覆盖数。
证明:每个团至多只有一个点在最大独立集中。
-
弦图的任意导出子图一定是弦图。
证明:如果弦图有导出子图不是弦图,则导出子图上存在 \(> 3\) 的无弦环,因而原图不是弦图,矛盾。
-
图 \(G\) 关于 \(u, v\) 的极小点割集 \(S\) 将原图分成了若干个连通块,设包含 \(u\) 的连通块为 \(V_u\) ,包含 \(v\) 的连通块为 \(V_v\) ,则对于任意 \(x \in S\) ,\(N(x)\) 一定同时包含 \(V_u, V_v\) 中的点。
证明:若 \(N(x)\) 不同时包含 \(V_u, V_v\) 中的点,则删去 \(x\) 后可以得到更小的点割集。
-
弦图上任意两点间的极小点割集的导出子图一定为一个团。
证明:极小点割集大小 \(\le 1\) 时显然成立,否则设 \(x, y\) 为极小点割集中的点,\(x_u, x_v\) 为 \(V_u, V_v\) 中满足 \(x_u, x_v \in N(x)\) 的点,\(y_u, y_v\) 为 \(V_u, V_v\) 中满足 \(y_u, y_v \in N(y)\) 的点。
再设 \(x, y\) 在 \(V_u, V_v\) 中的最短路为 \(x \to x_u \rightsquigarrow y_u \to y\) 和 \(x \to x_v \rightsquigarrow y_v \to y\) ,此时这两条最短路能拼成一个点数 \(\ge 4\) 的环,由定义该环上一定存在一条弦。
- 若这条弦连接了 \(V_u, V_v\) 两个连通块,则点集不是点割集,矛盾。
- 若这条弦连接了单个连通块内部的两个点或一个连通块内部的一个点和一个点割集上的点,都不满足最短路的性质,矛盾。
因此这条弦只能连接 \(x, y\) 两点,因此弦图中每个极小点割集中的两点都有边直接相连。
-
任何一个弦图都至少有一个单纯点,不是完全图的弦图至少有两个不相邻的单纯点。
证明:考虑数学归纳法,首先有:
- 当图与完全图同构时,图上任意一点都是单纯点。
- 当图的点数 \(\le 3\) 时,结论显然成立。
若图的点数 \(\ge 4\) 且图不为完全图,可知必然存在 \(u, v\) 满足 \((u, v) \notin E\) 。设 \(I\) 是 \(u, v\) 的极小点割集,\(V_u, V_v\) 为删去 \(I\) 后 \(u, v\) 所在的连通块。由对称性只需考虑 \(u\) 的一侧即可,设 \(L = I \cup V_u\) 。
若 \(L\) 为完全图,则 \(u\) 为单纯点,否则由于 \(L\) 是原图的导出子图,因此 \(L\) 是弦图,因而存在两个不相邻的单纯点。而 \(I\) 是一个团,因此 \(V_u\) 中一定存在单纯点,该单纯点扩展到全图也为单纯点。
由于每次将整个图分成若干个连通块证明,大小一定减小,且都满足性质,故归纳成立。
-
一个无向图是弦图当且仅当其存在完美消除序列。
证明:
充分性:点数为 \(1\) 的弦图有完全消除序列,而点数 \(n > 1\) 的弦图的完美消除序列可以有点数 \(n - 1\) 的弦图的完美消除序列加上一个单纯点得到。
必要性:假设有无向图存在点数 \(> 3\) 的环且存在完美消除序列,设在完美消除序列中出现的第一个环上的点为 \(v\) ,设 \(v\) 在环上与 \(v_1, v_2\) 相连,则由单纯点的定义得到 \(v_1, v_2\) 有边相连,矛盾。
MCS 算法判定
MCS 算法(最大势算法)能够在 \(O(n + m)\) 的时间复杂度内求出无向图的完美消除序列。
逆序内点编号,即按 \(n \to 1\) 的顺序给点标号。
设 \(label_x\) 表示第 \(x\) 个点与多少个已经标号的点相邻,每次选择 \(label\) 最大的未标号点进行标号。
用 vector
对每个 \(i\) 维护 \(label_x = i\) 的点 \(x\) ,每条边对 \(\sum_{i = 1}^n label_i\) 的贡献最多为 \(2\) ,时间复杂度 \(O(n + m)\) 。
如果此时原图是弦图,此时求出的就是完美消除序列,否则此时求出的一定不是完美消除序列,所以还需要判断求出的序列是否是原图的完美消除序列。
根据完美消除序列的定义,设 \(v_i\) 在 \(v_{i \sim n}\)中相邻的点按在序列中的位置从小到大为 \(v_{c_1 \sim c_k}\) ,则只要判断 \(v_{c_1}\) 与其他点是否直接相连即可。正确性可以归纳证明,时间复杂度 \(O(n + m)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 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;
vector<int> vec[N];
int label[N], id[N];
bool vis[N], mark[N];
int n, m;
inline vector<int> MCS() {
vector<int> seq;
for (int i = 0; i <= n; ++i)
vec[i].clear();
for (int i = 1; i <= n; ++i)
vec[0].emplace_back(i);
memset(label + 1, 0, sizeof(int) * n);
memset(id + 1, -1, sizeof(int) * n);
for (int i = 1, p = 0; i <= n; ++i) {
for (;;) {
while (!vec[p].empty() && ~id[vec[p].back()])
vec[p].pop_back();
if (vec[p].empty())
--p;
else {
seq.emplace_back(vec[p].back()), id[vec[p].back()] = n - seq.size();
break;
}
}
for (int x : G.e[seq.back()])
if (id[x] == -1)
vec[++label[x]].emplace_back(x), p = max(p, label[x]);
}
reverse(seq.begin(), seq.end());
return seq;
}
inline bool check(vector<int> seq) {
memset(vis + 1, false, sizeof(bool) * n);
for (int it : seq) {
vis[it] = true;
int u = 0, tot = 0;
for (int x : G.e[it])
if (!vis[x]) {
mark[x] = true, ++tot;
if (!u || id[x] < id[u])
u = x;
}
if (!tot)
continue;
int sum = 1;
for (int x : G.e[u])
if (mark[x] && x != u)
++sum;
for (int x : G.e[it])
if (!vis[x])
mark[x] = false;
if (sum != tot)
return false;
}
return true;
}
signed main() {
while (~scanf("%d%d", &n, &m)) {
if (!n && !m)
break;
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);
}
vector<int> seq = MCS();
puts(check(seq) ? "Perfect" : "Imperfect"), puts("");
}
return 0;
}
极大团
记 \(N(x)\) 表示 \(x\) 邻域中在完美消除序列上排 \(x\) 后面的点,则弦图的极大团即为:
色数与团数
按完美消除序列从后往前依次给每个点染色,给每个点染上可以染的最小颜色,时间复杂度 \(O(n + m)\) 。
正确性:设该方法用了 \(t\) 中颜色,则 \(t \ge \chi(G)\) 。而团上每个点都是不同的颜色,所以 \(t = \omega(G)\) 。又 \(\omega(G) \le \chi(G)\) ,因此 \(t = \omega(G) = \chi(G)\) 。
无需构造方案时,答案即为 \(\max_x |\{ x \} \cup N(x)|\) 。
最大独立集与最小团覆盖
最大独立集:完美消除序列从前往后贪心选点。
最小团覆盖:独立集中的每个点 \(x\) 的 \(\{ x \} \cup N(x)\) 组成一组最小团覆盖。
正确性:设该方法求出的数量为 \(t\) ,则 \(\kappa(G) \le t \le \alpha(G)\) ,而 \(\alpha(G) \le \kappa(G)\) ,因此 \(t = \alpha(G) = \kappa(G)\) 。