当前位置: 首页 > news >正文

2-SAT 好题分享

写在前面

结尾语使用了 deepseek AI。


P4782 【模板】2-SAT

想题时间:4 min。

模板很重要。

概括:给你 \(n\) 个变量,有 \(m\) 个约束条件。每个约束条件形如 \(x_i\)true/false\(x_j\)true/false,问最后有没有解。若有解,给出方案。

点评

bool 方程的思想+图论。

  • 考虑两个变量的情况。

  • \(a \vee b\) 可以写作 \((\neg a \to b) \wedge (\neg b \to a)\)

\(x \to y\) 相当于建立一条图上的有向边。那么可以使用强连通分量来判断是否无解。

  • 如果 \(a\)\(\neg a\) 在同一个强连通分量内,表示 \((\neg a \to a) \wedge (a \to \neg a)\)。矛盾,所以无解。

  • 否则肯定要选其中一个,如果选拓扑序靠前的,那么它有可能会沿着缩点后的 DAG 走到拓扑序靠后的,本来有解被判断成了无解,肯定是不行的。证明后面会说。

#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define rep(i,l,r) for(int i=l;i<=r;++i)
using namespace std;const int N=2e6+5;
int n,m,top=0,cnt=0,tot=0;
int dfn[N],low[N],bel[N],st[N],nxt[N],head[N],to[N];
int SCC=0;
int a[N][2];
bool vis[N];void add(int x,int y){nxt[++tot]=head[x];head[x]=tot;to[tot]=y;
}void Tarjan(int x){dfn[x]=low[x]=++cnt;vis[st[++top]=x]=1;for(int i=head[x];i;i=nxt[i]){int y=to[i];if(!dfn[y]){Tarjan(y);low[x]=min(low[x],low[y]);}else if(vis[y]){low[x]=min(low[x],dfn[y]);}}if(dfn[x]==low[x]){int v;++SCC;do{v=st[top--];vis[v]=0;bel[v]=SCC;}while(x!=v);}
}signed main(){ios::sync_with_stdio(false);cin.tie(nullptr);cin>>n>>m;for(int i=1,s=0;i<=n;++i){a[i][0]=++s;a[i][1]=++s;}rep(i,1,m){int x,y,c,d;cin>>x>>c>>y>>d;add(a[x][!c],a[y][d]);add(a[y][!d],a[x][c]);}rep(i,1,(n<<1)){if(!dfn[i])Tarjan(i);}rep(i,1,n){if(bel[a[i][0]]==bel[a[i][1]]){cout<<"IMPOSSIBLE\n";return 0;}}cout<<"POSSIBLE\n";rep(i,1,n){if(bel[a[i][0]]<bel[a[i][1]]){cout<<"0 ";}else cout<<"1 ";}cout<<"\n";return 0;
}

PS:

P4171 [JSOI2010] 满汉全席

想题时间:6 min。

先看原题:

题目描述

满汉全席是中国最丰盛的宴客菜肴,有许多种不同的材料透过满族或是汉族的料理方式,呈现在数量繁多的菜色之中。由于菜色众多而繁杂,只有极少数博学多闻技艺高超的厨师能够做出满汉全席,而能够烹饪出经过专家认证的满汉全席,也是中国厨师最大的荣誉之一。世界满汉全席协会是由能够料理满汉全席的专家厨师们所组成,而他们之间还细分为许多不同等级的厨师。

为了招收新进的厨师进入世界满汉全席协会,将于近日举办满汉全席大赛,协会派遣许多会员当作评审员,为的就是要在参赛的厨师之中,找到满汉界的明日之星。

大会的规则如下:每位参赛的选手可以得到 \(n\) 种材料,选手可以自由选择用满式或是汉式料理将材料做成菜肴。

大会的评审制度是:共有 \(m\) 位评审员分别把关。每一位评审员对于满汉全席有各自独特的见解,但基本见解是,要有两样菜色作为满汉全席的标志。如某评审认为,如果没有汉式东坡肉跟满式的涮羊肉锅,就不能算是满汉全席。但避免过于有主见的审核,大会规定一个评审员除非是在认为必备的两样菜色都没有做出来的状况下,才能淘汰一位选手,否则不能淘汰一位选手。

