集合划分 题解
一、题目:
二、思路:
这道题思路挺清奇的。属于那种既考算法模板又考思维的一类题。
看到一个点,可以放在\(A\)或\(B\)两个集合,就要联想到2-SAT问题。
这道题如果只让判断是否存在合法方案,并输出任意一种方案,那这题就属于2-SAT问题的模板。
但是这题比较难的一点就是求方案数。
一般的2-SAT问题能不能求方案数呢?我在网上搜了一下,没有找到满意的答案。
那么这道题肯定就是要挖掘题目的性质了。
考虑现在已经用普通的Tarjan算法求出来一种合法方案了,用这一种合法方案去求出其他的合法方案。那么无非就是试图将A集合中的元素放到B集合中,或者将B集合中的元素放到A集合中,或者交换一下A集合、B集合中的元素呗。
一个很重要的性质:我们发现每次最多只能将A集合中的一个元素放到B集合中。形式化一点,假设我们已经求出来一种合法方案\(S\),那么一定不会存在这样的合法方案\(T\),使得存在两个元素\(x, y\), 在\(T\)这种方案下\(x, y\)在\(B\)集合中,但在\(S\)这种方案下\(x,y\)却处于\(A\)集合中。
原因是因为如果\(x,y\)都在\(A\)集合中,那么说明\(x,y\)之间一定有边,那么\(x,y\)就不能放在\(B\)集合中了。
反之,也一定不会存在这样的合法方案\(T'\),使得存在两个元素\(x,y\),在\(T'\)这种方案下\(x,y\)在\(A\)集合中,但在\(S\)这种方案下\(x,y\)却处于\(B\)集合中。原因是类似的。
所以,要想从已知的合法方案\(S\),构造出其他合法方案\(T\),只有三种情况。
- 从\(A\)中取出一个元素\(x\),丢到\(B\)中去。需满足的条件:所有与\(x\)相连的点\(y\),都必须在\(A\)中。
- 从\(B\)中取出一个元素\(y\),丢到\(A\)中去。需满足的条件:所有与\(x\)不相连的点\(y\),都必须在\(B\)中。
- 从\(A\)中取出一个元素\(x\),从\(B\)中取出一个元素\(y\),交换\(x,y\)。需满足的条件:\(\forall (x,u)\in E\),且\(u\neq y\),都有\(u\in A\);\(\forall (y, v)\notin E\),且\(v\neq x\),都有\(v\in B\)。
时间复杂度:\(O(n^2)\)。
然后呢?然后就做完了。
三、实现细节:
- 注意枚举交换\(x,y\)的时候稍微维护一下必要的信息,尽量不要让复杂度上升到\(O(n^3)\)及以上。
- 注意判断无解的情况。
四、代码:
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <queue>
#include <set>
using namespace std;
#define FILEIN(s) freopen(s".in", "r", stdin)
#define FILEOUT(s) freopen(s".out", "w", stdout)
inline int read(void) {
int x = 0, f = 1; char ch = getchar();
while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); }
return f * x;
}
const int maxn = 5005, maxm = maxn * maxn * 2;
int n, head[maxn << 1], tot;
bool G[maxn][maxn], OK = true;
bool ins[maxn << 1];
int dfn[maxn << 1], low[maxn << 1], idx, num, cnt, stk[maxn << 1], top;
int ans[maxn << 1];
int tmp[maxn];
int p[maxn];
struct Edge {
int y, next;
Edge() {}
Edge(int _y, int _next) : y(_y), next(_next) {}
} e[maxm];
set<int>S;
inline void connect(int x, int y) {
e[++ tot] = Edge(y, head[x]);
head[x] = tot;
}
void tarjan(int x) {
dfn[x] = low[x] = ++ num;
stk[++ top] = x; ins[x] = true;
for (int i = head[x]; i; i = e[i].next) {
int y = e[i].y;
if (!dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
} else if (ins[y]) low[x] = min(low[x], dfn[y]);
}
if (low[x] == dfn[x]) {
int y; ++ cnt; S.clear();
do {
y = stk[top --]; ins[y] = false;
if (S.find(y) != S.end()) OK = false;
if (y > n && ans[y - n] == -1) ans[y - n] = 1;
if (y <= n && ans[y] == -1) ans[y] = 0;
if (y > n) S.insert(y - n);
if (y <= n) S.insert(y);
} while (y != x);
}
}
inline void solve(void) {
int res1 = 0, res2 = 0;
int siz1 = 0, siz2 = 0;
for (int i = 1; i <= n; ++ i) {
if (ans[i] == 0) {
++ siz1;
bool flag = true;
for (int j = 1; j <= n; ++ j) {
if (i == j) continue;
if (G[i][j] && ans[j] == 1) flag = false, ++ tmp[i], p[i] = j;
}
if (flag) {
++ res1;
}
}
else { // ans[i] == 1
++ siz2;
bool flag = true;
for (int j = 1; j <= n; ++ j) {
if (i == j) continue;
if (!G[i][j] && ans[j] == 0) flag = false, ++ tmp[i], p[i] = j;
}
if (flag) {
++ res2;
}
}
}
long long sum = 0;
long long res3 = 1LL * res1 * res2;
for (int i = 1; i <= n; ++ i) {
if (tmp[i] != 1) continue;
int cur = p[i];
if (tmp[cur] == 1 && p[cur] == i && cur > i) ++ res3;
if (tmp[cur] == 0) ++ res3;
}
if (siz1 != 1) sum += res1;
if (siz2 != 1) sum += res2;
if (siz1 != 0 && siz2 != 0) sum += res3 + 1;
printf("%lld\n", sum);
}
int main() {
FILEIN("conspiracy"); FILEOUT("conspiracy");
n = read();
memset(ans, -1, sizeof ans);
for (int i = 1; i <= n; ++ i) {
int cnt = read();
while (cnt --) {
int x = read(); G[i][x] = G[x][i] = true;
}
}
for (int i = 1; i <= n; ++ i) {
for (int j = i + 1; j <= n; ++ j) {
if (G[i][j]) {
connect(i + n, j);
connect(j + n, i);
} else {
connect(i, j + n);
connect(j, i + n);
}
}
}
for (int i = 1; i <= 2 * n; ++ i) if (!dfn[i]) tarjan(i);
if (!OK) { puts("0"); return 0; }
solve();
return 0;
}