【学习笔记】图的连通性相关

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 练习

P4334 [COI2007] Policija

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;
}
posted @   happy_zero  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示