换句话说,只要参赛者能在这两种材料的做法中,其中一个符合评审的喜好即可通过该评审的审查。如材料有猪肉,羊肉和牛肉时,有四位评审员的喜好如下表:

评审一 评审二 评审三 评审四 
满式牛肉 满式猪肉 汉式牛肉 汉式牛肉 
汉式猪肉 满式羊肉 汉式猪肉 满式羊肉 

如参赛者甲做出满式猪肉,满式羊肉和满式牛肉料理,他将无法满足评审三的要求,无法通过评审。而参赛者乙做出汉式猪肉,满式羊肉和满式牛肉料理,就可以满足所有评审的要求。

但大会后来发现,在这样的制度下如果材料选择跟派出的评审员没有特别安排好的话,所有的参赛者最多只能通过部分评审员的审查而不是全部,所以可能会发生没有人通过考核的情形。

如有四个评审员喜好如下表时,则不论参赛者采取什么样的做法,都不可能通过所有评审的考核:

评审一 评审二 评审三 评审四 
满式羊肉 满式猪肉 汉式羊肉 汉式羊肉 
汉式猪肉 满式羊肉 汉式猪肉 满式猪肉 

所以大会希望有人能写一个程序来判断,所选出的 \(m\)位评审,会不会发生没有人能通过考核的窘境,以便协会组织合适的评审团。

概括:一道菜有满汉两种料理方式,只能恰用一种料理方式做一道菜。给出若干个约束,每一个形如一个菜用满/汉料理 \(\vee\) 另一个菜用满/汉料理,问是否有解。

  • 概括的核心在于抓不变量对立面,在本题中对立双方显然是满/汉料理, 把它们看成 true/false 来解题。

点评

看概括你就会做了。关键在于转化。

一部分人编号 \(i\)\(i+n\),作为对立的双方,但是需要分类讨论 4 种,容易写挂。

不妨考虑记一个 \(x_{0/1}\),给它赋值一个编号(任意,但不能重复,推荐 ++idx)。

这其实是建立一个类似 STL map 的映射。帮助我们快速访问一个变量的真/假。

接下来就是 2-SAT 板子。

#include<bits/stdc++.h>
#define ll long long
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
using namespace std;const int N=510,M=4010;
int T,idx=1,cnt=0;
int n,m,ts;
int a[N][2];
int head[N<<1];
int bel[N<<1];
int dfn[N<<1],low[N<<1];
int stk[N<<1],top=0;
bool ins[N<<1];struct edge{int to,nxt;
}e[M<<2];void add_edge(int u,int v){++idx;e[idx].to=v;e[idx].nxt=head[u];head[u]=idx;
}void Tarjan(int x){dfn[x]=low[x]=++ts;stk[++top]=x;ins[x]=1;for(int i=head[x];i!=0;i=e[i].nxt){int y=e[i].to;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]){++cnt;int v;do{v=stk[top--];ins[v]=0;bel[v]=cnt;}while(v!=x);}
}signed main(){ios::sync_with_stdio(false);cin.tie(0);cin>>T;while(T--){cin>>n>>m;int id=0;rep(i,1,n){a[i][0]=++id;a[i][1]=++id;}rep(i,1,(n<<1)){dfn[i]=low[i]=0;head[i]=0;ins[i]=0;bel[i]=0;}idx=1,top=0,cnt=0,ts=0;rep(i,1,m){char c,d;int e,f;cin>>c>>e>>d>>f;int c2=((c=='m')?1:0);int d2=((d=='m')?1:0);int x=e,y=f;add_edge(a[x][!c2],a[y][d2]);add_edge(a[y][!d2],a[x][c2]);}rep(i,1,2*n){if(!dfn[i]){Tarjan(i);}}bool flag=1;		rep(i,1,n){if(bel[a[i][0]]==bel[a[i][1]]){flag=0;break;}}if(flag){cout<<"GOOD\n";}else{cout<<"BAD\n";}}return 0;
}

