【知识】图论 2-SAT
P4782 【模板】2-SAT
Code
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 2000010, M = 2000010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], ts, stk[N], top;
int id[N], cnt;
bool ins[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ts;
stk[++top] = u, ins[u] = true;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if (ins[j])
low[u] = min(low[u], dfn[j]);
}
if (low[u] == dfn[u])
{
int y;
cnt++;
do
{
y = stk[top--], ins[y] = false, id[y] = cnt;
} while (y != u);
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m--)
{
int i, a, j, b;
scanf("%d%d%d%d", &i, &a, &j, &b);
i--, j--;
add(2 * i + !a, 2 * j + b);
add(2 * j + !b, 2 * i + a);
}
for (int i = 0; i < n * 2; i++)
if (!dfn[i])
tarjan(i);
for (int i = 0; i < n; i++)
if (id[i * 2] == id[i * 2 + 1])
{
puts("IMPOSSIBLE");
return 0;
}
puts("POSSIBLE");
for (int i = 0; i < n; i++)
if (id[i * 2] < id[i * 2 + 1])
printf("0 ");
else
printf("1 ");
return 0;
}
SAT 问题: 给定
SAT 问题是一个 NPC 问题(NP 完全问题),如果给我们一个一般的 SAT 问题,我们是无法在多项式的复杂度内把这个问题求解出来的。但是对于 2-SAT 问题,我们是有一些比较效率的解法的,最好的算法可以做到线性的时间复杂度。
2-SAT 问题: 区别于 SAT 问题,2-SAT 问题的每个条件中一定只有两个变量,举例如
2-SAT 问题的一般解法:
给定
接下来我们想把这个问题放到图论里面,我们把每一个命题表示成图论里的一个点,然后把它们之间的推导关系看成边。
如何去推导,这里涉及到 离散数学 中的 数理逻辑,假设有两个变量
对于
得结果为 ( 为 , 一定要是 ) 得结果为 得结果为 ( 不为 , 为几都成立) 得结果为
可以发现,要么
我们通过
通过上述方式,我们能将整个问题转化成一张有向图,每个变量的两种取值对应两个节点,每个条件对应两条边。这样对于图中一条路径
转化完之后,先考虑什么时候会无解,如果从
那么如果规避这个必要条件,如果任意一个变量的真值和假值不在同一个强连通分量中,那么是不是一定有解呢?
这里用构造法,如果任意一个变量的真值和假值在不同的强连通分量里的话,我们用一个特定的方式去构造一组合法的解。首先在求完强连通分量后进行缩点,这样原图就变成一个拓扑图,并求一下拓扑图的拓扑排序,我们可以按照任意顺序枚举所有命题,然后看一下
而在我们求完强连通分量并缩点后,所有强连通分量的编号其实就是拓扑排序的逆序(具体请看强连通分量相关证明),所以对于每个变量,我们要想知道哪个取值在拓扑排序中更靠后,我们只需要看哪个取值所在的强连通分量编号更靠前,因为是倒序,所以编号越小越靠后。
为什么这样构造是正确的呢,首先确定每个变量都只有一种取值,然后对于一个强连通分量,如果选了这个连通分量中的一个点,那么由于推导关系,就需要把该连通分量中的其他点都选上,我们要看一下按照这个构造方法是否能满足这个要求。
对于任意一个命题都存在一个与它等价的逆否命题,即
最后我们再回归原题,我们证明
综上所述,我们证明能用强连通分量求 2-SAT 问题,并且得出构造合法解的方法。由于用 Tarjan 算法能在线性复杂度
注意: 在一般的 2-SAT 问题中,通常不会给出
P3825 [NOI2017] 游戏
本题有
可以发现,这若干个要求
但是现在还有一个有三种取值的
通过以上操作,我们将
时间复杂度为
Code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010, M = 200010;
int n, d, m;
char s[N];
int h[N], e[M], ne[M], idx; //邻接表
int dfn[N], low[N], timestamp; //Tarjan 算法的数组
int stk[N], top; //栈
bool in_stk[N]; //记录每个点是否在栈中
int id[N], cnt; //记录每个点所在的强连通分量
int pos[10]; //记录所有 x 的位置
struct Op
{
int x, y;
char a, b;
}op[M]; //记录所有条件
void add(int a, int b) //添加边
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int get(int x, char b, int t) //返回 x 选 b 时的编号,t 为 1 表示选 -b,t 为 0 表示选 b
{
char a = s[x] - 'a';
b -= 'A';
/*
图 a 能选 B 和 C,图 b 能选 C 和 A,图 c 能选 A 和 B,设选前一个表示取 0,选后一个表示取 1,
若 A/a = 0, B/b = 1, C/c = 2,则 (a + 1) % 3 != b 表示取 0(不是 b 图才能选 b),(a - 1) % 3 != b 表示取 1,
设最开始默认取 1,若 (a + 1) % 3 != b,则取 0,即为取反一次,t 为 1 又表示一次取反,
有个规律,(a + 1) % 3 != b 和 t 都为 1,则取 1,(a + 1) % 3 != b 和 t 都为 0,也取 1,
(a + 1) % 3 != b 和 t 不同,则只取反一次,取 0
*/
if(((a + 1) % 3 != b) ^ t) return x + n; //取 1
return x; //取 0
}
char put(int x, int t) //返回 x 取 t 时对应的赛车字母
{
int y = s[x] - 'a';
return 'A' + (y + 3 + (t ? -1 : 1)) % 3;
}
void tarjan(int u) //求强连通分量
{
dfn[u] = low[u] = ++timestamp;
stk[++top] = u, in_stk[u] = true;
for(int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if(!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(in_stk[j]) low[u] = min(low[u], dfn[j]);
}
if(dfn[u] == low[u])
{
cnt++;
int y;
do
{
y = stk[top--];
in_stk[y] = false;
id[y] = cnt;
} while(y != u);
}
}
bool check() //判断当前情况是否有解,有解并输出方案
{
//初始化
memset(h, -1, sizeof h);
memset(dfn, 0, sizeof dfn);
idx = timestamp = cnt = 0;
//根据所有条件建立推导公式
//设 i 取 1 编号为 i + n, i 取 0 编号为 i
for(int i = 0; i < m; i++)
{
int x = op[i].x - 1, y = op[i].y - 1; //规定下标从 0 开始
char a = op[i].a, b = op[i].b;
if(s[x] != a - 'A' + 'a') //只有第 x 张图能取 a 时,才需要限制条件
{
//第 y 张图能取 b 时,即 x 选 a 时 y 必须选 b,得出推导公式 a -> b, -b -> a
if(s[y] != b - 'A' + 'a') add(get(x, a, 0), get(y, b, 0)), add(get(y, b, 1), get(x, a, 1));
//第 y 张图不能取 b 时,即 x 选 a 时 y 无法选 b,则 x 不能选 a,得出推导公式 a -> -a
else add(get(x, a, 0), get(x, a, 1));
}
}
//求强连通分量
for(int i = 0; i < n * 2; i++)
if(!dfn[i])
tarjan(i);
//如果某一张图的两个取值在同一个强连通分量,说明无解
for(int i = 0; i < n; i++)
if(id[i] == id[i + n])
return false;
//输出方案
for(int i = 0; i < n; i++)
if(id[i] < id[i + n]) printf("%c", put(i, 0)); //取 0
else printf("%c", put(i, 1)); //取 1
return true;
}
int main()
{
scanf("%d%d%s", &n, &d, s);
//找出所有的 x 并记录位置
for(int i = 0, j = 0; i < n; i++)
if(s[i] == 'x')
pos[j++] = i;
scanf("%d", &m);
for(int i = 0; i < m; i++) scanf("%d %c %d %c", &op[i].x, &op[i].a, &op[i].y, &op[i].b);
//用一个长度为 d 的二进制数枚举 x 的所有情况,共 2^d 种
for(int k = 0; k < 1 << d; k++)
{
for(int i = 0; i < d; i++) //枚举所有 x 的情况
if(k >> i & 1) s[pos[i]] = 'a'; //如果第 i 位取 1,则第 i 个 x 变成 a 图
else s[pos[i]] = 'b'; //如果第 i 位取 0,则第 i 个 x 变成 b 图
if(check()) return 0; //如果有解直接结束
}
//到这说明无解
puts("-1");
return 0;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库