图论
连通性相关
SCC & 缩点
这个概念是在有向图里的。
void tarj(ll u) {
dfn[u] = low[u] = ++ tim;
stk.push(u); instk[u] = true;
for (auto x : G[u]) {
if (!dfn[x]) {
tarj(x);
low[u] = min(low[u], low[x]);
}
else if (instk[x])
low[u] = min(low[u], dfn[x]);
}
if (low[u] == dfn[u]) {
++ tot; ll x;
do {
x = stk.top(); stk.pop();
scc[tot].pb(x);
bel[x] = tot;
instk[x] = false;
} while (x != u);
}
}
缩点就是把相同 scc 的点缩成一个,然后在新的 dag 上做事情。
割点 / 点双
无向图里定义。
割点:如果去掉某个点与其出边,这个图就不再连通,这个点就是割点。
点双:不存在割点的图即为点双连通图,一个图中的每个极大点双连通子图称即为一个点双。
求割点
void tarj(ll u, ll rt) {
dfn[u] = low[u] = ++ tim;
ll son = 0;
for (auto x : G[u]) {
if (!dfn[x]) {
tarj(x, rt);
low[u] = min(low[u], low[x]);
++ son;
if (u != rt && low[x] >= dfn[u]) vis[u] = true;
}
else
low[u] = min(low[u], dfn[x]);
}
if (u == rt && son > 1)
vis[u] = true;
}
求点双
void tarj(ll u, ll rt) {
dfn[u] = low[u] = ++ tim; stk.push(u);
if (u == rt && !G[u].size()) {
bcc[++ nbcc].pb(u);
return ;
}
for (auto v : G[u]) {
if (!dfn[v]) {
tarj(v, rt);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
++ nbcc; ll x;
do {
x = stk.top(); stk.pop();
bcc[nbcc].pb(x);
} while (x != v);
bcc[nbcc].pb(u);
}
}
else
low[u] = min(low[u], dfn[v]);
}
}
杂题
P4819
显然最坏情况下,我们需要得知每个人的身份。考虑我们第一次问的一个人,如果他汇报出了若干个平民,我们肯定会继续问这些平民直到汇报出杀手。考虑缩点,相同 scc 内的人看作一个。那么在这个 dag 上,对于那些入度为 \(0\) 的点我们显然都要问一次,记个数为 \(c\),答案不就是 \(1 - \frac{c}{n}\)。
但是考虑排除法。如果存在某个 scc,它的大小为 \(1\) 且出边到达的点不依赖它(入度不为 \(1\)),这个 scc 内的点可以不问,用剩下 \(n-1\) 个点来反推出他即可。如果存在就 \(c \to c-1\) 即可。
P3225
那我菜了呗。
显然考虑求割点。把割点删掉之后图断成了若干连通块,对每个连通块分别考虑:
- 连通块中没割点。说明该连通块孤立,需要建两个,随便选两个建即可。
- 连通块中有 \(1\) 个割点。把割点删掉就不合法了,需要建 \(1\) 个。这是因为如果建的位置塌了,可以跑出去。
- 连通块有 \(2\) 个割点。无需建立。可以任选一边跑掉,总能跑到情况 \(2\) 的连通块。
那么做完了。考虑怎么实现,每次选了一个不是割点的点开搜,遇到割点就统计,不访问即可。
P1407
很妙啊。给一对夫妻连边,男到女连边。给情人女到男连边。然后可以发现,换配偶的过程等价于走一个环。判断一对夫妻是否在同一 scc 即可。
2-SAT
例题
所有条件形如 \(A \lor B\)。那么 \(A\) 变量为假时,\(B\) 变量要求为真,将 \(\neg A\) 向 \(B\) 连边,同理将 \(\neg B\) 向 \(A\) 连边。然后跑 scc,如果 \(A\) 和 \(\neg A\) 在同一 scc 则无解。然后考虑怎么构造方案,选择 \(A\) 和 \(\neg A\) 所在 scc 拓扑序较大的即可。可以感性理解。
void solve() {
cin >> n >> m;
for (ll i = 1; i <= m; ++ i ) {
ll x, a, y, b; cin >> x >> a >> y >> b;
if (a && b)
add(x + n, y), add(y + n, x);
if (a && !b)
add(x + n, y + n), add(y, x);
if (!a && b)
add(x, y), add(y + n, x + n);
if (!a && !b)
add(x, y + n), add(y, x + n);
}
for (ll i = 1; i <= (n << 1); ++ i ) {
if (!dfn[i]) tarj(i);
}
for (ll i = 1; i <= n; ++ i ) {
if (bel[i] == bel[i + n]) {
cout << "IMPOSSIBLE\n";
return 0;
}
}
cout << "POSSIBLE\n";
for (ll i = 1; i <= n; ++ i )
cout << (bel[i] < bel[i + n]) << ' ';
cout << '\n';
}
杂题
Two SAT
枚举点对 \((i,j)\)。若 \(|A_i - A_j| < D\),要求 \(i\) 为 \(1\) 或 \(j\) 为 \(1\),其余同理。时间复杂度 \(O(n^2)\)。
CF776D
经典结论是一个开关只会操作一次。那么 \(i\) 为 \(0/1\) 表示是否要按。根据 \(a_i = 0/1\) 来决定关联的是否要按即可。
hdu4421
考虑拆位。\(a_i\) 显然只有 \(0/1\) 两种状态。枚举 \(i,j\) 根据 \(b_{i,j}\) 状态可以推出 \(a_i,a_j\) 的变量关系。时间复杂度 \(O(n^2)\)。
当然有更聪明的做法。枚举 \(a_0,a_1\),显然可以推出整个 \(a\)。
P10969
考虑一个 2SAT 更通用的连边方法。对于 \(A \land B = 0\) 和 \(A \lor B = 1\) 显然容易。对于 \(A \land B = 1\),要求 \(A,B\) 都填 \(1\),那么就将 \(\neg A\) 向 \(A\) 连边,因为我们取的是 topo 序更大的那个值。对于 \(A \lor B = 0\) 同理。
对于 \(A \oplus B = 0\),将 \(A\) 到 \(B\) 和 \(\neg A\) 到 \(\neg B\) 连双向边。另外一个同理。
浙公网安备 33010602011771号