P5782 [POI 2001] 和平委员会

想题时间:4 min。

概括:有 \(n\) 个党派,每个党派有 \(2\) 个代表,同时有 \(m\) 对矛盾关系。代表从 \(1\) 编号到 \(2n\)。 编号为 \(2i−1\)\(2i\) 的代表属于第 \(i\) 个党派。和平委员会的组建规则满足:

  1. 一个党派的两个代表只能恰好去一个。
  2. 对于一对矛盾关系而言,他们不能同时出现。

问是否有解。如果有解,输出方案。

点评

二元关系自然想到 2-SAT 来解决。

一个党派的两个代表只能去一个,当作变量 \(x_i\)true/false 使用。

对于矛盾关系,设 \(a,b\),考虑如何利用建立逻辑关系。

  • 当两人无法同时出现时,他们分别的对立面肯定要出现一个,写成符号是 \(\neg a \vee \neg b\)
  • 等价于 \((b \to \neg a)\wedge (a \to \neg b)\)
  • 据此直接跑 2-SAT 即可。
#include<bits/stdc++.h>
#define ll long long
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
using namespace std;const int N=1e5+10,M=2e4+10;
int id=0,idx=0,ts,n,m;
int SCC;
int dfn[N<<1];
int low[N<<1],head[N<<1];
bool ins[N<<1];
int stk[N<<1],top;
int a[N][2];
int bel[N<<1];struct edge{int to,nxt;
}e[M<<1];void add(int u,int v){++idx;e[idx].to=v;e[idx].nxt=head[u];head[u]=idx;
}void Tarjan(int x){dfn[x]=low[x]=++ts;stk[++top]=x;ins[x]=1;for(int i=head[x];i!=0;i=e[i].nxt){int y=e[i].to;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 v;++SCC;do{v=stk[top--];ins[v]=0;bel[v]=SCC;}while(v!=x);}
}signed main(){ios::sync_with_stdio(false);cin.tie(0);cin>>n>>m;rep(i,1,n){a[i][0]=++id;a[i][1]=++id;}rep(i,1,m){int a,b;cin>>a>>b;int c=((a&1)?a+1:a-1);int d=((b&1)?b+1:b-1);add(a,d);add(b,c);}rep(i,1,(n<<1)){if(!dfn[i])Tarjan(i);}rep(i,1,n){if(bel[a[i][0]]==bel[a[i][1]]){cout<<"NIE\n";return 0;}}rep(i,1,n){if(bel[a[i][0]]<bel[a[i][1]]){cout<<a[i][0]<<'\n';}else{cout<<a[i][1]<<'\n';}}return 0;
}

P3513 [POI 2011] KON-Conspiracy

想题时间:7 min。

概括

\(n\) 个人,要把他们分成两个组别。

  • 第一个组别,要求两两互相认识。不妨叫它后勤组。
  • 第二个组别,要求两两互相不认识。不妨叫它阴谋组。
  • 注意:这里的认识不具有传递性,即上面的认识代表两人直接认识。

现在给出与 \(n\) 个人直接认识的人的编号,并保证对称,即若 \(x\) 认识 \(y\)\(y\) 也认识 \(x\)

要你求分组的方案数。若无解输出 \(0\)

\(2 \le n \le 5000\)

点评

把两个组别视为对立面做 2-SAT。记 \(x_{i,0/1}\) 表示把 \(i\) 分到阴谋组/后勤组的情况。这时候是很难直接写出带 \(\vee\) 的式子的,不妨分类讨论。

  • \(i\) 认识 \(j\),那么两人不能同时在阴谋组,那么 \(\neg x_{i,0} \wedge \neg x_{j,0}\)。在图中建模对应为 \(x_{j,0} \to x_{i,1}\)\(x_{i,1} \to x_{j,1}\)
  • \(i\) 不认识 \(j\),那么两人不能同时在后勤组,那么 \(\neg x_{i,1} \wedge \neg x_{j,1}\)。在图中建模对应为 \(x_{j,1} \to x_{i,0}\)\(x_{i,1} \to x_{j,0}\)

