一.2-SAT学习
2-SAT建图
- 问题描述: 有 n 个 bool 变量,每个布尔变量都有 0,1 两种可能,给定 m 条限制,例如 (u=0/1)||(v=0/1),或者 (u=0/1)&&(v=0/1),求满足限制的一种方案?
- 2-SAT: 以图论的形式做,将每个点抽象成两个点,表示等于 0 或等于 1。其中: [ 0 , 2 , 4 … … ] [0,2,4……] [0,2,4……] 表示等于 0 , [ 1 , 3 , 5 … … ] [1,3,5……] [1,3,5……] 表示等于 1。然后对限定条件,转化为连边即可。例如:u 为 0 时,v 必须为1: 2 u − > 2 v + 1 2u->2v+1 2u−>2v+1
限定条件连边补充:
若且关系:
u去v必去 :2u+1->2v+1
u去v就不去 :2u+1->2v
强制限定关系:
u必须去 :2u->2u+1
u不可去 :2u+1->2u
位运算
u or v = 1 :2u->2v+1,2v->2u+1
u & v = 0 :2u+1->2v,2v+1->2u
u xor v = 1 :2u->2v+1,2v+1->2u;2v->2u+1,2u+1->2v
u xor v = 0 :2u->2v,2v->2u;2u+1->2v+1,2v+1->2u+1
tarjan O(n) 求 2-SAT 的一组解(不可限定特征)
- 原理: 若 u 的 0 和 1 在同一个强连通分量中,则题意无法满足。否则存在若干解
- 特点: t a r j a n tarjan tarjan 只能输出一个随机的解,无法输出有特征的解。并且只支持有向图
- 输出一个解: 拓扑序越大 -> 越优,即若 u 的 1 拓扑序较 u 的 0 大,就输出 1 ,否则输出 0 。事实上,在缩点染色的时候我们就已经有个一个倒序拓排序。那么:颜色编号越小 -> 拓扑序越大 -> 越优,即 c o l o r [ 2 u ] > c o l o r [ 2 u + 1 ] color[2u]>color[2u+1] color[2u]>color[2u+1] 时输出 1 ;否则输出 0 。
Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+20;
struct ppp {
int u,v,next;
} e[N*2];
int n,k,vex[N*2];
int dfn[N*2],low[N*2],cnt;
int co,color[N*2],stk[N*2],top,instk[N*2];
void add(int u,int v) {
k++;
e[k].u=u;
e[k].v=v;
e[k].next=vex[u];
vex[u]=k;
}
void tarjan(int u) {
dfn[u]=low[u]=++cnt;
instk[u]=1;
stk[++top]=u;
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(!dfn[v]){
tarjan(v);
low[u]=min(low[v],low[u]);
}
else if(instk[v])low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){
co++;
while(1){
int t=stk[top--];
color[t]=co;
instk[t]=0;
if(t==u)break;
}
}
}
void solve(){
for(int i=2;i<=2*n+1;i++)if(!dfn[i])tarjan(i);
for(int i=1;i<=n;i++){
if(color[2*i]==color[2*i+1]){
cout<<"IMPOSSIBLE";
return ;
}
}
cout<<"POSSIBLE"<<endl;
for(int i=1; i<=n; i++)cout<<(coler[2*i]>color[2*i+1])<<" ";
}
int main() {
int m,u,v,x,y;
cin>>n>>m;
for(int i=1; i<=m; i++) {
cin>>u>>x>>v>>y;
add();//限定关系连边
}
solve();
}
dfs 求解 2-SAT 的一组解(可以限定特征)
- 原理: 通过 d f s dfs dfs 染色,可以得到 2-SAT 问题的一组特殊解,其支持:按照位开始,优先染色。例如:字典序最小的解
- 特点: d f s dfs dfs 染色可以求出有特征的解,并且支持无向图求解。缺点是时间复杂度偏高。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+20;
struct ppp {
int u,v,next;
} e[N*2];
int n,k,vex[N*2];
int dfn[N*2],low[N*2],cnt;
int co,color[N*2],stk[N*2],top,instk[N*2];
void add(int u,int v) {
k++;
e[k].u=u;
e[k].v=v;
e[k].next=vex[u];
vex[u]=k;
}
int dfs(int u){
if(vis[u^1])return 0;
if(vis[u])return 1;
vis[u]=1;
stk[++top]=u;
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(dfs(v)==0)return 0;
}
return 1;
}
void solve() {//最小字典序版
for(int i=1;i<=n;i++){
if(vis[2*i]==0&&vis[2*i+1]==0){
top=0;
if(dfs(2*i)==0){//优先考虑不去
while(top)vis[stk[top--]]=0;
if(dfs(2*i+1)==0){//其次考虑去
cout<<"无解";
return;
}
}
}
}
for(int i=1;i<=n;i++){
if(vis[2*i])cout<<"0 ";
else cout<<"1 ";
}
}
int main() {
int m,u,v;
cin>>n>>m;
for(int i=1; i<=m; i++) {
add();//限定关系连边
}
solve();
}
二.2-SAT 例题
tarjan 求解
例题1:建图,判断是否有解
- 题目描述: n 个队伍,每个队伍有三个人 a i , b i , c i a_i,b_i,c_i ai,bi,ci 。其中 a i a_i ai 为队长。要求要么队长去,要么两个队员都去。同时还有 m 个限定条件 a i , b i a_i,b_i ai,bi,表示 a i , b i a_i,b_i ai,bi 必须只去一个。求是否有可行解。
- 限定条件 a i , b i , c i a_i,b_i,c_i ai,bi,ci:
- 2 a i + 1 − > 2 b i , 2 a i + 1 − > 2 c i 2a_i+1->2b_i,2a_i+1->2c_i 2ai+1−>2bi,2ai+1−>2ci
- 2 a i − > 2 b i + 1 , 2 a i − > 2 c i + 1 2a_i->2b_i+1,2a_i->2c_i+1 2ai−>2bi+1,2ai−>2ci+1
- 限定条件 a i , b i a_i,b_i ai,bi:
- 2 a i + 1 − > 2 b i , 2 a i − > 2 b i + 1 2a_i+1->2b_i,2a_i->2b_i+1 2ai+1−>2bi,2ai−>2bi+1
- 使用 t a r j a n tarjan tarjan 判断是否有可行解即可。时间复杂度 O ( n + m ) O(n+m) O(n+m)
例题2:建图,判断是否有解
- 题目描述: 1 对夫妇结婚,n-1 对夫妇去参加婚礼,有一个很长的桌子,新郎新娘坐在桌子两边,然后 n-1 对夫妇就座,要求任何一对夫妇都不能坐在同一边。同时有 m 对人有奸情,有奸情的两个人不能同时坐在新娘对面,只能分开做或都做到新娘的那边,判断是否有解。如果有解,要求输出坐在新娘一边的人。
- 问题分析: 将新娘所在桌子的那边记为 “1”。将 2n 个人拆分成 2n 个 0 和 2n 个 1 。
- 对于新娘(x)新郎(y)位置:
- x − > x + 2 n , y + 2 n − > y x->x+2n,y+2n->y x−>x+2n,y+2n−>y
- 对于夫妇关系 x , y x,y x,y
- x − > y + 2 n , y − > x + 2 n ; y + 2 n − > x , x + 2 n − > y x->y+2n,y->x+2n;y+2n->x,x+2n->y x−>y+2n,y−>x+2n;y+2n−>x,x+2n−>y
- 对于奸情关系 x , y x,y x,y
- x + 2 n − > y , y + 2 n − > x x+2n->y,y+2n->x x+2n−>y,y+2n−>x
二.开始刷题
例题1
将满式和汉式分别记为0,1即可,其他相当于模板题
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+20;
const int M=5e4;
struct ppp {
int u,v,next;
} e[M];
int n,k,vex[N];
int dfn[N],low[N],cnt;
int co,color[N],stk[N],top,instk[N];
void add(int u,int v) {
k++;
e[k].u=u;
e[k].v=v;
e[k].next=vex[u];
vex[u]=k;
}
void tarjan(int u) {
dfn[u]=low[u]=++cnt;
instk[u]=1;
stk[++top]=u;
for(int i=vex[u]; i; i=e[i].next) {
int v=e[i].v;
if(!dfn[v]) {
tarjan(v);
low[u]=min(low[v],low[u]);
} else if(instk[v])low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]) {
co++;
while(1) {
int t=stk[top--];
color[t]=co;
instk[t]=0;
if(t==u)break;
}
}
}
void init(){
memset(vex,0,sizeof(vex));
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(e,0,sizeof(e));
memset(color,0,sizeof(color));
k=0;
cnt=0;
co=0;
}
void solve() {
for(int i=1; i<=2*n; i++)if(!dfn[i])tarjan(i);
for(int i=1; i<=n; i++) {
if(color[i]==color[i+n]) {
cout<<"BAD"<<endl;
return ;
}
}
cout<<"GOOD"<<endl;
}
int main() {
int T,m,u,v;
int p1,p2;
char x,y;
cin>>T;
while(T--) {
init();
cin>>n>>m;
for(int i=1; i<=m; i++) {
cin>>x>>u>>y>>v;
if(x=='h')p1=1;
else p1=0;
if(y=='h')p2=1;
else p2=0;
add(u+(!p1)*n,v+p2*n);
add(v+(!p2)*n,u+p1*n);
}
solve();
}
return 0;
}
dfs暴搜
例题1:dfs染色,优先染和上一位置一样的颜色
- 题目描述: n 个元素的 01 串,其元素未知。有 m 个已知条件 [ l i , r i ] , k i [l_i,r_i],k_i [li,ri],ki,表示区间 [ l i , r i ] [l_i,r_i] [li,ri] 中的 1 的个数为奇数或偶数。求一个最小字典序解。 n , m ∈ [ 1 , 1000 ] n,m\in[1,1000] n,m∈[1,1000]
- 问题分析: [ l i , r i ] , s i [l_i,r_i],s_i [li,ri],si 可以转化为 ( l i ⨁ l i + 1 ⨁ … … ⨁ r i = s i ) (l_i⨁l_{i+1}⨁……⨁r_i=s_i) (li⨁li+1⨁……⨁ri=si) ,再转化为 ( p r e r i ⨁ p r e l i − 1 = k i ) (pre_{r_i}⨁pre_{l_{i-1}}=k_i) (preri⨁preli−1=ki) 。这样就可以转化为 2-SAT 问题了。考虑在 dfs 的时候,优先考虑与上一次解一样的解,即可。注意 p r e [ 0 ] = 0 pre[0]=0 pre[0]=0
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+20;
struct ppp {
int u,v,next;
} e[N*2];
int n,k,vex[N*2];
int dfn[N*2],low[N*2],cnt;
int co,color[N*2],stk[N*2],top,instk[N*2],vis[N*2];
void add(int u,int v) {
k++;
e[k].u=u;
e[k].v=v;
e[k].next=vex[u];
vex[u]=k;
}
int dfs(int u){
if(vis[u^1])return 0;
if(vis[u])return 1;
vis[u]=1;
stk[++top]=u;
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(dfs(v)==0)return 0;
}
return 1;
}
void solve() {
int last=0;
for(int i=0;i<=n;i++){
if(vis[2*i]==0&&vis[2*i+1]==0){
top=0;
if(last==0){
int t=dfs(2*i);
if(t==0){
while(top)vis[stk[top--]]=0;
dfs(2*i+1);
last=1;
}else last=0;
}else{
int t=dfs(2*i+1);
if(t==0){
while(top)vis[stk[top--]]=0;
dfs(2*i);
last=0;
}else last=1;
}
}else last=(vis[2*i+1]==1);
}
for(int i=1;i<=n;i++){
if(vis[2*i+1]==vis[2*(i-1)+1])cout<<"0";
else cout<<"1";
if(i!=n)cout<<" ";
}
}
int main() {
int m,u,v,s;
cin>>n>>m;
for(int i=1; i<=m; i++) {
cin>>u>>v>>s;
u--;
if(s==1){
add(2*u+1,2*v);
add(2*v,2*u+1);
add(2*v+1,2*u);
add(2*u,2*v+1);
}else{
add(2*u,2*v);
add(2*v,2*u);
add(2*u+1,2*v+1);
add(2*v+1,2*u+1);
}
}
add(2*0+1,2*0+0);
solve();
}
例题2:dfs染色,求 1 的数量最多为多少
- 题目描述: n 个人,每个人只有两个身份,要么骗子,要么同伴。骗子只会说假话,同伴只会说真话。m 个评论,表示 u i u_i ui 说 v i v_i vi 是 w i w_i wi,其中 w i = 1 / 0 w_i=1/0 wi=1/0 表示是骗子/同伴。 n ∈ [ 1 , 2 e 5 ] , m ∈ [ 0 , 2 e 5 ] n\in[1,2e5],m\in[0,2e5] n∈[1,2e5],m∈[0,2e5]
- 问题分析: 仔细想想,其实并不是单向的,而是双向的。限定条件为 u ⨁ v = w u⨁v=w u⨁v=w。