【学习笔记】图的连通性相关
1. 无向图的连通性
2. 圆方树
2.1 定义 & 性质
圆方树用来解决需要无向图按点双缩点的问题。这里的点双指的是无割点极大连通子图。
由割点的性质可得,不同的点双之间,实际上是通过割点来连接的。
那么怎么“缩点”?事实上,对于点双来讲,应该叫“缩边”,就是将无用的边缩掉。
具体的,对于每个点双建立一个方点,不保留原来的边,而是将方点与点双中的每一个点连边。例如:
可以发现,不同点双之间是靠割点连接起来的。
圆方树上 \(u\leftrightarrow v\) 路径上的圆点即原图上 \(u\leftrightarrow v\) 的必经点。
2.2 例题
P5058 [ZJOI2004] 嗅探器
板子题,即求圆方树上两点所经过圆点编号最小值,不存在则无解。
注意由于要新建方点,数组应开到两倍大小。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 5;
int n, dfn[N], low[N], ts, cnt, fa[N], d[N];
bool vis[N];
stack <int> q;
vector <int> p[N], e[N];
void tarjan(int k) {
dfn[k] = low[k] = ++ts;
q.push(k);
for (auto i : p[k]) {
if (!dfn[i]) {
tarjan(i);
low[k] = min(low[k], low[i]);
if (low[i] == dfn[k]) {
cnt++; vis[k] = true; int t;
do {
t = q.top(); q.pop();
e[t].push_back(cnt + n);
e[cnt + n].push_back(t);
} while (t != i);
e[k].push_back(cnt + n);
e[cnt + n].push_back(k);
}
} else low[k] = min(low[k], dfn[i]);
}
}
void dfs(int k) {
d[k] = d[fa[k]] + 1;
for (auto i : e[k])
if (i != fa[k]) fa[i] = k, dfs(i);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
while (1) {
int u, v; cin >> u >> v;
if (!u && !v) break;
p[u].push_back(v);
p[v].push_back(u);
}
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i);
dfs(1); int x, y; cin >> x >> y;
if (d[x] < d[y]) swap(x, y);
int minn = N;
while (d[x] > d[y]) {
x = fa[x];
if (vis[x] && x != y)
minn = min(minn, x);
}
while (x != y) {
x = fa[x], y = fa[y];
if (vis[x]) minn = min(minn, x);
if (vis[y]) minn = min(minn, y);
}
if (minn != N) cout << minn << '\n';
else cout << "No solution" << '\n';
return 0;
}
P4606 [SDOI2018] 战略游戏
答案即为包含所有圆点联通块大小的最小值减去圆点数量。
将圆点权值设为 \(1\),方点设为 \(0\),并把每个点权值放到父亲上。
一个 trick 是将给定的所有点按 dfs 序排列,走过相邻两两点所经过的路径上不包括 lca 的点正好经过了两次(第一个和最后一个相邻)。
注意如果所有点的 lca 是圆点的话还得加上 \(1\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, ts, cnt, dfn[N], low[N];
vector <int> p[N], e[N];
stack <int> q;
void tarjan(int k) { /* 同上 */ }
const int M = 20;
int dep[N], anc[N][M], w[N];
void dfs(int k, int fa) {
dep[k] = dep[anc[k][0] = fa] + 1; dfn[k] = ++ts;
for (int j = 1; j < M; j++)
anc[k][j] = anc[anc[k][j - 1]][j - 1];
for (auto i : e[k])
if (i != fa) w[i] = w[k] + (i <= n), dfs(i, k);
}
int lca(int x, int y) { /* 同上 */ }
int b[N];
bool cmp(int x, int y) {
return dfn[x] < dfn[y];
}
void solve() {
memset(dfn, 0, sizeof(dfn)); ts = 0;
while (!q.empty()) q.pop();
int m; cin >> n >> m; cnt = n;
for (int i = 1; i <= m; i++) {
int u, v; cin >> u >> v;
p[u].push_back(v);
p[v].push_back(u);
}
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i);
memset(dfn, 0, sizeof(0)), ts = 0;
dfs(1, 0); int q; cin >> q;
while (q--) {
int s, ans = 0; cin >> s;
for (int i = 1; i <= s; i++) cin >> b[i];
sort(b + 1, b + 1 + s, cmp); b[0] = b[s];
for (int i = 1; i <= s; i++)
ans += w[b[i]] + w[b[i - 1]] - 2 * w[lca(b[i], b[i - 1])];
cout << (ans >> 1) + (lca(b[1], b[s]) <= n) - s << '\n';
}
for (int i = 1; i <= n; i++) p[i].clear();
for (int i = 1; i <= 2 * n; i++) e[i].clear();
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while (T--) solve();
return 0;
}
P4630 [APIO2018] 铁人两项
首先可以证明,一个点双内两点之间简单路径的并就是这个点双。
于是建出圆方树,将方点权值赋为点双大小,圆点权值赋为 \(-1\),那么两点之间可选点的个数就是简单路径上点的权值之和。(圆点取 \(-1\) 是正好抵消端点贡献)
换根处理即可。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 5;
int ts, cnt, n, dfn[N], low[N], w[N];
stack <int> q;
vector <int> e[N], p[N];
void tarjan(int k) {
dfn[k] = low[k] = ++ts, q.push(k);
for (auto i : e[k]) {
if (!dfn[i]) {
tarjan(i), low[k] = min(low[k], low[i]);
if (low[i] == dfn[k]) {
int t; cnt++; w[cnt]++;
do {
t = q.top(), q.pop(); w[cnt]++;
p[cnt].push_back(t);
p[t].push_back(cnt);
} while (t != i);
p[cnt].push_back(k);
p[k].push_back(cnt);
}
} else low[k] = min(low[k], dfn[i]);
}
}
int s, f[N], siz[N], g[N];
void dfs1(int k, int fa) {
if (k <= n) siz[k] = 1, w[k] = f[k] = -1;
for (auto i : p[k])
if (i != fa) {
dfs1(i, k), siz[k] += siz[i];
f[k] += f[i] + w[k] * siz[i];
}
}
void dfs2(int k, int fa, int rt) {
for (auto i : p[k])
if (i != fa) {
g[i] = g[k] - siz[i] * w[k];
g[i] += (siz[rt] - siz[i]) * w[i];
dfs2(i, k, rt);
}
}
signed main() {
ios::sync_with_stdio(false);
int m; cin >> n >> m; cnt = n;
for (int i = 1; i <= m; i++) {
int u, v; cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
for (int i = 1; i <= n; i++)
if (!dfn[i]) {
tarjan(i), dfs1(i, 0);
g[i] = f[i], dfs2(i, 0, i);
}
int ans = 0;
for (int i = 1; i <= n; i++) ans += g[i];
cout << ans + n << '\n';
return 0;
}
CF487E Tourists
建出圆方树,一个想法是将方点权值设为周围圆点权值最小值(multiset 维护),路径树剖维护。
但是这样如果修改割点的话就寄了(要修改很多方点)。
巧妙的思路是,圆点不变,将方点权值改为所有儿子权值的最小值,这样查询就只用修改一个了。
注意如果 lca 是方点的话还得和其父亲的权值取 \(\min\)。
代码。
2.3 练习
3. 强连通分量
3.1 定义
在有向图 dfs 中,定义:
- 树边:实际 dfs 经过的边
- 横叉边:指向非祖先非后代的非树边
- 返祖边:指向祖先的非树边
- 前向边:指向后代的非树边
强连通分量至的是极大的强连通子图。
3.2 tarjan 算法
定义 \(dfn_i\) 表示 \(i\) 的 dfs 序。\(low_i\) 表示:
- \(i\) 通过树边可以到达的点集 \(S\)
- \(S\) 中的点经过一条返祖边可以到达的点
- \(S\) 中的点经过一条横叉边可以到达,且可以到达 \(i\) 祖先的点
的 \(dfn\) 最小值。
如果 \(dfn_i=low_i\),则说明 \(i\) 子树内无到 \(i\) 祖先的返祖边,且不存在横叉边 \(i\to t\) 使得 \(t\) 可到达 \(i\),那么 \(i\) 和其子树内的某些点(也可以没有)组成的强连通块已经是极大的了,即组成一个强连通分量。
具体地:
- 访问点 \(k\),将 \(k\) 入栈
- 遍历 \(k\) 可以通过边 \(id\) 直接到达的 \(i\):
- 若 \(i\) 未被访问,则 \(id\) 是树边,先访问 \(i\),然后 \(low_k\leftarrow \min(low_k, low_i)\)
- 若 \(i\) 被访问过且 \(i\) 在栈中,\(low_k\leftarrow \min(low_k, dfn_i)\)
- 若 \(dfn_k=low_k\),则将栈顶到 \(k\) 的所有元素弹出,这些元素组成一个强连通分量
实现时,维护 \(vis_k\) 表示 \(k\) 是否在栈内。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int dfn[N], low[N], ts, cnt, co[N];
// co[i] 表示 i 所在的强连通分量的编号
bool vis[N];
vector <int> p[N];
stack <int> q;
void tarjan(int k) {
dfn[k] = low[k] = ++ts;
q.push(k), vis[k] = true;
for (auto i : p[k]) {
if (!dfn[i]) tarjan(i), low[k] = min(low[k], low[i]);
else if (vis[i]) low[k] = min(low[k], dfn[i]);
}
if (low[k] == dfn[k]) {
int t; cnt++;
do {
t = q.top(), q.pop();
co[t] = cnt, vis[t] = false;
} while (t != k);
}
}
int main() {
int n, m; cin >> n >> m;
// 输入边,存在 p 中
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i);
return 0;
}
3.3 缩点
缩点及把一个强连通分量及内部的边缩成一个点,保留强连通分量之间的边。不难发现,缩点完整个图是一个 DAG,此时可以通过拓扑排序等解决一些问题。
由于 tarjan 求出强连通分量的顺序是倒着的,因此求出的强连通编号是逆拓扑序。
P3387 【模板】缩点
tarjan 缩点后在 DAG 上 dp 即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 5;
int a[N], dfn[N], low[N], ts, co[N], cnt, into[N], f[N], w[N];
bool in[N];
vector <int> p[N], pp[N];
stack <int> q;
queue <int> qq;
void tarjan(int k) {
dfn[k] = low[k] = ++ts;
q.push(k), in[k] = true;
for (auto i : p[k]) {
if (!dfn[i]) tarjan(i), low[k] = min(low[k], low[i]);
else if (in[i]) low[k] = min(low[k], dfn[i]);
}
if (low[k] == dfn[k]) {
int t; cnt++;
do {
t = q.top(), q.pop(); w[cnt] += a[t];
in[t] = false, co[t] = cnt;
} while (t != k);
}
}
int main() {
int n, m; cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= m; i++) {
int x, y; cin >> x >> y;
p[x].push_back(y);
}
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i);
for (int i = 1; i <= n; i++)
for (auto j : p[i])
if (co[i] != co[j])
pp[co[i]].push_back(co[j]), into[co[j]]++;
for (int i = 1; i <= cnt; i++)
if (!into[i]) qq.push(i), f[i] = w[i];
int ans = 0;
while (!qq.empty()) {
int t = qq.front(); qq.pop();
ans = max(ans, f[t]);
for (auto i : pp[t]) {
into[i]--; f[i] = max(f[i], f[t] + w[i]);
if (!into[i]) qq.push(i);
}
}
cout << ans << '\n';
return 0;
}
4. 2-SAT
4.1 定义
k-SAT 问题即:有一些 bool 类型的变量,给定一些 \(k\) 元 bool 方程,要求未变量赋值满足所有的方程。当 \(k>2\) 时,k-SAT 问题是 NPC 问题,但 2-SAT 有多项式复杂度解。
先考虑拆点 \(i\) 表示 \(i=\text{true}\),\(i+n\) 表示 \(i=\text{false}\) 并建出图。
设形如 \(x=\text{true}/\text{false}\) 的条件为 \(p,q\)。若 \(p\to q\),则 \(p\) 向 \(q\) 连边,同时对于原命题的逆否命题进行连边,即 \(\lnot q\to \lnot p\)。比如:若 \(x=1\),则 \(y=0\),则建边 \(x\to \lnot y,y\to \lnot x\)。
而如果是类似 \(x=0,x=1\) 这类的条件 \(p\),则可以连边 \(\lnot p\to p\),如 \(x=0\) 则连边 \(x\to \lnot x\)。
以 P4782 【模板】2-SAT 为例。\(p\lor q\) 等价于 \(\lnot p\to q, \lnot q\to p\),按上述操作即可。
4.2 暴力
建出图后,枚举每一个变量,尝试其的值是否可以为 \(\text{true}/\text{false}\)。
具体的,设 \(vis_i\) 表示是否选 \(i\)(拆完点后的)。尝试是否可以选 \(x\),就从 \(x\) 出发遍历整张图,若有变量 \(j\) 使得 \(j\) 和 \(j+n\) 都被选了,则 \(x\) 不能被选,把遍历到的变量回溯,然后尝试 \(x\) 的另一个状态;否则就确定 \(x\) 及遍历到的点的状态。
若两种取值都可以选,不难证明随意选一个是正确的。
复杂度 \(O(nm)\) 但跑不满。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 5;
int n;
bool vis[N];
vector <int> p[N];
stack <int> q;
bool dfs(int k) {
if (vis[k > n ? k - n : k + n]) return 0;
if (vis[k]) return 1; vis[k] = true, q.push(k);
for (auto i : p[k])
if (!dfs(i)) return 0;
return 1;
}
int main() {
int m; cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v, a, b; cin >> u >> a >> v >> b;
p[u + (a ^ 1) * n].push_back(v + b * n);
p[v + (b ^ 1) * n].push_back(u + a * n);
}
for (int i = 1; i <= n; i++)
if (!vis[i] && !vis[i + n]) {
while (!q.empty()) q.pop();
if (!dfs(i)) {
while (!q.empty()) vis[q.top()] = false, q.pop();
if (!dfs(i + 1)) return cout << "IMPOSSIBLE", 0;
//如果两个变量都不行,则整张图肯定不可行
}
}
cout << "POSSIBLE\n";
for (int i = 1; i <= n; i++)
cout << vis[i + n] << ' ';
return 0;
}
暴力虽然慢,但它可以求出字典序最小的解等。
4.3 tarjan
首先,若 \(x\) 可以到达 \(\lnot x\) 且 \(\lnot x\) 可以到达 \(x\),及 \(x\) 与 \(\lnot x\) 强连通肯定无解。于是,先用 tarjan 把原图缩点,如果 \(x\) 与 \(\lnot x\) 在一个强连通分量内则无解。
如果有解,若存在 \(x\) 可以到达 \(\lnot x\) 则一定选 \(\lnot x\),反之亦然。
结论:选缩点完所在强连通分量拓扑序更大的一定可行。
证明:若非法,记拓扑序为 \(id\),不妨设存在 \(y\) 可到达 \(x\),\(\lnot y\) 可到达 \(\lnot x\),且 \(id_x>id_{\lnot x},id_{\lnot y}>id_y\)。则有:\(dis_{\lnot y}>dis_y>dis_x>dis_{\lnot x}\)
由于建了逆否命题的边,所以 \(\lnot x\) 也可以到达 \(\lnot y\),于是 \(dis_{\lnot x}>dis_{\lnot y}\),矛盾。原命题得证。
实现时,可以不必拓扑排序,因为强连通分量的编号就是拓扑序的逆序,直接用即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 5;
vector <int> p[N];
int dfn[N], low[N], ts, co[N], cnt;
bool in[N];
stack <int> q;
void tarjan(int k) {
dfn[k] = low[k] = ++ts;
q.push(k), in[k] = true;
for (auto i : p[k]) {
if (!dfn[i]) tarjan(i), low[k] = min(low[k], low[i]);
else if (in[i]) low[k] = min(low[k], dfn[i]);
}
if (low[k] == dfn[k]) {
cnt++; int t;
do {
t = q.top(); q.pop();
in[t] = false, co[t] = cnt;
} while (t != k);
}
}
int main() {
int n, m; cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y, z, q; cin >> x >> y >> z >> q;
p[x + n * (!y)].push_back(z + n * q);
p[z + n * (!q)].push_back(x + n * y);
}
for (int i = 1; i <= n * 2; i++)
if (!dfn[i]) tarjan(i);
for (int i = 1; i <= n; i++)
if (co[i] == co[i + n]) return cout << "IMPOSSIBLE\n", 0;
cout << "POSSIBLE\n";
for (int i = 1; i <= n; i++)
cout << (co[i] > co[i + n]) << ' ';
return 0;
}
4.4 例题
P5782 [POI2001] 和平委员会
\(2i-1\) 和 \(2i\) 恰有一个属于委员会,不妨设他们互相为反面,即:\(\lnot 2i=2i-1,\lnot (2i-1)=2i\)。
若 \(u\) 与 \(v\) 彼此讨厌,则连边:\(u\to \lnot v, v\to \lnot u\)。如上跑 tarjan 即可。
P3825 [NOI2017] 游戏
这题看似是 3-SAT,但实际上,非问号只有两种取值,而问号只有 \(8\) 个,可以枚举问号是什么。
注意到 \(O(3^d)\) 比较难过,发现枚举问号用什么赛车是不必要的,因为枚举地图也可以处理(和其它一样)。而枚举地图只用枚举任意两个地图,因为任意两个地图已经包含了所有赛车。
建图时记得处理限制 \(i\) 取不到 \(h_i\) 或 \(j\) 取不到 \(h_j\) 的情况。复杂度 \(O(2^d(n+m))\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
struct node {
int x, y;
char a, b;
}e[N];
int n, m, b[N], ch[N][2];
int ts, cnt, dfn[N], low[N], co[N];
bool vis[N];
stack <int> q;
vector <int> p[N];
void tarjan(int k) {
dfn[k] = low[k] = ++ts;
q.push(k), vis[k] = true;
for (auto i : p[k]) {
if (!dfn[i]) tarjan(i), low[k] = min(low[k], low[i]);
else if (vis[i]) low[k] = min(low[k], dfn[i]);
}
if (low[k] == dfn[k]) {
int t; cnt++;
do {
t = q.top(); q.pop();
co[t] = cnt, vis[t] = false;
} while (t != k);
}
}
void add(int x, int y, int u, int v) {
p[x + u * n].push_back(y + v * n);
p[y + (v ^ 1) * n].push_back(x + (u ^ 1) * n);
}
void solve() {
ts = cnt = 0;
memset(dfn, 0, sizeof(dfn)), memset(low, 0, sizeof(low));
memset(co, 0, sizeof(co)), memset(vis, 0, sizeof(vis));
while (!q.empty()) q.pop();
for (int i = 1; i <= 2 * n; i++) p[i].clear();
for (int i = 1; i <= m; i++) {
int u = -1, v = -1;
for (int j = 0; j < 2; j++) {
if (ch[e[i].x][j] == e[i].a - 'A' + 1) u = j;
if (ch[e[i].y][j] == e[i].b - 'A' + 1) v = j;
}
if (u == -1) continue;
else if (v == -1) add(e[i].x, e[i].x, u, u ^ 1);
else add(e[i].x, e[i].y, u, v);
}
for (int i = 1; i <= 2 * n; i++)
if (!dfn[i]) tarjan(i);
for (int i = 1; i <= n; i++)
if (co[i] == co[i + n]) return;
for (int i = 1; i <= n; i++)
cout << (char)(ch[i][co[i] > co[i + n]] + 'A' - 1);
exit(0);
}
int main() {
int ts = 0, d; cin >> n >> d;
for (int i = 1; i <= n; i++) {
char x; cin >> x;
if (x == 'x') b[++ts] = i;
else if (x == 'a') ch[i][0] = 2, ch[i][1] = 3;
else if (x == 'b') ch[i][0] = 1, ch[i][1] = 3;
else ch[i][0] = 1, ch[i][1] = 2;
}
cin >> m;
for (int i = 1; i <= m; i++)
cin >> e[i].x >> e[i].a >> e[i].y >> e[i].b;
for (int i = 0; i < (1 << d); i++) {
for (int j = 1; j <= d; j++) {
if (i & (1 << j - 1)) ch[b[j]][0] = 1, ch[b[j]][1] = 2;
else ch[b[j]][0] = 1, ch[b[j]][1] = 3;
}
solve();
}
cout << "-1\n";
return 0;
}
P6378 [PA2010] Riddle
每条边有至少一个端点是简单的,关键在于每个部分只选一个。
暴力的想法是直接将部分中的每一个点(选)想这部分中除它以外的所有点(不选)连边。这样边数是平方级别的,考虑优化。
PS:这样连边看似只限制了每个部分最多一个,但事实上,这题的另一个限制是每条边至少一个,而非恰好,若存在一种方案,一个部分一个点都没选随便选一个也是合法的,不影响结果。
把点按输入顺序(随便什么顺序都行)排好,发现相当于第 \(i\) 个点向第 \([1,i-1]\) 和 \([i+1,w]\) 中的点连边。线段树优化建图是可以的,但这里都是前缀或后缀,不如用前后缀优化建图。
以前缀为例。对每个前缀新建一个虚点,向前缀所有点连边。第 \(i\) 个点就向 \([1,i-1]\) 的虚点连边。
然后发现优化了个寂寞()但代表 \([1,j]\) 的虚点其实也就比 \([1,j-1]\) 多了一个 \(j\)。运用类似前缀和的思想,可以得出一个优化:\([1,j]\) 只用向 \([1,j-1]\) 和 \(j\) 连边即可。
后缀同理,这样边数就是 \(O(n)\) 的了,正常做 2-SAT 即可。
#include <bits/stdc++.h>
#define inv(x) (x + n)
using namespace std;
const int N = 4e6 + 5;
vector <int> p[N];
int dfn[N], low[N], ts, cnt, co[N];
bool vis[N];
stack <int> q;
void tarjan(int k) {
dfn[k] = low[k] = ++ts;
q.push(k), vis[k] = true;
for (auto i : p[k]) {
if (!dfn[i]) tarjan(i), low[k] = min(low[k], low[i]);
else if (vis[i]) low[k] = min(low[k], dfn[i]);
}
if (low[k] == dfn[k]) {
int t; cnt++;
do {
t = q.top(), q.pop();
co[t] = cnt, vis[t] = false;
} while (t != k);
}
}
int main() {
int n, m, k; cin >> n >> m >> k;
for (int i = 1; i <= m; i++) {
int u, v; cin >> u >> v;
p[inv(u)].push_back(v);
p[inv(v)].push_back(u);
}
int ts = 2 * n;
for (int i = 1; i <= k; i++) {
int s; cin >> s;
for (int j = 2; j <= s; j++)
p[ts + j].push_back(ts + j - 1);
for (int j = s - 1; j >= 1; j--)
p[ts + j + s].push_back(ts + j + s + 1);
for (int j = 1; j <= s; j++) {
int x; cin >> x;
p[ts + j].push_back(inv(x));
p[ts + j + s].push_back(inv(x));
if (j != 1) p[x].push_back(ts + j - 1);
if (j != s) p[x].push_back(ts + j + 1 + s);
}
ts += 2 * s;
}
for (int i = 1; i <= ts; i++)
if (!dfn[i]) tarjan(i);
for (int i = 1; i <= n; i++)
if (co[i] == co[i + n]) return cout << "NIE", 0;
cout << "TAK";
return 0;
}
P6965 [NEERC2016] Binary Code
一个字符串只有两种情况,且互为对立面,可以 2-SAT。如何 \(A\) 包含 \(B\) 则互向对方反面连边。
边数 \(O(n^2)\),还是考虑优化建图。01 字符串前缀关系想到 01-trie。
字典树类似线段树,建根向和叶向的两棵树 \(T1,T2\):
- 若 \(x\) 对应字典树上的点为 \(y\),则 \(T1_y\to \lnot x,T2_y\to \lnot x\)
- \(\forall x\in T\),\(T1_x\to T2_{fa_x}\)
- \(\forall x\in T\),\(T2_{ls_x}\to T1_x,T2_{rs_x}\to T1_x\)
这里的字典树和线段树优化建图一样,只是优化边数的工具,而并不能代表具体的字符串。
但发现这样处理不了两个相同字符串。于是把相同的字符串两两连边,参考上一题用前后缀优化即可。
注意一个小细节。
#include <bits/stdc++.h>
#define fir first
#define sec second
using namespace std;
const int N = 4e6 + 5;
int n, ts, id[N], fa[N], t[N][2];
pair <string, int> s[N];
vector <int> p[N];
int find(string s) {
int now = 2 * n, n = s.size();
for (int i = 0; i < n; i++) {
if (t[now][s[i] - '0'] == -1) {
t[now][s[i] - '0'] = ++ts;
p[ts].push_back(now); fa[ts] = now;
t[now ^ 1][s[i] - '0'] = ++ts;
p[now ^ 1].push_back(ts); fa[ts] = now ^ 1;
}
now = t[now][s[i] - '0'];
}
return now;
}
void work(int x) {
id[x] = find(s[x].fir);
p[id[x]].push_back(x ^ 1);
p[id[x] ^ 1].push_back(x ^ 1);
}
int tss, dfn[N], low[N], cnt, co[N];
bool vis[N];
string ans[N];
stack <int> q;
void tarjan(int k, int fa = -1) {
dfn[k] = low[k] = ++tss;
q.push(k), vis[k] = true;
for (auto i : p[k]) {
if (!dfn[i]) tarjan(i, k), low[k] = min(low[k], low[i]);
else if (vis[i]) low[k] = min(low[k], dfn[i]);
}
if (low[k] == dfn[k]) {
int t; cnt++;
do {
t = q.top(), q.pop();
co[t] = cnt, vis[t] = false;
} while (t != k);
}
}
int main() {
memset(fa, -1, sizeof(fa));
memset(t, -1, sizeof(t));
cin >> n; ts = 2 * n + 1;
for (int i = 1; i <= n; i++) {
string t; cin >> t; int x = i * 2 - 2, y = i * 2 - 1;
s[x].fir = s[y].fir = t, s[x].sec = x, s[y].sec =y;
for (int j = 0; j < t.size(); j++)
if (t[j] == '?') s[x].fir[j] = '0', s[y].fir[j] = '1';
work(x), work(y);
}
for (int x = 0; x < 2 * n; x++) {
if (t[id[x]][0] != -1) p[x].push_back(t[id[x] ^ 1][0]);
if (t[id[x]][1] != -1) p[x].push_back(t[id[x] ^ 1][1]);
if (fa[id[x]] != -1) p[x].push_back(fa[id[x]]);
}
sort(s, s + 2 * n);
for (int i = 0, j = 0; i < 2 * n; i = j) {
while (j < 2 * n && s[j].fir == s[i].fir) j++;
int len = j - i;
for (int k = i; k < j; k++) {
p[ts + k - i + 1].push_back(s[k].sec ^ 1);
p[ts + k - i + 1 + len].push_back(s[k].sec ^ 1);
}
for (int k = i + 1; k < j; k++) {
p[s[k].sec].push_back(ts + k - i);
p[ts + k - i + 1].push_back(ts + k - i);
}
for (int k = i; k < j - 1; k++) {
p[s[k].sec].push_back(ts + k - i + 2 + len);
p[ts + k - i + 1 + len].push_back(ts + k - i + 2 + len);
}
ts += 2 * len;
}
for (int i = 0; i <= ts; i++)
if (!dfn[i]) tarjan(i);
for (int i = 0; i < n * 2; i++) {
if (co[s[i].sec] == co[s[i].sec ^ 1]) return cout << "NO", 0;
if (co[s[i].sec] < co[s[i].sec ^ 1]) ans[(s[i].sec >> 1) + 1] = s[i].fir;
}
cout << "YES\n";
for (int i = 1; i <= n; i++)
cout << ans[i] << '\n';
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】