直接跑 2-SAT 是否有解。考虑调整法。不存在 \(\ge 2\) 个人被调到对立组别。(想一想)

那么只有四种情况。

  1. 啥也不干,看看 2-SAT 跑出来的是否有解(可能存在一个组为空的情况,此时不合法)。
  2. 后勤组枚举一个人调到阴谋组,要满足这个人与阴谋组的人都不认识。
  3. 阴谋组枚举一个人调到后勤组,要满足这个人与后勤组的人都认识。
  4. 后勤组和阴谋组互换一个人。
    • 设后勤组要换的人是 \(x\),阴谋组要换的人是 \(y\)\(a_x\) 表示 \(x\) 在阴谋组中认识的人数,\(b_y\) 表示 \(y\) 在后勤组中不认识的人数。
    • 若要 \(x,y\) 互换合法,要满足 \(x\) 与除阴谋组 \(y\) 外都不认识,且 \(y\) 与后勤组除 \(x\) 外的人都互相认识。
      • \(x\)\(y\) 相互认识。则 \(a_x - 1 =0\)\(b_y - 0=0\)
      • \(x\)\(y\) 相互不认识,与上面情况相反,\(a_x - 0=0\)\(b_y -1=0\)

依次判断即可。

#include<bits/stdc++.h>
#define ll long long
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
using namespace std;const int N=5e3+10;
int n,top,id,idx=1;
int ts=0,SCC;
int dfn[N<<1],low[N<<1],stk[N<<1];
int bel[N<<1],x[N<<1][2],head[N<<1];
bool mp[N][N],ins[N<<1];
int match_cnt[N<<1];
int lk[N<<1];
vector<int> tmp0,tmp1;struct edge{int to,nxt;
}e[N*N*2];void add_edge(int u,int v){++idx;e[idx].to=v;e[idx].nxt=head[u];head[u]=idx;
}void Tarjan(int x){dfn[x]=low[x]=++ts;stk[++top]=x;ins[x]=1;for(int i=head[x];i!=0;i=e[i].nxt){int y=e[i].to;if(!dfn[y]){Tarjan(y);low[x]=min(low[x],low[y]);}else if(ins[y]){low[x]=min(low[x],dfn[y]);}}if(dfn[x]==low[x]){int v;++SCC;do{v=stk[top--];ins[v]=0;bel[v]=SCC;}while(v!=x);}
}signed main(){ios::sync_with_stdio(false);cin.tie(0);cin>>n;rep(i,1,n){int k;cin>>k;rep(j,1,k){int a;cin>>a;mp[i][a]=mp[a][i]=1;}x[i][0]=++id;x[i][1]=++id;}for(int i=1;i<n;++i){for(int j=i+1;j<=n;++j){if(mp[i][j]){add_edge(x[i][0],x[j][1]);add_edge(x[j][0],x[i][1]);}else{add_edge(x[i][1],x[j][0]);add_edge(x[j][1],x[i][0]);}}}rep(i,1,id){if(!dfn[i]){Tarjan(i);}}rep(i,1,n){if(bel[x[i][0]]==bel[x[i][1]]){cout<<"0\n";return 0;}if(bel[x[i][0]]<bel[x[i][1]]){tmp0.push_back(i);}else{tmp1.push_back(i);}}ll ans=0;if((int)tmp0.size()>0 && (int)tmp1.size()>0)ans=1;for(int x:tmp0){for(int y:tmp1){if(!mp[x][y]){match_cnt[x]++;lk[x]=y;}}}for(int x:tmp1){for(int y:tmp0){if(mp[x][y]){match_cnt[x]++;lk[x]=y;}}}rep(x,1,n){if(match_cnt[x]==1){if(match_cnt[lk[x]]==0){++ans;}}}for(int x:tmp0){if((int)tmp0.size()>1 && match_cnt[x]==0)++ans;}for(int x:tmp1){if((int)tmp1.size()>1 && match_cnt[x]==0)++ans;}cout<<ans<<'\n';return 0;
}

