连通性相关
连通性相关
强连通分量
强连通分量(SCC):极大的强连通子图。
Tarjan 算法
维护一个栈存储搜索到的还未确定强连通分量的点,定义:
- \(dfn_u\) :节点 \(u\) 被搜索的次序。
- \(low_u\) :\(u\) 子树中能回溯到的最小的 \(dfn\) 。
不难得到:
- 一个点子树内的 \(dfn\) 大于该点的 \(dfn\) 。
- 从根开始的路径上的 \(dfn\) 递增,\(low\) 非降。
对于 \(u\) 的出点 \(v\) ,考虑
-
\(v\) 未被访问过:继续 dfs ,并用 \(low_v\) 更新 \(low_u\) 。因为存在 \(u \to v\) ,所以 \(v\) 可以直接回溯到已在栈中的点 \(u\) 一定可以回溯到。
-
\(v\) 被访问过
-
已在栈中:根据 \(low\) 的定义,用 \(dfn_v\) 更新 \(low_u\) 。
-
不在栈中:说明 \(v\) 已搜索完毕,其所在的连通分量已被处理,不用管它。
-
对于一个强连通分量,不难发现只有一个 \(u\) 满足 \(dfn_u = low_u\) ,其一定是这个强连通分量的根。
因此回溯过程中,若 \(dfn_u = low_u\) ,则新增一个强连通分量。
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u]) {
++scc;
while (sta[top] != u)
leader[sta[top--]] = scc;
leader[sta[top--]] = scc;
}
}
应用
对于一张有向图,其可能存在环。可以将每个强连通分量分别缩成一个点,这个图就会变成一张 DAG,可能会便于处理。
Kosaraju 算法
- 第一次 dfs:遍历所有点并在回溯时入栈。
- 第二次 dfs:在反图上依次从栈顶开始 dfs ,此时遍历到的点集就是一个强连通分量。
时间复杂度 \(O(n + m)\) 。
void dfs1(int u) {
vis[u] = true;
for (int v : G.e[u])
if (!vis[v])
dfs1(v);
sta.emplace(u);
}
void dfs2(int u) {
leader[u] = scc;
for (int v : rG.e[u])
if (!leader[v])
dfs2(v);
}
inline void kosaraju() {
for (int i = 1; i <= n; ++i)
if (!vis[i])
dfs1(i);
for (; !sta.empty(); sta.pop())
if (!leader[sta.top()])
++scc, dfs(sta.top());
}
用 bitset
优化可以做到 \(O(\dfrac{n^2}{\omega})\) 的复杂度,某些题目有奇效。
void dfs1(int u) {
vis.set(u);
bitset<N> now = ~vis & e1[u];
while (now.any())
dfs1(now._Find_first()), now &= ~vis;
sta.push(u);
}
void dfs2(int u) {
vis.set(u), leader[u] = scc;
bitset<N> now = ~vis & e2[u];
while (now.any())
dfs2(now._Find_first()), now &= ~vis;
}
inline void kosaraju() {
vis.reset();
for (int i = 1; i <= n; ++i)
if (!vis.test(i))
dfs1(i);
vis.reset();
for (; !sta.empty(); sta.pop())
if (!vis.test(sta.top()))
++scc, dfs(sta.top());
}
应用
BZOJ5218 省队十连测 友好城市
给出一张有向图,\(q\) 次询问仅保留编号属于 \([l_i, r_i]\) 的边时有多少无序对城市满足可以两两到达。
\(n \leq 150, m \leq 3 \times 10^5, q \leq 5 \times 10^4\)
注意到 \(n\) 很小,使用 kosaraju 配合莫队即可,时间复杂度 \(O(\dfrac {(\sqrt{m} \log m + q) n^2}{\omega} + q \sqrt m)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1.5e2 + 7, M = 3e5 + 7, Q = 5e4 + 7;
struct Edge {
int u, v;
} E[M];
struct Query {
int l, r, *ans, bid;
inline bool operator < (const Query &rhs) const {
return bid == rhs.bid ? (bid & 1 ? r < rhs.r : r > rhs.r) : bid < rhs.bid;
}
} qry[Q];
bitset<N> e1[N], e2[N];
bitset<N> vis;
stack<int> sta;
int cnt1[N][N], cnt2[N][N], ans[Q];
int n, m, q, block, scc;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = c == '-';
while (c < '0' || c > '9')
c = getchar(), sign |= c == '-';
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline void Add(int x) {
int u = E[x].u, v = E[x].v;
if (!cnt1[u][v])
e1[u][v] = true;
if (!cnt2[v][u])
e2[v][u] = true;
++cnt1[u][v], ++cnt2[v][u];
}
inline void Del(int x) {
int u = E[x].u, v = E[x].v;
--cnt1[u][v], --cnt2[v][u];
if (!cnt1[u][v])
e1[u][v] = false;
if (!cnt2[v][u])
e2[v][u] = false;
}
void dfs1(int u) {
vis.set(u);
bitset<N> now = ~vis & e1[u];
while (now.any())
dfs1(now._Find_first()), now &= ~vis;
sta.push(u);
}
int dfs2(int u) {
vis.set(u);
bitset<N> now = ~vis & e2[u];
int siz = 1;
while (now.any())
siz += dfs2(now._Find_first()), now &= ~vis;
return siz;
}
inline int kosaraju() {
vis.reset(), scc = 0;
int res = 0;
for (int i = 1; i <= n; ++i)
if (!vis.test(i))
dfs1(i);
vis.reset();
for (; !sta.empty(); sta.pop())
if (!vis.test(sta.top())) {
int siz = dfs2(sta.top());
res += siz * (siz - 1) / 2;
}
return res;
}
signed main() {
n = read(), m = read(), q = read();
block = sqrt(m);
for (int i = 1; i <= m; ++i)
E[i].u = read(), E[i].v = read();
for (int i = 1; i <= q; ++i)
qry[i].l = read(), qry[i].r = read(), qry[i].bid = qry[i].l / block, qry[i].ans = ans + i;
sort(qry + 1, qry + 1 + q);
for (int i = 1, l = 1, r = 0; i <= q; ++i) {
while (l > qry[i].l)
Add(--l);
while (r < qry[i].r)
Add(++r);
while (l < qry[i].l)
Del(l++);
while (r > qry[i].r)
Del(r--);
*qry[i].ans = kosaraju();
}
for (int i = 1; i <= q; ++i)
printf("%d\n", ans[i]);
return 0;
}
应用
给定一张有向图,每个点有 \(k_i\) 号钥匙,保证 \(k\) 为排列。能进入一个点当且仅当你有该点的钥匙。
对每个点 \(i\) ,求拿着 \(i\) 号点的钥匙从 \(i\) 出发可达多少点,以及有多少点可达且能够返回 \(i\) 。
\(\sum n \leq 1.5 \times 10^6, \sum m \leq 3 \times 10^6\)
首先在任意时刻,接下来新进入的点是固定的,且该点的钥匙在上一个新进入的点内。因此只要依次考虑 \(k_i, k_{k_i}, \cdots\) 是否可达。
对于当前点 \(j\) 以及从 \(i\) 不断跳 \(k_i\) 到 \(j\) 的序列 \(a = \{i, k_i, k_{k_i}, \cdots, j \}\) ,考虑 \(k_j\) 可达的充要条件:
- \(k_j \not \in a\) ,即 \(k_j \neq i\),因为 \(k\) 形成排列。
- 对于最大的 \(p\) 使得 \(a_p\to k_j\),\(a_p\) 存在且 \(j\) 可以回到 \(a_p\) 。
因为 \(a_i\) 可达 \(a_{i + 1}\),所以每次新进入点 \(j\) 时一条返祖边覆盖的点强连通。枚举 \(k_j\) 的入边 \(u \to k_j\) ,若 \(u \in a\) 且 \(u\) 和 \(j\) 强连通,则 \(k_j\) 可达。用并查集维护 SCC 的合并,于是得到 \(O(n^2 \alpha(n))\) 的做法。
因为 \(k\) 形成排列,考虑将 \(k\) 上的环单独考虑。首先断环成链,将环复制两份得到序列 \(c\) 。令 \(k_i\) 为 \(i\) 的后继,则每个点的前一份复制品在链上的答案就是它在环上的答案。
因为后面的点不会到达前面的点,所以我们从后往前加入计算每个点的答案。不难发现可达性具有传递性,于是可以用若干条链描述。
设 \(c_{i + 1} \sim c_L\) 的答案已知,计算 \(c_i\) 的答案,则整个过程相当于不断尝试合并两条链。设第一条链的末尾为 \(c_p\) ,第二条链的开头为 \(c_{p + 1}\) ,根据朴素做法中的判定条件,考虑 \(c_{p + 1}\) 的最靠近 \(c_p\) 且落在 \(c_i \sim c_p\) 上的入边 \(c_u \to c_{p + 1}\)( \(i \leq u \leq p\) 且 \(u\) 最大),若 \(c_u\) 存在且和 \(c_p\) 强连通,则可以合并,否则无法合并。
用两个并查集分别维护可达链和强连通分量,预处理每个点在链上最靠近它且在它前方的入点的位置即可。
接下来考虑合并两条可达链时强连通分量的合并。虽然产生贡献的返祖边数量总数只有 \(O(m)\),但若直接枚举第二条链上的所有返祖边就无法跳过无贡献的返祖边。一个解决方案是对每个可达链维护所有终点编号不小于 \(i\) 且未考虑过的返祖边。合并两条链时,统计第二条链维护的所有返祖边的影响,并将它们全部删除。此时新链的返祖边集合就等于第一条链的返祖边集合,不需要启发式合并。
于是得到 \(O(n \alpha(n))\) 的做法。
注意到如果两条链可以合并,那么第一条链的开头,即当前的 \(c_i\) 一定产生了贡献,否则在加入 \(c_i\) 之前这两条链就已经可以合并了。而如果 \(c_i\) 要产生贡献,一定将第一条链的末尾 \(c_p\) 所在的强连通分量扩大了,这说明 \(c_i\) 和 \(c_p\) 在同一强连通分量,即第一条链整体是一个强连通分量。这样,对于每条链,只需维护编号最大的有还未统计过的返祖边的节点,因为这些返祖边具体指向哪些点是不重要的,反正它们在同一个强连通分量,而每个起始点都相当于将 \(c_i\) 到该起始点的强连通分量全部合并,所以我们只关心编号最大的起始点。
此外,有了该性质,在判定是否可以合并两条链时,也可以维护每个点 \(c_{p + 1}\) 是否被之前 \(c_i \sim c_p\) 的某个点指向,代替计算每个点的最近前驱的位置,但这样就要存正向边了。
#include <bits/stdc++.h>
using namespace std;
const int N = 3e6 + 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);
}
} rG;
struct DSU {
int fa[N];
inline void prework(int n) {
iota(fa + 1, fa + 1 + n, 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) {
x = find(x), y = find(y);
if (x != y)
fa[y] = x;
}
} dsu1, dsu2;
pair<int, int> ans[N];
int k[N], cyc[N], idx[N], mxid[N], pre[N];
// idx : 结点在环上的编号
// mxid : 结构编号最大的有返祖边的分量
// pre : 结点的最近前驱入边位置
bool vis[N];
int n, m, tot;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
signed main() {
int T = read();
while (T--) {
n = read(), m = read();
for (int i = 1; i <= n; ++i)
k[i] = read();
rG.clear(n);
for (int i = 1; i <= m; ++i) {
int u = read(), v = read();
rG.insert(v, u);
}
memset(vis + 1, false, sizeof(bool) * n);
for (int i = 1; i <= n; ++i) {
if (vis[i])
continue;
int cur = i;
while (!idx[cur])
cyc[idx[cur] = ++tot] = cur, cur = k[cur];
dsu2.prework(tot * 2), dsu1.prework(tot * 2);
memset(mxid + 1, 0, sizeof(int) * tot * 2);
memset(pre + 1, 0, sizeof(int) * tot * 2);
for (int j = tot * 2; j; --j) {
int u = cyc[j <= tot ? j : j - tot];
for (int v : rG.e[u]) {
if (!idx[v])
continue;
v = idx[v];
if (v + tot < j)
v += tot;
else if (v > j)
v -= tot;
pre[j] = max(pre[j], v);
if (v < j)
v += tot;
if (v <= tot * 2)
mxid[dsu1.find(v)] = max(mxid[dsu1.find(v)], dsu2.find(v));
}
for (;;) {
for (;;) {
int id1 = dsu1.find(j), id2 = dsu2.find(j);
if (id2 < mxid[id1])
dsu2.merge(id2 + 1, id2);
else
break;
}
int id1 = dsu1.find(j), id2 = dsu2.find(j);
mxid[id1] = 0;
if (id1 == tot * 2 || id2 != id1 || pre[id1 + 1] < j)
break;
dsu1.merge(id1 + 1, id1);
}
ans[u] = make_pair(min(tot, dsu1.find(j) - j + 1), min(tot, dsu2.find(j) - j + 1));
}
for (int j = 1; j <= tot; ++j)
idx[cyc[j]] = 0, vis[cyc[j]] = true;
tot = 0;
}
for (int i = 1; i <= n; ++i)
printf("%d %d\n", ans[i].first, ans[i].second);
}
return 0;
}
给定一张带边权的有向图,\(q\) 次询问,每次给定 \(v, s, t\) ,询问时候存在一条经过 \(v\) 的回路满足长度与 \(-s\) 在模 \(t\) 意义下同余。
\(n, m, q \leq 2 \times 10^5\) 。
首先不难发现每个 SCC 的答案是一致的,且不同 SCC 之间相互独立,故考虑对于每个 SCC 分开计算。
假设经过 \(u\) 有两个长度为 \(a\) 和 \(b\) 的环,那么就相当于找两个非负整数 \(x\) 和 \(y\),使得 \(ax + by = w\),其中 \(w\) 为题中的路径长,根据裴蜀定理得到上述方程成立当且仅当 \(\gcd(a, b) \mid w\) 。
考虑如何求出经过点 \(u\) 的所有环长度的 \(\gcd\) 。通过分析发现,所有的非树边 \(u \to v\) 对答案的贡献都是 \(dis_u + w - dis_v\) 。于是搜索时顺便记录贡献即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(const int u, const int v, const int w) {
e[u].emplace_back(v, w);
}
} G;
ll dis[N], g[N];
int dfn[N], low[N], sta[N], leader[N];
bool vis[N];
int n, m, q, dfstime, top, scc;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline ll gcd(ll a, ll b) {
if (!a || !b)
return a | b;
while (a ^= b ^= a ^= b %= a);
return b;
}
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (auto it : G.e[u]) {
int v = it.first;
if (!dfn[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++scc;
while (sta[top] != u)
leader[sta[top--]] = scc;
leader[sta[top--]] = scc;
}
}
void dfs(int u, int cur) {
vis[u] = true;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (leader[v] != cur)
continue;
if (!vis[v])
dis[v] = dis[u] + w, dfs(v, cur);
else
g[cur] = gcd(g[cur], abs(dis[u] - dis[v] + w));
}
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= m; ++i) {
int u = read(), v = read(), w = read();
G.insert(u, v, w);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
Tarjan(i);
for (int i = 1; i <= n; ++i)
if (!vis[i])
dfs(i, leader[i]);
q = read();
while (q--) {
int x = read(), s = read(), t = read();
if (g[leader[x]])
puts(s % gcd(g[leader[x]], t) ? "NO" : "YES");
else
puts(s ? "NO" : "YES");
}
return 0;
}
给定一张有向强连通图。一个点是好的当且仅当它到其它点有且只有一条简单路径。如果好的点至少有 \(20\%\) ,则输出所有好的点, 否则输出 \(-1\) 。
\(\sum n \leq 10^5, \sum m \leq 2 \times 10^5\)
考虑如何判定 \(u\) 是好的。只要以 \(u\) 为根建出 dfs 树,若无横叉边或前向边则 \(u\) 即为好的。于是可以做到 \(O(n)\) 的判定。
接下来考虑确定一个好节点 \(u\) 后求出其余好节点。以 \(u\) 为根建立 dfs 树,考虑某个 \(v\) 的子树,由于整个图的强连通性,\(v\) 的子树中有连向其祖先的返祖边。不难发现这样的边有且仅有一条,否则 \(v\) 有两条路径可以到 \(fa_v\) 。
那么先把所有子树 \(v\) 内返祖到根的祖先的边的数量记录下来,如果数量 \(\geq 2\) 则 \(v\) 不为好点,否则就顺便记录每个 \(v\) 子树的那条返祖边指向的点。
假设 \(v\) 的子树这条返祖边指向了 \(w\),那么 \(v\) 是好点,当且仅当 \(w\) 是好点。
于是一个点 \(v\) 是好点,当且仅当 \(v\) 的子树内有且仅有一条连向 \(v\) 的祖先的返祖边,并且这条边所连向的点是好点。
第一个条件可以考虑所有返祖边 \((a,b)\),它对哪些 \(v\) 的子树内返向 \(v\) 祖先的边的数量的有贡献。显然这样的 \(v\) 分布在 \(a\to fa_a\to...\to son_b\) 上,这里的 \(son_b\) 为 \(b\) 的儿子节点中靠近 \(a\) 侧的那个,树上差分即可和第二个条件一起解决。
于是可以在已知一个好点的情况下线性求出所有好点。
考虑随机选取一定数量的点进行上述算法流程,若均不满足条件则输出 \(-1\) 。注意随机取点不能取重。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 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;
int cnt[N], pre[N], dep[N];
bool vis[N], in[N], ans[N];
mt19937 myrand(time(0));
int n, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
bool dfs(int u) {
vis[u] = in[u] = true;
for (int v : G.e[u]) {
if (vis[v] && !in[v])
return false;
else if (!in[v]) {
dep[v] = dep[u] + 1, T.insert(u, v);
if (!dfs(v))
return false;
} else {
++cnt[u], --cnt[v];
if (dep[v] < dep[pre[u]])
pre[u] = v;
}
}
in[u] = 0;
return true;
}
inline bool check(int u) {
memset(cnt + 1, 0, sizeof(int) * n);
memset(pre + 1, 0, sizeof(int) * n);
memset(vis + 1, false, sizeof(bool) * n);
memset(in + 1, false, sizeof(bool) * n);
T.clear(n), dep[u] = 1, dep[0] = n + 1;
return dfs(u);
}
inline void calc(int u) {
for (int v : T.e[u]) {
calc(v), cnt[u] += cnt[v];
if (dep[pre[v]] < dep[pre[u]])
pre[u] = pre[v];
}
}
void solve(int u) {
if(cnt[u] == 1 && dep[pre[u]] < dep[u] && ans[pre[u]])
ans[u] = true;
for (int v : T.e[u])
solve(v);
}
signed main() {
int T = read();
while (T--) {
n = read(), m = read();
G.clear(n);
for (int i = 1; i <= m; ++i) {
int u = read(), v = read();
G.insert(u, v);
}
vector<int> id(n);
iota(id.begin(), id.end(), 1);
shuffle(id.begin(), id.end(), myrand);
int root = -1;
for (int i = 0; i < min(n, 100); ++i)
if (check(id[i])) {
root = id[i];
break;
}
if (root == -1) {
puts("-1");
continue;
}
calc(root);
memset(ans + 1, false, sizeof(bool) * n);
ans[root] = true, solve(root);
if (count(ans + 1, ans + 1 + n, true) * 5 < n)
puts("-1");
else {
for (int i = 1; i <= n; ++i)
if (ans[i])
printf("%d ", i);
puts("");
}
}
return 0;
}
2-SAT
2-SAT 问题:给定一串布尔变量,每个变量只能为真或假。要求对这些变量进行赋值,满足布尔方程。
实现
构造状态
点的状态:将点 \(u\) 拆分成 \(u0,u1\) 两个点,分别表示 \(u\) 点为假、真。
边的状态:若连的边为 \(u \to v\) ,就表示选择 \(u\) 就必须选 \(v\) 。
判断有无解
由所构造的状态可知,对于图中的每一个强连通分量,如果选择了其中任意一个点,那就意味着这个强连通分量中的所有点都要选。显然 \(x0,x1\) 不可以同时选,由此可判断有无解。
方案输出
由连边的方式可以得知,我们对于每个点的两种状态,选择拓扑序大的,舍弃掉另一个。
注意到用 Tarjan 求得的强连通分量编号就是反拓扑序,于是选择强连通分量编号较小的点即可。
如果要求字典序最小,就深搜枚举点 \(1 \to 2n\) ,贪心选取。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 7;
vector<int> e[N];
stack<int> sta;
int dfn[N], low[N], leader[N];
int n, m, dfstime, cnt;
inline void AddEdge(int u, int v) {
e[u].push_back(v);
}
inline void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime;
sta.push(u);
for (int i = 0, v; i < e[u].size(); ++i) {
v = e[u][i];
if (!dfn[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u]) {
for (++cnt; sta.top() != u; sta.pop())
leader[sta.top()] = cnt;
leader[sta.top()] = cnt;
sta.pop();
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1, a, x, b, y; i <= m; ++i) {
scanf("%d%d%d%d", &a, &x, &b, &y);
if (!x && !y)
AddEdge(a + n, b), AddEdge(b + n, a);
else if (x && y)
AddEdge(a, b + n), AddEdge(b, a + n);
else if (!x && y)
AddEdge(b, a), AddEdge(a + n, b + n);
else if (x && !y)
AddEdge(a, b), AddEdge(b + n, a + n); // a 表示 a0, a + n 表示 a1
}
for (int i = 1; i <= (n << 1); ++i)
if (!dfn[i])
Tarjan(i);
for (int i = 1; i <= n; ++i)
if (leader[i] == leader[i + n]) // 若 a0 和 a1 必须同时选, 就无解
return puts("IMPOSSIBLE"), 0;
puts("POSSIBLE");
for (int i = 1; i <= n; ++i)
printf("%d ", leader[i] > leader[i + n]); // 输出一组解
return 0;
}
应用
给定一串序列,有 \(d\) 个 \(x\) 位置有三种选择,其他位置有两种选择,求解一种合法方案。
\(n \leq 5 \times 10^4, d \leq 8\)
暴力枚举每个 \(x\) 地图不填 \(A\) 或不填 \(B\) 。因为不填 \(A\) 就可以填 \(B, C\) ,不填 \(B\) 就可以填 \(A, C\) ,这样就包含了 \(A, B, C\) 三种赛车。
时间复杂度降为 \(O((n+m) \times 2^d)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 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(const int u, const int v) {
e[u].emplace_back(v);
}
} G;
struct Node {
int x, y;
char cx, cy;
} nd[N];
int dfn[N], low[N], leader[N], sta[N];
char str[N];
int n, d, m, dfstime, top, scc;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline char readc() {
char c = getchar();
while (c < 'A' || c > 'Z')
c = getchar();
return c;
}
inline int trans(int x) {
return x <= n ? x + n : x - n;
}
inline int getid(int x, char op) {
if (str[x] == 'a')
return op == 'b' ? x : x + n;
else
return op == 'a' ? x : x + n;
}
inline void clear(int n) {
G.clear(n);
memset(dfn + 1, 0, sizeof(int) * n);
memset(low + 1, 0, sizeof(int) * n);
memset(leader + 1, 0, sizeof(int) * n);
dfstime = scc = 0;
}
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : G.e[u]) {
if (!dfn[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u]) {
++scc;
while (sta[top] != u)
leader[sta[top--]] = scc;
leader[sta[top--]] = scc;
}
}
inline bool solve() {
clear(n * 2);
for (int i = 1; i <= m; ++i) {
if (str[nd[i].x] == nd[i].cx)
continue;
int x = getid(nd[i].x, nd[i].cx), y = getid(nd[i].y, nd[i].cy);
if (str[nd[i].y] == nd[i].cy)
G.insert(x, trans(x));
else
G.insert(x, y), G.insert(trans(y), trans(x));
}
for (int i = 1; i <= n * 2; ++i)
if (!dfn[i])
Tarjan(i);
for (int i = 1; i <= n; ++i)
if (leader[i] == leader[i + n])
return false;
return true;
}
bool dfs(int pos) {
if (pos > n)
return solve();
else if (str[pos] != 'x')
return dfs(pos + 1);
for (int i = 0; i < 2; ++i) {
str[pos] = 'a' + i;
if (dfs(pos + 1))
return true;
}
return str[pos] = 'x', false;
}
signed main() {
n = read(), d = read();
scanf("%s", str + 1);
m = read();
for (int i = 1; i <= m; ++i) {
nd[i].x = read(), nd[i].cx = tolower(readc());
nd[i].y = read(), nd[i].cy = tolower(readc());
}
if (!dfs(1))
return puts("-1"), 0;
for (int i = 1; i <= n; ++i)
if (str[i] == 'a')
putchar(leader[i] < leader[i + n] ? 'B' : 'C');
else if (str[i] == 'b')
putchar(leader[i] < leader[i + n] ? 'A' : 'C');
else
putchar(leader[i] < leader[i + n] ? 'A' : 'B');
return 0;
}
[ARC161E] Not Dyed by Majority (Cubic Graph)
给出每个点的度数恰为 \(3\) 的无向图。一次操作为将每个点的颜色变为所有邻居颜色的众数。构造黑白颜色序列使得无论如何染色,操作后都不可能变为该颜色序列。
\(\sum n \leq 5 \times 10^4\) ,保证 \(n\) 为偶数。
考虑如何判定一个颜色序列是否可作为操作后的颜色序列。
设操作前的颜色序列为 \(a_i\)(不妨使 \(a_i = 0, 1\) 代表黑和白),操作后的颜色序列为 \(b_i\) 。对于点 \(i\),\(e_{i, j}\) 为与 \(i\) 相邻的三个点。
对每个 \(i\) 有两种情况:
- \(b_i = 0\):则对 \(j \not = k\),若 \(a_{e_{i, j}} = 1\) 则 \(a_{e_{i, k}} = 0\) 。
- \(b_i=1\):则对 \(j\neq k\),若 \(a_{e_{i, j}} = 0\) 则 \(a_{e_{i, k}} = 1\) 。
这是一个关于 \(a_i\),有 \(6n\) 个条件的 2-SAT 问题,可以 \(O(n)\) 解决。
考虑 \(1\) 的邻域 \(x,y,z\),以及他们的邻域 \(\{ 1, x_0, x_1 \}, \{ 1, y_0, y_1 \}, \{1, z_0, z_1\}\) ,若 \(d_{x_0}=d_{x_1},d_{y_0}=d_{y_1},d_{z_0}=d_{z_1}\) ,那么无论 \(d_1\) 是什么,答案都不会更改。
因此至少有 \(\dfrac 1{16}\) 的序列会互相重复,那么期望 \(O(1)\) 次随机后能得到一组解。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 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, nG;
int col[N], dfn[N], low[N], sta[N], leader[N];
mt19937 myrand(time(0));
int n, dfstime, scc, top;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int v : nG.e[u]) {
if (!dfn[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (!leader[v])
low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
++scc;
while (sta[top] != u)
leader[sta[top--]] = scc;
leader[sta[top--]] = scc;
}
}
signed main() {
int T = read();
while (T--) {
G.clear(n = read());
for (int i = 1; i <= n / 2 * 3; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
for (;;) {
nG.clear(n * 2);
for (int u = 1; u <= n; ++u) {
col[u] = rand() & 1;
for (int v : G.e[u])
for (int w : G.e[u])
if (v != w) {
if (col[u])
nG.insert(v, w + n);
else
nG.insert(v + n, w);
}
}
memset(dfn + 1, 0, sizeof(int) * (n * 2));
memset(low + 1, 0, sizeof(int) * (n * 2));
memset(leader + 1, 0, sizeof(int) * (n * 2));
dfstime = scc = 0;
for (int i = 1; i <= n * 2; ++i)
if (!dfn[i])
Tarjan(i);
bool flag = false;
for (int i = 1; i <= n; ++i)
if (leader[i] == leader[i + n]) {
flag = true;
break;
}
if (flag)
break;
}
for (int i = 1; i <= n; ++i)
putchar(col[i] ? 'B' : 'W');
puts("");
}
return 0;
}
割点
定义:对于一个无向图,若把一个点删除后这个图的极大连通分量增加了,则这个点就是图的一个割点。
特判根节点。对于非根节点,若存在一个儿子点 \(v\) 使得 \(low_v \geq dfn_u\) (即不能回到祖先),则该点为割点。
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime;
int sonsum = 0;
for (int v : G.e[u])
if (!dfn[v]) {
++sonsum, Tarjan(v, u);
low[u] = min(low[u], low[v]);
if (f && low[v] >= dfn[u])
tag[u] = true;
} else if (v != f)
low[u] = min(low[u], dfn[v]);
if (!f && sonsum >= 2)
tag[u] = true;
}
桥(割边)
定义:对于一个无向图,若把一条边删除后这个图的极大连通分量增加了,则这条边就是图的一个桥。
代码和割点差不多,只要改一处: \(low_v > dfn_u\) ,而且不用特判根节点。
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (!dfn[v]) {
Tarjan(v, u);
if (low[v] > dfn[u])
G.e[i].tag = G.e[i ^ 1].tag = true;
low[u] = min(low[u], low[v]);
} else if (v != f)
low[u] = min(low[u], dfn[v]);
}
}
边双连通
- 边双连通:无向图中对于两点 \(u, v\) ,若满足删去任意边两点均连通,则称 \(u, v\) 边双连通。
- 边双连通分量:不存在割边的极大连通块。
求解
遍历时不走割边即可求得边双。
也可以用栈维护 dfs 到的所有点,每次找到割边 \((fa,son)\) 就不断弹栈直到弹出 \(son\) ,则弹出的所有点是一个边双。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7, M = 4e6 + 7;
struct Graph {
struct Edge {
int nxt, v;
bool tag;
} e[M];
int head[N];
int tot = 1;
inline void insert(int u, int v) {
e[++tot] = (Edge) {head[u], v, true}, head[u] = tot;
}
} G;
vector<vector<int> > edcc;
int dfn[N], low[N];
bool vis[N];
int n, m, dfstime;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (!dfn[v]) {
Tarjan(v, u), low[u] = min(low[u], low[v]);
if (low[v] > low[u])
G.e[i].tag = G.e[i ^ 1].tag = false;
} else if (v != f)
low[u] = min(low[u], dfn[v]);
}
}
void dfs(int u) {
vis[u] = true, edcc.back().emplace_back(u);
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (!vis[v] && G.e[i].tag)
dfs(v);
}
}
int main() {
n = read(), m = read();
for (int i = 1; i <= m; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
Tarjan(i, 0);
for (int i = 1; i <= n; ++i)
if (!vis[i]) {
edcc.emplace_back(vector<int>());
dfs(i);
}
printf("%d\n", edcc.size());
for (auto it : edcc) {
printf("%d ", it.size());
for (int x : it)
printf("%d ", x);
puts("");
}
return 0;
}
相关结论
-
边双对点有传递性。
-
每个点恰属于一个边双。
-
对于边双内任意一条边 \(e\) ,存在经过 \(e\) 的回路。
-
对于边双内任意两点 \(u, v\) ,存在经过 \(u, v\) 的简单环。
-
两点之间任意一条迹(不经过重复边的路径)上的所有割边,就是两点之间的所有必经边。
-
\(u, v\) 边双连通当且仅当 \(u, v\) 间无必经边。
点双连通
- 点双连通:无向图中对于两点 \(u, v\) ,若满足删去任意除这两点以外的点两点均连通,则称 \(u, v\) 点双连通。
- 点双连通分量:不存在割点的极大连通块。
求解
每次判定 \(low_v \geq dfn_u\) 时,\(v\) 子树栈内的点与 \(u\) 共同构成一个点双,需要特判一下孤立点的情况。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<vector<int> > vdcc;
int dfn[N], low[N], sta[N];
int n, m, dfstime, top;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
void Tarjan(int u, int f) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
int sonsum = 0;
for (int v : G.e[u]) {
if (!dfn[v]) {
++sonsum, Tarjan(v, u), low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
vdcc.emplace_back((vector<int>){u});
while (sta[top] != v)
vdcc.back().emplace_back(sta[top--]);
vdcc.back().emplace_back(sta[top--]);
}
} else if (v != f)
low[u] = min(low[u], dfn[v]);
}
if (!sonsum && !f)
vdcc.emplace_back((vector<int>){sta[top--]});
}
int main() {
n = read(), m = read();
for (int i = 1; i <= m; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
Tarjan(i, 0);
printf("%d\n", vdcc.size());
for (auto it : vdcc) {
printf("%d ", it.size());
for (int x : it)
printf("%d ", x);
puts("");
}
return 0;
}
相关结论
- 点双对点不具有传递性。
- 每条边恰属于一个点双。
- 一个点是割点当且仅当它属于多个点双。
- 由一条边直接相连的两个点点双连通。
- 对于点双内的任意点 \(u\) ,存在经过 \(u\) 的简单环。
- 当 \(n \geq 3\) 时,在边中间插入点不影响点双连通性,因此钦定经过一个点和经过一条边是几乎等价的。
- 对 \(n \geq 3\) 的点双中任意点 \(u\) 与任意边 \(e\) ,存在经过 \(u, e\) 的简单环。
- 对 \(n \geq 3\) 的点双中任意不同两点 \(u, v\) 与任意边 \(e\) ,存在 \(u \rightsquigarrow e \rightsquigarrow v\) 的简单路径。
- 对 \(n \geq 3\) 的点双中任意不同三点 \(u, v, w\) ,存在 \(u \rightsquigarrow v \rightsquigarrow w\) 的简单路径。
- 两点之间任意一条路径上的所有割点,就是两点之间的所有必经点。
- 若两点双有交,那么交点一定是割点。
应用
给定边权为 \(0\) 或 \(1\) 的无向连通图,求有多少组点对之间存在同时经过 \(0\) 和 \(1\) 的简单路径。
\(n \leq 4 \times 10^5, m \leq 10^6\)
对于落在不同点双的点对,如果经过的点双包含 \(0\) 边和 \(1\) 边则合法,否则显然不合法。
对于落在相同点双的点对,如果点双内部边权相同,显然不合法,否则只有一对特殊点(点双唯二既有 \(0\) 出边又有 \(1\) 出边的点对)不合法。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 4e5 + 7, M = 1e6 + 7;
struct DSU {
int fa[N << 1], siz[N << 1];
inline void prework(int n, int ext) {
iota(fa + 1, fa + 1 + n + ext, 1);
fill(siz + 1, siz + 1 + n, 1);
fill(siz + 1 + n, siz + 1 + n + ext, 0);
}
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)
return;
fa[y] = x, siz[x] += siz[y];
}
} dsu1, dsu2;
struct Graph {
struct Edge {
int nxt, v, w;
} e[M << 1];
int head[N];
int tot = 1;
inline void insert(int u, int v, int w) {
e[++tot] = (Edge) {head[u], v, w}, head[u] = tot;
}
} G;
vector<int> bscc[N];
int dfn[N], low[N], sta[N], esta[M], tag[N], tp[N];
bool in[M];
ll ans;
int testid, n, m, dfstime, top, etop, ext;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
inline char readc() {
char c = getchar();
while (c != 'd' && c != 'D')
c = getchar();
return c;
}
void Tarjan(int u) {
dfn[u] = low[u] = ++dfstime, sta[++top] = u;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (!dfn[v]) {
in[esta[++etop] = i >> 1] = true;
Tarjan(v);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
bscc[++ext].emplace_back(u);
while (sta[top] != v)
bscc[ext].emplace_back(sta[top--]);
bscc[ext].emplace_back(sta[top--]);
vector<int> E;
while (esta[etop] != (i >> 1))
E.emplace_back(esta[etop--]);
E.emplace_back(esta[etop--]);
for (int x : E) {
in[x] = false;
tp[ext] |= 1 << G.e[x << 1].w;
tag[G.e[x << 1].v] |= 1 << G.e[x << 1].w;
tag[G.e[x << 1 | 1].v] |= 1 << G.e[x << 1].w;
}
int sum = 0;
for (int x : bscc[ext])
sum += (tag[x] == 3), tag[x] = 0;
if (sum == 2)
--ans;
}
} else {
low[u] = min(low[u], dfn[v]);
if (dfn[v] < dfn[u] && !in[i >> 1])
esta[++etop] = i >> 1;
}
}
}
signed main() {
testid = read(), n = read(), m = read();
for (int i = 1; i <= m; ++i) {
int u = read(), v = read(), w = (readc() == 'd');
G.insert(u, v, w), G.insert(v, u, w);
}
ans = 1ll * n * (n - 1) / 2;
Tarjan(1);
dsu1.prework(n, ext), dsu2.prework(n, ext);
for (int u = 1; u <= ext; ++u)
for (int v : bscc[u])
if (tp[u] == 1)
dsu1.merge(u + n, v);
else if (tp[u] == 2)
dsu2.merge(u + n, v);
for (int i = 1; i <= n + ext; ++i) {
if (dsu1.find(i) == i)
ans -= 1ll * dsu1.siz[i] * (dsu1.siz[i] - 1) / 2;
if (dsu2.find(i) == i)
ans -= 1ll * dsu2.siz[i] * (dsu2.siz[i] - 1) / 2;
}
printf("%lld", ans);
return 0;
}
连通度
- 边连通度:对任意不同的两点 \(u, v\) ,使 \(u, v\) 不连通所需删去的边的数量的最小值 \(k\) 等于 \(u, v\) 之间边不相交的迹的数量的最大值,\(k\) 即为 \(u, v\) 间的边连通度。
- 点连通度:对任意不同且不相邻的两点 \(u, v\) ,使得 \(u, v\) 不连通所需删去的点(除去 \(u, v\) )的数量的最小值 \(k\) 等于 \(u, v\) 之间点(除去 \(u, v\) )不相交的路径数量的最大值,\(k\) 即为 \(u, v\) 间的点连通度。
\(k\) -边连通当且仅当任意两点之间 \(k\) -边连通,点连通同理。
可以用最大流计算边连通度。
Menger 定理:两点间的迹的最大数量等于割集的最小大小。
Whitney 不等式:边连通度不大于点连通度不大于最小度,且对每个满足它的三元组,均可以找出满足这个三元组的图。