闲话 10.10
想到什么写什么
昨晚
CTH(大喊):HDK!
HDK(大喊):CTH!
CTH(愣了一下):干啥?
2-SAT
定义
给出若干个形如 \(a\lor b\) 的限制条件,询问是否有满足条件的一组解。
人话:给出 \(n\) 个集合,每个集合两个元素,再给定若干个限制条件 \(\left \langle a,b\right \rangle\) 表示 \(a\) 与 \(b\) 矛盾,询问在这 \(n\) 个集合中各选一个能否满足所有限制条件。
思路
考虑转化限制条件。已知 \(a\lor b\) 为真,则当 \(\lnot a\) 为真时 \(b\) 必为真,简单推一下可得:
这样模型就构建出来了,我们可以将其转化成图来做。
考虑这样连边的含义,若起点为真则终点必为真,那么若成环则表示这些点同时为真或假,我们可以用 tarjan 将它们缩成一个点。
判断有无解的办法是看 \(a\) 与 \(\lnot a\) 的点是否缩成同一个点,若是则显然无解,否则有解。
若要输出方案,一种合法的构造是判断 \(a\) 与 \(\lnot a\) 缩点的先后,先缩为真。
实现
直接按思路做即可。
点击查看代码
#include<bits/stdc++.h>
#define fo(x, y, z) for(register int (x) = (y); (x) <= (z); (x)++)
#define fu(x, y, z) for(register int (x) = (y); (x) >= (z); (x)--)
using namespace std;
typedef long long ll;
#define lx ll
inline lx qr()
{
char ch = getchar();lx x = 0 , f = 1;
for(;ch<'0'||ch>'9';ch = getchar()) if(ch == '-') f = -1;
for(;ch>= '0' && ch<= '9';ch = getchar()) x = (x<<3) + (x<<1) + (ch^48);
return x*f;
}
#undef lx
#define qr qr()
#define pii pair<int, int>
#define pil pair<int, ll>
#define fi first
#define se second
const int Ratio = 0;
const int N = 2e6 + 5;
int n, m;
int dfn[N], low[N], tot, bl[N], num;
int hh[N], to[N << 1], ne[N << 1], cnt;
stack<int> st;
namespace Wisadel
{
void Wadd(int u, int v)
{
to[++cnt] = v;
ne[cnt] = hh[u];
hh[u] = cnt;
}
void Wtarjan(int u)
{
dfn[u] = low[u] = ++tot, st.push(u);
for(int i = hh[u]; i != -1; i = ne[i])
{
int v = to[i];
if(!dfn[v])
{
Wtarjan(v);
low[u] = min(low[u], low[v]);
}
else if(!bl[v]) low[u] = min(low[u], dfn[v]);
}
if(dfn[u] == low[u])
{
++num;
while(st.size())
{
int zc = st.top(); st.pop();
bl[zc] = num;
if(zc == u) break;
}
}
}
short main()
{
// freopen(".in", "r", stdin) , freopen(".out", "w", stdout);
n = qr, m = qr;
memset(hh, -1, sizeof hh);
fo(i, 1, m)
{
int x = qr, v1 = qr, y = qr, v2 = qr;
if(v1 && v2) Wadd(x + n, y), Wadd(y + n, x);
if(v1 && !v2) Wadd(x + n, y + n), Wadd(y, x);
if(!v1 && v2) Wadd(x, y), Wadd(y + n, x + n);
if(!v1 && !v2) Wadd(x, y + n), Wadd(y, x + n);
}
fo(i, 1, 2 * n) if(!dfn[i]) Wtarjan(i);
bool can = 1;
fo(i, 1, n) if(bl[i] == bl[i + n]){can = 0; break;}
if(!can) puts("IMPOSSIBLE");
else
{
puts("POSSIBLE");
fo(i, 1, n) printf("%d ", (bl[i] < bl[i + n]));
}
return Ratio;
}
}
int main(){return Wisadel::main();}
例题
每一种食材不是满就是汉,比较能看出来是 2-SAT,看出来后就是纯板子了。
点击查看代码
#include<bits/stdc++.h>
#define fo(x, y, z) for(register int (x) = (y); (x) <= (z); (x)++)
#define fu(x, y, z) for(register int (x) = (y); (x) >= (z); (x)--)
using namespace std;
typedef long long ll;
#define lx ll
inline lx qr()
{
char ch = getchar();lx x = 0 , f = 1;
for(;ch<'0'||ch>'9';ch = getchar()) if(ch == '-') f = -1;
for(;ch>= '0' && ch<= '9';ch = getchar()) x = (x<<3) + (x<<1) + (ch^48);
return x*f;
}
#undef lx
#define qr qr()
#define pii pair<int, int>
#define pil pair<int, ll>
#define fi first
#define se second
const int Ratio = 0;
const int N = 1e5 + 5;
int m, n;
int dfn[N], low[N], tot, bl[N], num;
int hh[N], to[N << 1], ne[N << 1], cnt;
stack<int> st;
namespace Wisadel
{
void Wadd(int u, int v)
{
to[++cnt] = v;
ne[cnt] = hh[u];
hh[u] = cnt;
}
void Wtarjan(int u)
{
dfn[u] = low[u] = ++tot, st.push(u);
for(int i = hh[u]; i != -1; i = ne[i])
{
int v = to[i];
if(!dfn[v])
{
Wtarjan(v);
low[u] = min(low[u], low[v]);
}
else if(!bl[v]) low[u] = min(low[u], dfn[v]);
}
if(dfn[u] == low[u])
{
num++;
while(st.size())
{
int zc = st.top(); st.pop();
bl[zc] = num;
if(zc == u) break;
}
}
}
short main()
{
// freopen(".in", "r", stdin) , freopen(".out", "w", stdout);
int T = qr;
while(T--)
{
n = qr, m = qr;
cnt = num = tot = 0;
memset(hh, -1, sizeof hh);
memset(bl, 0, sizeof bl);
memset(dfn, 0, sizeof dfn);
fo(i, 1, m)
{
string s1, s2; cin >> s1 >> s2;
int l1 = s1.size(), l2 = s2.size();
int v1 = 0, v2 = 0;
fo(j, 1, l1 - 1) v1 = v1 * 10 + s1[j] - '0';
fo(j, 1, l2 - 1) v2 = v2 * 10 + s2[j] - '0';
if(s1[0] == 'm' && s2[0] == 'm') Wadd(v1 + n, v2), Wadd(v2 + n, v1);
if(s1[0] == 'm' && s2[0] == 'h') Wadd(v1 + n, v2 + n), Wadd(v2, v1);
if(s1[0] == 'h' && s2[0] == 'm') Wadd(v1, v2), Wadd(v2 + n, v1 + n);
if(s1[0] == 'h' && s2[0] == 'h') Wadd(v1, v2 + n), Wadd(v2, v1 + n);
}
fo(i, 1, 2 * n) if(!dfn[i]) Wtarjan(i);
bool can = 1;
fo(i, 1, n) if(bl[i] == bl[i + n]){can = 0; break;}
if(!can) puts("BAD");
else puts("GOOD");
}
return Ratio;
}
}
int main(){return Wisadel::main();}
发现要用到 tarjan,但是大部分忘了,所以复习。
Tarjan 求点双,缩点
感觉现阶段再看 tarjan 好理解了不少。
相关定义
\(dfn_u\) 为点 \(u\) 的 dfs 序,\(low_u\) 是点 \(u\) 能够回溯到的最早的已经在栈中的结点。
damn 你说的对,但我又把双连通分量和强连通分量搞混了。
思路
缩点
在 2-SAT 中,发现有些点构成了环,环上的点都互相能够到达,可以看做一个点来处理;其它的一些问题,也会有许多点共同构成一个集合的情况。我们把一个任意两点联通的子图称为一个强连通分量。考虑怎么求出一个图的强连通分量。
直接 dfs 图的每个点,并让遍历到的点入栈。当找到一个强连通分量时就出栈。这样我们搜索时会有三种情况:
-
遍历到的点没有 dfs 序,即未遍历过,那么直接遍历该点,并用该点的 \(low\) 值更新当前点的 \(low\) 值。
-
遍历到的点有 dfs 序,但当前不存在于一个已搜到的连通分量内,那么根据定义此时该点必在栈中,用该点的 \(dfn\) 值更新当前点的 \(low\) 值。
-
遍历到的点有 dfs 序,存在于一个连通分量内,已经处理完了,所以不理。
这样操作后,考虑对于一个连通分量,有且仅有其中第一个被遍历到的点满足 \(dfn_u=low_u\),其它的点根据强连通分量的定义都能回溯到该点即 \(dfn_v\gt low_v=dfn_u\),所以回溯时判断二者是否相等即可判断是否该点与栈内在该点上方的点构成一个强连通分量。
求点双
点双概念:在一个点双中,删去任意一个点,其余点仍联通。与之相反的,割点的概念:在一个联通图中,删去该点,其余部分不再联通。
那么我们找到割点就相当于找到了点双。
那么考虑怎么找割点。若当前点是割点,那么其所有子节点都应有 \(low_v\ge dfn_u\)。那么我们直接将此时栈内点 \(v\) 及以上的点全部出栈为一个点双,然后再把当前点加入即可。
注意特判根节点和独立节点的情况。
实现
缩点
例题:所有 2-SAT 的题目。
不同的题可能有不同的细节,但大体上是一样的。
点击查看代码
void Wtarjan(int u)
{
dfn[u] = low[u] = ++tot, st.push(u);
for(int i = hh[u]; i != -1; i = ne[i])
{
int v = to[i];
if(!dfn[v])
{
Wtarjan(v);
low[u] = min(low[u], low[v]);
}
else if(!bl[v]) low[u] = min(low[u], dfn[v]);
}
if(dfn[u] == low[u])
{
++num;
while(st.size())
{
int zc = st.top(); st.pop();
bl[zc] = num;
if(zc == u) break;
}
}
}
点双
纯板子,按上述思路实现即可。
点击查看代码
#include<bits/stdc++.h>
#define fo(x, y, z) for(register int (x) = (y); (x) <= (z); (x)++)
#define fu(x, y, z) for(register int (x) = (y); (x) >= (z); (x)--)
using namespace std;
typedef long long ll;
#define lx ll
inline lx qr()
{
char ch = getchar();lx x = 0 , f = 1;
for(;ch<'0'||ch>'9';ch = getchar()) if(ch == '-') f = -1;
for(;ch>= '0' && ch<= '9';ch = getchar()) x = (x<<3) + (x<<1) + (ch^48);
return x*f;
}
#undef lx
#define qr qr()
#define pii pair<int, int>
#define pil pair<int, ll>
#define fi first
#define se second
const int Ratio = 0;
const int N = 5e5 + 5, M = 2e6 + 5;
int n, m, num;
int dfn[N], low[N], tot;
int hh[N], to[M << 1], ne[M << 1], cnt;
stack<int> st;
vector<int> ans[N];
namespace Wisadel
{
void Wadd(int u, int v)
{
to[++cnt] = v;
ne[cnt] = hh[u];
hh[u] = cnt;
}
void Wtarjan(int u, int fa)
{
int son = 0;
dfn[u] = low[u] = ++tot, st.push(u);
for(int i = hh[u]; i != -1; i = ne[i])
{
int v = to[i];
if(v == fa) continue;
if(!dfn[v])
{
son++;
Wtarjan(v, u);
low[u] = min(low[u], low[v]);
if(low[v] >= dfn[u])
{
++num;
while(st.size())
{
int zc = st.top(); st.pop();
ans[num].push_back(zc);
if(zc == v) break;
}
ans[num].push_back(u);
}
}
else low[u] = min(dfn[v], low[u]);
}
if(fa == 0 && son == 0) ans[++num].push_back(u);
}
short main()
{
// freopen(".in", "r", stdin) , freopen(".out", "w", stdout);
n = qr, m = qr;
memset(hh, -1, sizeof hh);
fo(i, 1, m)
{
int a = qr, b = qr;
Wadd(a, b), Wadd(b, a);
}
fo(i, 1, n) if(!dfn[i]) Wtarjan(i, 0);
printf("%d\n", num);
fo(i, 1, num)
{
printf("%d ", ans[i].size());
for(int j : ans[i]) printf("%d ", j);
puts("");
}
return Ratio;
}
}
int main(){return Wisadel::main();}
体育课
羽毛球教练:咱们这人数有点多了啊,好多别的课的都跑过来了,所以点一下人数,点到名的站这边来。
Ratio:int_R 没来,他是不是羽毛球的?
5k:正确的,你一会就报他名就行。
Aqr(对 CTH):9G 也报的羽毛球,你一会报他名去。
“9G!”
(CTH 冲了过去)
【省略点名的同学和其它熟知 9G 的人的疑惑目光】
“int_R!”
(Ratio 冲了过去,发现顶真也在)
教练(对 Ratio):你叫什么?
Ratio:int_R。(跑
然后顶真就坠机了。
5k:上体育课先练习随机映射。
Ratio:这是离散化数组开小了。
5k:在范围内的数不变,不在范围内的数随机赋值然后到顶真直接越界返回错误指针了是吧。
晚饭
Ratio(对 HDK):没吃饱,再去买点。
(中途碰到一个大概率见过的学妹)
学妹(对另一个):哇,饮月(指我带的徽章)!我又看到他了。
回来后告诉 HDK 全过程
Ratio:这种感觉还挺爽的。
HDK:😡😡😡
大概率不会再更了