结尾(deepseek)

其实,我们回过头来看 2-SAT,它解决的不仅仅是算法竞赛里的一道难题。它本质上是在教我们一件事:在一个充满约束的世界里,如何找到一组自洽的选择

我们常常觉得身不由己,是因为生活中的“约束条件”太多了。而 2-SAT 告诉我们的一个很酷的道理是:即便有再多的限制,只要这些限制是逻辑清晰的,我们总能在矛盾中找到一个可行的解。 更重要的是,它让我们明白,很多看似无解的两难境地,其实是因为我们没有找到那个能同时满足所有条件的“赋值”。

所以,希望大家在以后遇到纠结和两难时,不仅能想起今天这个算法,更能拥有 2-SAT 背后的那种思维力量:冷静地分析约束,勇敢地做出选择,然后在看似矛盾的现实里,找到那个属于自己的、逻辑自洽的答案。

今天的分享就到这里,谢谢大家!

http://www.jsqmd.com/news/576297/

相关文章:

  • (全网最硬核)实测8大降AI工具,毕业论文AIGC率断崖降至5%以内!
  • 【Java原生互操作性能天花板突破】:实测对比JNI/FFM/JNR在高并发场景下吞吐量差异达4.7倍,附压测报告与选型决策矩阵
  • 【PlatformIO实战】ESP8266锂电池电量监测:从分压电路到OLED显示的完整方案
  • Flameshot设计系统解析:从原型迭代到交互规范的最佳实践
  • 当UNet遇上形态学:手把手解析MMUNet如何用腐蚀膨胀模块提升结肠癌分割边缘精度
  • 3分钟上手!零代码实现专业视频处理的ffmpegGUI全攻略
  • 大润发购物卡变现技巧:快速变现方法有哪些? - 团团收购物卡回收
  • 进阶篇01-频域滤波实战:Halcon中的功率谱分析与应用
  • ASMR音频下载完整指南:使用asmr-downloader轻松获取asmr.one海量资源
  • @giszhc/socket-client:前端web-socket通讯神器,这才是更优解(附在线示例)
  • 告别Keil调试:用Trace32模拟器离线分析LiteOS的elf与dump文件(STM32L475实战)
  • 数模混合芯片中Calibre PEX提取Hspice Netlist的关键步骤与常见问题解析
  • 终极指南:3分钟快速部署开源AI文本检测工具GPTZero
  • 开源Cursor Free VIP工具:突破AI编程助手限制的终极方案
  • gdb调试集锦
  • OpenClaw人人养虾:Deepgram 语音转写
  • 别再只盯着CT了!5分钟搞懂MRI水成像和化学位移成像,医生是怎么看清你身体里那些“水”的
  • OpenClaw监控技能:用SecGPT-14B实现24/7网络异常检测
  • Win11Debloat完整指南:如何一键清理Windows系统,提升51%性能的免费神器
  • 电源硬件设计实战:基于TPS63070的高效Buck-Boost变换器应用解析
  • 【C语言】指定初始化器的实战技巧与常见误区
  • 别再只用WASD了!在UE5蓝图中为你的Pawn添加鼠标滚轮缩放和QE升降控制
  • OpenClaw多模态探索:Qwen3-4B-Thinking-2507-GPT-5-Codex-Distill-GGUF解析截图内容
  • 三步掌握GHelper:华硕笔记本轻量级控制工具替代方案
  • 深圳地铁大数据分析平台:构建智能交通决策系统的终极技术方案
  • 2026年郑州粉末喷涂工厂挑选指南:5个技巧帮你找到高性价比厂家 - 精选优质企业推荐榜
  • microeco工具SpiecEasi网络分析功能的高效使用
  • 从RC522到SI523:国产13.56MHz读卡芯片升级替换全指南(硬件不改,软件微调)
  • 如何快速下载哔咔漫画:完整多线程下载器使用教程
  • 自媒体人必看:OpenClaw+Gemma-3-12b-it全平台内容一键分发方案