「算法笔记」2-SAT 问题
修改于不知道哪年不知道哪月。
2021 年写的(已折叠)
一、定义
k-SAT(Satisfiability)问题的形式如下:
-
有 \(n\) 个 01 变量 \(x_1,x_2,\cdots,x_n\),另有 \(m\) 个变量取值需要满足的限制。
-
每个限制是一个 \(k\) 元组 \((x_{p_1},x_{p_2},\cdots,x_{p_k})\),满足 \(x_{p_1}\oplus x_{p_2}\oplus\cdots\oplus x_{p_k}=a\)。其中 \(a\) 为 \(0\) 或 \(1\),\(\oplus\) 是某种二元 bool 运算(如 或运算 \(\vee\)、与运算 \(\wedge\))。
-
要求构造一种满足所有限制的变量的赋值方案。
当 \(k>2\) 时该问题为 NP 完全的,只能暴力求解。因此一般讨论的是 \(k=2\) 的情况,即 2-SAT 问题。
二、基本思想
以 Luogu P4782 【模板】2-SAT 问题 为例,建立图论模型。
\(m\) 个限制,每个限制的形式都是 「\(x_i\) 为 真/假 或 \(x_j\) 为 真/假」。
对于变量 \(x_i\),建立两个点 \(i\) 与 \(i+n\),分别表示 \(x_i\) 为真、\(\neg x_i\) 为真。
若 \(x\) 为真,则 \(\neg x\) 为假;若 \(\neg x\) 为假,则 \(x\) 为真。反之亦然。显然 \(x\) 和 \(\neg x\) 是互斥的。即,点 \(i\) 与 \(i+n\) 分别表示 \(x_i\) 为真或假。
对变量关系建有向图。有向边 \(u\to v\) 表示,若 \(u\) 为真,则 \(v\) 一定为真。
具体地,对于每个限制 \((a\vee b)\)(变量 \(a,b\) 至少满足一个),可将其转化为 \(\neg a\rightarrow b\wedge\neg b\rightarrow a\)(\(a\) 为假则 \(b\) 一定为真;\(b\) 为假则 \(a\) 一定为真)。即节点 \(\neg a\) 向节点 \(b\) 连边,从节点 \(\neg b\) 向节点 \(a\) 连边。
考虑节点 \(i\) 与 \(i+n\) 在图中的关系。若它们 互相可达,即在 同一个强连通分量 中,则说明在赋值限制下,它们代表的一对互斥取值会同时被取到。则不存在一组合法的赋值方案。
否则,说明有解,考虑如何构造一组合法解。
首先,对建出的图进行缩点得到一个 DAG。考虑节点 \(i\) 与 \(i+n\) 所在强连通分量的 拓扑关系。若两分量不连通,则 \(x_i\) 取任意值(真或假)。否则只能取属于拓扑序较大的分量的值。因为若取拓扑序较小的值,可以根据逻辑关系推出取另一个值也是同时发生的。
三、具体实现
以 Luogu P4782 【模板】2-SAT 问题 为例。
-
对于每个限制 \((a\vee b)\)(变量 \(a,b\) 至少满足一个),节点 \(\neg a\) 向节点 \(b\) 连边,从节点 \(\neg b\) 向节点 \(a\) 连边。
-
用 Tarjan 算法对建出的图缩点。
-
对于 \(i\in [1,n]\),若 \(i\) 与 \(i+n\) 在同一个强连通分量中,则不存在一组合法的赋值方案。
-
否则,根据 Tarjan 求得的强连通分量的标号为拓扑逆序(Tarjan 算法求强连通分量时使用了栈),即反向的拓扑序 ,可以得到 \(x_i\) 的值(取 \(i\) 与 \(i+n\) 所在强连通分量拓扑序较大的点的值)。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e6+5; int n,m,x,a,y,b,cnt,hd[N],to[N<<1],nxt[N<<1],tot,c[N],top,s[N],num,dfn[N],low[N]; void add(int x,int y){ to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt; } void tarjan(int x){ dfn[x]=low[x]=++num,s[++top]=x; for(int i=hd[x];i;i=nxt[i]){ int y=to[i]; if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]); else if(!c[y]) low[x]=min(low[x],dfn[y]); } if(low[x]==dfn[x]){ c[x]=++tot; while(s[top]!=x) c[s[top--]]=tot; --top; } } signed main(){ scanf("%lld%lld",&n,&m); for(int i=1;i<=m;i++){ scanf("%lld%lld%lld%lld",&x,&a,&y,&b); if(a&&b) add(x+n,y),add(y+n,x); if(!a&&b) add(x,y),add(y+n,x+n); if(a&&!b) add(x+n,y+n),add(y,x); if(!a&&!b) add(x,y+n),add(y,x+n); } for(int i=1;i<=2*n;i++) if(!dfn[i]) tarjan(i); for(int i=1;i<=n;i++) if(c[i]==c[i+n]) puts("IMPOSSIBLE"),exit(0); puts("POSSIBLE"); for(int i=1;i<=n;i++) printf("%d%c",c[i]<c[i+n],i==n?'\n':' '); //Tarjan 求得的强连通分量的标号为拓扑逆序,即反向的拓扑序 return 0; }
一、2-SAT
每个变量建两个点 \(yes(x_i),no(x_i)\)。边 \(x\to y\) 表示取了 \(x\) 就必须取 \(y\)。
- 判断可行:不存在 \(yes(x_i),no(x_i)\) 在同一 SCC 中。
- 求可行解:SCC 缩点,对每个变量选拓扑排序中靠后的那个取值。拓扑序较大在 Tarjan 中就是 color 较小。
技巧:
-
强制选某一取值:比如强制取 \(no(x)\),连 \(yes(x)\to no(x)\)。
-
有时设计变量 \(a_i=x\) 不好处理,可以设计 \(a_i\geq x\) 之类的。
同时有 \(\geq,\leq ,>,<\) 可以简单转化为只有 \(\geq,<\) 这种。
-
强制 \(a_i\neq x\):\(yes(a_i\geq x)\to yes(a_i\geq x+1)\),\(no(a_i\geq x+1)\to no(a_i\geq x)\)(也就是 \(yes(a_i\leq x)\to yes(a_i\leq x-1)\))。
-
前后缀优化建图。
比如要连向除一个点外所有点,可以拆成前缀与后缀。
-
有些看上去有
A/B/C三种选择的问题,本质上只有形如A/C、B/C这样的两种选择。 -
保留有用的点。比如一些变量 \(a_i\geq x\),很多 \(x\) 其实是无用的。
注意输出方案时要注意某个 \(a_i\) 没有有用 \(x\) 时的情况。
二、例题
1. P3825 [NOI2017] 游戏
2022.3.23
有 \(n\) 个地图和三辆车
A,B,C。如果地图为a表示A不能在这个地图上开,b-B、c-C也是。如果地图为x表示A,B,C都能在这个地图上开,这种地图最多有 \(d\) 个。\(m\) 个限制 \((i,h_i,j,h_j)\),表示若第 \(i\) 个地图用 \(h_i\),那么第 \(j\) 个地图必须用 \(h_j\)(\(h_i,h_j\in\)
A,B,C)。输出任意一组合法分配方案。无解输出 \(-1\)。
\(n\leq 5\times 10^4\),\(d\leq 8\),\(m\leq 10^5\)。
除了 \(d\) 个地图外的其他地图都只有 \(2\) 种选择。
暴力枚举 x 用 A/B/C \(3^d\) 无法承受。发现 a 已经包含了选 B/C,b 已经包含了选 A/C,暴力 \(2^d\) 枚举 x 选 a/b 即可。
现在所有地图都变为只有 \(2\) 种取值,对应 2-SAT 中的一个变量,图上的两个点。
- 若 \(yes(i,h_i)\) 无法满足,该限制无效。
- 否则:
- 若 \(yes(j,h_j)\) 无法满足,该情况无解(不能选 \(yes(i,h_i)\),\(yes(i,h_i)\to no(i,h_i)\))。
- 否则连 \(yes(i,h_i)\to yes(j,h_j)\),\(no(j,h_j)\to no(i,h_i)\)。
时间复杂度 \(\mathcal O(2^d(n+m))\)。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,d,m,p[N],cnt,a[N],b[N],tot,c[N],top,st[N],tim,dfn[N],low[N],tmp;
char s[N],x[N],y[N];
vector<int>v[N];
int yes(int i,char o){return o==(s[i]=='a'?'B':'A')?i:i+n;}
int no(int i,char o){return o==(s[i]=='a'?'B':'A')?i+n:i;}
void tarjan(int x){
dfn[x]=low[x]=++tim,st[++top]=x;
for(int y:v[x]){
if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]);
else if(!c[y]) low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x]){
tot++;
do{c[tmp=st[top--]]=tot;}while(tmp!=x);
}
}
void work(){
for(int i=1;i<=n*2;i++)
if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++) if(c[i]==c[i+n]) return ;
for(int i=1;i<=n;i++)
for(int j=0;j<3;j++) if(s[i]-'a'!=j)
if(c[yes(i,j+'A')]<c[no(i,j+'A')]) putchar(j+'A');
exit(0);
}
signed main(){
scanf("%d%d%s%d",&n,&d,s+1,&m);
for(int i=1;i<=n;i++) if(s[i]=='x') p[i]=++cnt;
for(int i=1;i<=m;i++)
scanf("%d %c%d %c",&a[i],&x[i],&b[i],&y[i]);
for(int k=0;k<(1<<d);k++){
tim=top=tot=0;
for(int i=1;i<=n*2;i++) v[i].clear(),dfn[i]=c[i]=0;
for(int i=1;i<=n;i++)
if(p[i]) s[i]=(k>>(p[i]-1)&1)?'a':'c';
for(int i=1;i<=m;i++) if(s[a[i]]-'a'!=x[i]-'A'){
if(s[b[i]]-'a'==y[i]-'A') v[yes(a[i],x[i])].push_back(no(a[i],x[i]));
else
v[yes(a[i],x[i])].push_back(yes(b[i],y[i])),
v[no(b[i],y[i])].push_back(no(a[i],x[i]));
}
work();
}
puts("-1");
return 0;
}
2. P3209 [HNOI2010] 平面图判定
2023.1.17 CF27D Ring Road 2(*2200)强化
给出一张 \(n\) 个点 \(m\) 条边的无向图,判断其是否为平面图。
特别地,保证存在一条哈密顿回路(并在输入中给出)。
\(T\leq 100\),\(3\leq n\leq 200\),\(m\leq 10000\)。
按照哈密顿回路将所有的点排在一个圆上,边就变成了圆上的弦。
变量:每条边在 圆内/圆外。
若两条边都在圆内会相交(判断条件为对应区间有交且不是包含关系),都在圆外时显然也会相交,只能一个连在圆内一个连在圆外。连 \(yes(x)\to no(y),no(x)\to yes(y),yes(y)\to no(x),no(y)\to yes(x)\)。
\(m\leq 10^4\)?一个结论是,平面图中,\(m\leq 3n-6\),详情见平面图欧拉公式。
时间复杂度 \(\mathcal O(Tn^2)\)。
#include<bits/stdc++.h>
#define yes(i) i
#define no(i) i+k
using namespace std;
const int N=2e3+5;
int t,n,m,x,k,pos[N],eu[N],ev[N],tim,dfn[N],low[N],top,s[N],c[N],tmp,tot;
vector<int>v[N];
void tarjan(int x){
dfn[x]=low[x]=++tim,s[++top]=x;
for(int y:v[x]){
if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]);
else if(!c[y]) low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x]){
tot++;
do{c[tmp=s[top--]]=tot;}while(tmp!=x);
}
}
void work(){
for(int i=1;i<=no(k);i++)
if(!dfn[i]) tarjan(i);
for(int i=1;i<=k;i++)
if(c[yes(i)]==c[no(i)]){puts("NO");return ;}
puts("YES");
}
signed main(){
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&m),k=0;
for(int i=1;i<=m;i++)
scanf("%d%d",&eu[i],&ev[i]);
for(int i=1;i<=n;i++)
scanf("%d",&x),pos[x]=i;
if(m>3*n-6){puts("NO");continue;}
for(int i=1;i<=m;i++){
int x=pos[eu[i]],y=pos[ev[i]];
if(x>y) swap(x,y);
if((x==1&&y==n)||x+1==y) continue;
eu[++k]=x,ev[k]=y;
}
tim=top=tot=0;
for(int i=1;i<=no(k);i++) v[i].clear(),dfn[i]=c[i]=0;
for(int i=1;i<=k;i++)
for(int j=1;j<=k;j++)
if(eu[i]<eu[j]&&eu[j]<ev[i]&&ev[i]<ev[j])
v[yes(i)].push_back(no(j)),v[no(i)].push_back(yes(j)),
v[yes(j)].push_back(no(i)),v[no(j)].push_back(yes(i));
work();
}
return 0;
}
3. P3513 [POI2011]KON-Conspiracy
2022.3.23
给出一张 \(n\) 个点的无向图,需要将 \(n\) 个点分层两个非空点集 \(S_1,S_2\),其中 \(S_1\) 构成团,\(S_2\) 构成独立集。求方案数。
\(2\leq n\leq 5000\)。
变量:每个点 \(\in\) 团/独立集。
- 不存在边的点对 \((x,y)\) 不能同时在团中。
- 一条边 \((x,y)\) 连接的两端不能同时在独立集中。
跑 2-SAT,得到一组可行解。
如何求方案数?所有可行解,都可以通过移出 \(S_1\) 中 \(\leq 1\) 个点到 \(S_2\)、移出 \(S_2\) 中 \(\leq 1\) 个点到 \(S_1\) 得到:若某方移出了 \(\geq 2\) 个点显然不合法。
三种情况:\(S_1\) 某个点移到 \(S_2\),\(S_2\) 某个点移到 \(S_1\),\(S_1\) 某个点移到 \(S_2\) 并将 \(S_2\) 某个点移到 \(S_1\)(即交换)。
\(\mathcal O(n^2)\) 预处理 \(i\) 移动到另一方会与另一方的多少个点产生冲突,记为 \(cnt_i\)。若 \(cnt_i=0\),可以将它移动到另一方,或与另一方某个 \(cnt_j=0\) 的 \(j\) 交换;若 \(cnt_i=1\) 且与 \(i\) 冲突的点 \(j\) 满足 \(cnt_j=0\),可以将 \(i,j\) 交换。
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5;
int n,k,x,p[N],tot,c[N],top,s[N],tim,dfn[N],low[N],tmp,t[2],cnt[N],mp[N],ans;
bool e[N][N],b[N];
vector<int>v[N],A,B;
void tarjan(int x){
dfn[x]=low[x]=++tim,s[++top]=x;
for(int y:v[x]){
if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]);
else if(!c[y]) low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x]){
tot++;
do{c[tmp=s[top--]]=tot;}while(tmp!=x);
}
}
signed main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&k);
while(k--) scanf("%d",&x),e[i][x]=1;
}
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++){
if(e[i][j]) v[i+n].push_back(j),v[j+n].push_back(i);
else v[i].push_back(j+n),v[j].push_back(i+n);
}
for(int i=1;i<=n*2;i++) if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++)
if(c[i]==c[i+n]) puts("0"),exit(0);
for(int i=1;i<=n;i++){
if(c[i]<c[i+n]) A.push_back(i);
else B.push_back(i),b[i]=1;
}
ans=A.size()&&B.size();
for(int i:A)
for(int j:B){
if(e[i][j]) cnt[i]++,mp[i]=j;
else cnt[j]++,mp[j]=i;
}
for(int i=1;i<=n;i++){
if(!cnt[i]){
if((!b[i]&&A.size()>1)||(b[i]&&B.size()>1)) ans++;
t[b[i]]++;
}
else if(cnt[i]==1) ans+=!cnt[mp[i]];
}
printf("%d\n",ans+t[0]*t[1]);
return 0;
}
4. P6378 [PA2010] Riddle
2022.3.23
\(n\) 个点 \(m\) 条边的无向图被分成 \(k\) 个部分,每个部分包含一些点。
问是否能选择一些关键点,使得每个部分恰有一个关键点,且每条边至少有一个端点是关键点。
\(1\leq k,w\leq n\leq 10^6\),\(0\leq m\leq 10^6\)。
变量:每个点 选/不选。
每个部分恰有 \(1\) 个点被选 \(\Rightarrow\) 每个集合被选 \(\leq 1\) 个点:因为这样求出来不合法就是求得的可行解中某个部分无点被选,而第二种约束是“至少”而非“至多”,在这部分任选一个点即可,肯定不会影响第二种约束。
边 \((u,v)\) 上的约束:\(no(x)\to yes(y),no(y)\to yes(x)\);
某一部分的约束:对于这部分的某个点 \(x\),\(x\) 选取了其他点就不能选。\(\mathcal O(n^2)\) 建边无法接受,但每次 \(x\) 都是向这部分的一个前缀和后缀连边,前后缀优化建图即可。
#include<bits/stdc++.h>
using namespace std;
const int N=4e6+5;
int n,m,k,x,y,a[N],id,pre[N],suf[N],tot,c[N],tim,dfn[N],low[N],top,s[N],tmp;
vector<int>v[N];
void tarjan(int x){
dfn[x]=low[x]=++tim,s[++top]=x;
for(int y:v[x]){
if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]);
else if(!c[y]) low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x]){
tot++;
do{c[tmp=s[top--]]=tot;}while(tmp!=x);
}
}
signed main(){
scanf("%d%d%d",&n,&m,&k),id=2*n;
for(int i=1;i<=m;i++){
scanf("%d%d",&x,&y);
v[x+n].push_back(y),v[y+n].push_back(x);
}
for(int i=1,c;i<=k;i++){
scanf("%d",&c);
for(int j=1;j<=c;j++) pre[j]=++id,suf[j]=++id;
for(int j=1;j<=c;j++){
scanf("%d",&x);
v[pre[j]].push_back(x+n),v[suf[j]].push_back(x+n);
if(j>1) v[pre[j]].push_back(pre[j-1]),v[x].push_back(pre[j-1]);
if(j<c) v[suf[j]].push_back(suf[j+1]),v[x].push_back(suf[j+1]);
}
}
for(int i=1;i<=id;i++) if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++)
if(c[i]==c[i+n]) puts("NIE"),exit(0);
puts("TAK");
return 0;
}
5. 一个 ICPC 题
2023.1.17
平面上有 \(n\) 个矩形,每个矩形有 \(4\) 种涂色方案。要求涂色的部分不能重叠的方案。
![]()
\(n\leq 5000\)。
\(4\) 种取值?脑筋急转弯:
-
矩形由两条对角线切割为 \(4\) 个小三角形。
-
每种涂色方案都可以表示为,上下两个小三角形中选一个 + 左右两个小三角形中选一个。
每个矩形的涂色方案就可以由两个独立的变量确定。
-
在小三角形之间处理相交关系即可。
6. CF1215F Radio Stations(*2700)
2023.1.6 前缀优化建图
有 \(n\) 个电台,第 \(i\) 个电台覆盖 \([l_i,r_i]\),其中 \(1\leq l_i\leq r_i\leq m\)。
有 \(a\) 对 \((x,y)\) 表示电台 \(x,y\) 至少选其中一个,\(b\) 对 \((x,y)\) 表示电台 \(x,y\) 至多选其中一个。
要求选出一个电台的子集,使得覆盖范围交集非空,输出交集中任意一个点 \(f\) 以及选取的合法子集。无解输出 \(-1\)。
\(2\leq n,m,a,b\leq 4\times 10^5\)。
设 \(yes(i),no(i)\) 表示电台 \(i\) 选/不选。“电台 \(x,y\) 至少选一个”则 \(no(x)\to yes(y),no(y)\to yes(x)\),“至多选一个”则 \(yes(x)\to no(y),yes(y)\to no(x)\)。
处理若 \(f\not\in[l_i,r_i]\),必须选 \(no(i)\):对 \(i\in[0,m]\),设 \(yes(f\leq i),no(f\leq i)\) 表示 \(f\leq i\) 满足/不满足。
- \(yes(f\leq i)\to yes(f\leq i+1)\),\(no(f\leq i)\to no(f\leq i-1)\)。
- 若电台 \(i\) 选,则 \(f\in[l_i,r_i]\):\(yes(i)\to no(f\leq l_i-1)\),\(yes(i)\to yes(f\leq r_i)\)。
- 若 \(f<l_i\) 或 \(f>r_i\),则电台 \(i\) 不能选:\(yes(f\leq l_i-1)\to no(i)\),\(no(f\leq r_i)\to no(i)\)。
- 由于 \(f\) 不能 \(=0\),所以连 \(yes(f\leq 0)\to no(f\leq 0)\) 强制 \(f>0\)。
建完图跑 2-SAT 判是否有解。
方案:有解时肯定是一段前缀的 \(i\) 选 \(no(f\leq i)\),剩下的后缀选 \(yes(f\leq i)\),分界点 \(f_0\) 就是答案。
#include<bits/stdc++.h>
#define yes(x) (x)*2-1
#define no(x) (x)*2
#define yes2(x) yes(x+n+1)
#define no2(x) no(x+n+1)
using namespace std;
const int N=1.6e6+5;
int n,m,a,b,x,y,l,r,tim,dfn[N],low[N],top,s[N],c[N],tot,tmp;
vector<int>v[N],ans;
void add(int x,int y){v[x].push_back(y);}
void tarjan(int x){
dfn[x]=low[x]=++tim,s[++top]=x;
for(int y:v[x]){
if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]);
else if(!c[y]) low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x]){
tot++;
do{c[tmp=s[top--]]=tot;}while(tmp!=x);
}
}
signed main(){
scanf("%d%d%d%d",&a,&n,&m,&b);
for(int i=1;i<=a;i++)
scanf("%d%d",&x,&y),add(no(x),yes(y)),add(no(y),yes(x));
for(int i=0;i<=m;i++){
if(i<m) add(yes2(i),yes2(i+1));
if(i) add(no2(i),no2(i-1));
}
add(yes2(0),no2(0));
for(int i=1;i<=n;i++)
scanf("%d%d",&l,&r),
add(yes(i),no2(l-1)),add(yes(i),yes2(r)),
add(yes2(l-1),no(i)),add(no2(r),no(i));
for(int i=1;i<=b;i++)
scanf("%d%d",&x,&y),add(yes(x),no(y)),add(yes(y),no(x));
for(int i=1;i<=no2(m);i++)
if(!dfn[i]) tarjan(i);
for(int i=2;i<=no2(m);i+=2)
if(c[i]==c[i-1]) puts("-1"),exit(0);
for(int i=1;i<=n;i++)
if(c[yes(i)]<c[no(i)]) ans.push_back(i);
for(int i=1;i<=m;i++) if(c[yes2(i)]<c[no2(i)]){
printf("%d %d\n",(int)ans.size(),i);
for(int j:ans) printf("%d ",j); exit(0);
}
return 0;
}
7. CF1697F Too Many Constraints(*2800)
2023.1.7
要求构造 \(a_{1\sim n}\),满足 \(a_i\in[1,k]\) 且 \(a_i\leq a_{i+1}\),还有 \(m\) 个约束:
1 i x:\(a_i\neq x\)。2 i j x:\(a_i+a_j\leq x\)。3 i j x:\(a_i+a_j\geq x\)。输出方案,无解输出 \(-1\)。
\(1\leq T\leq 10^4\),\(2\leq n\leq 2\times 10^4\),
- \(yes(a_i\geq v)\to yes(a_i\geq v-1),no(a_i\geq v)\to no(a_i\geq v+1)\),\(no(a_i\geq 1)\to yes(a_i\geq 1)\)。
- \(a_i\leq a_{i+1}\):\(\forall v,yes(a_i\geq v)\to yes(a_{i+1}\geq v),no(a_{i+1}\geq v)\to no(a_i\geq v)\)。
- \(a_i\neq x\):\(yes(a_i\geq x)\to yes(a_i\geq x+1),no(a_i\geq x+1)\to no(a_i\geq x)\)(本题关键!)。
- \(a_i+a_j\leq x\):\(\forall v,yes(a_i\geq v)\to no(a_j\geq x-v+1),yes(a_j\geq v)\to no(a_i\geq x-v+1)\)。
- \(a_i+a_j\geq x\):\(\forall v,no(a_i\geq v)\to yes(a_j\geq x-v+1),yes(a_j\geq v)\to no(a_i\geq x-v+1)\)。
边界细节见代码。
方案:肯定是一个前缀的 \(v\) 选 \(yes(a_i\geq v)\),剩下的后缀选 \(no(a_i\geq v)\),分界点就是 \(a_i\)。
#include<bits/stdc++.h>
#define yes(i,v) ((i-1)*k+v)*2-1
#define no(i,v) yes(i,v)+1
using namespace std;
const int N=4e5+5;
int t,n,m,k,tim,dfn[N],low[N],top,s[N],tot,tmp,c[N];
vector<int>v[N];
void add(int x,int y){v[x].push_back(y);}
void tarjan(int x){
dfn[x]=low[x]=++tim,s[++top]=x;
for(int y:v[x]){
if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]);
else if(!c[y]) low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x]){
tot++;
do{c[tmp=s[top--]]=tot;}while(tmp!=x);
}
}
void get(){
for(int i=1;i<=no(n,k);i++)
if(!dfn[i]) tarjan(i);
for(int i=2;i<=no(n,k);i+=2)
if(c[i]==c[i-1]) return (void)puts("-1");
for(int i=1;i<=n;i++)
for(int j=k;j>=1;j--)
if(c[yes(i,j)]<c[no(i,j)]){printf("%d ",j);break;}
puts("");
}
signed main(){
scanf("%d",&t);
while(t--){
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++){
for(int j=1;j<=k;j++){
if(j>1) add(yes(i,j),yes(i,j-1));
if(j<k) add(no(i,j),no(i,j+1));
}
add(no(i,1),yes(i,1));
}
for(int i=1;i<n;i++)
for(int j=1;j<=k;j++)
add(yes(i,j),yes(i+1,j)),add(no(i+1,j),no(i,j));
while(m--){
int op,i,j,x;
scanf("%d%d",&op,&i);
if(op==1){
scanf("%d",&x);
if(x==k) add(yes(i,x),no(i,x));
else add(yes(i,x),yes(i,x+1)),add(no(i,x+1),no(i,x));
}
else{
scanf("%d%d",&j,&x);
if(op==2)
for(int v=1;v<=k;v++){
if(v+1>x) add(yes(i,v),no(i,v)),add(yes(j,v),no(j,v));
else if(x-v+1<=k) add(yes(i,v),no(j,x-v+1)),add(yes(j,v),no(i,x-v+1));
}
else
for(int v=1;v<=k;v++){
if(v-1+k<x) add(no(i,v),yes(i,v)),add(no(j,v),yes(j,v));
else if(x-v+1>=1) add(no(i,v),yes(j,x-v+1)),add(no(j,v),yes(i,x-v+1));
}
}
}
get(),tot=top=tim=0;
for(int i=1;i<=no(n,k);i++) dfn[i]=low[i]=c[i]=0,v[i].clear();
}
return 0;
}
8. LOJ#3629.「2021 集训队互测」序列
2023.1.25 转化限制 + 保留有用的点,同 QOJ#4892
有一个序列 \(a_{1\sim n}\),\(a_i\in[1,10^9]\)。
\(m\) 条限制形如 \((i,j,k,x)\) 表示满足 \(a_i+a_j+a_k-\max(a_i,a_j,a_k)-\min(a_i,a_j,a_k)=x\)。
要求构造一个满足条件的 \(a\),无解输出
NO。\(1\leq n,m\leq 10^5\),\(1\leq x\leq 10^9\)。
\((i,j,k,x)\) 即 \(a_i,a_j,a_k\) 的中位数是 \(x\)。显然有 \(a_i<x\Rightarrow a_j\geq x,a_k\geq x\)(同理还有 \(3\) 条),\(a_i>x\Rightarrow a_j\leq x,a_k\leq x\)(同理还有 \(3\) 条)。这 \(6\) 个条件足以推出 \(a_i,a_j,a_k\) 的中位数是 \(x\),证明可以简单考虑 (\(a_i,a_j,a_k\) 中有无 \(<x\) 的数,有无 \(>x\) 的数) 共 \(4\) 种情况。
然后类似上一题将每个 \(a_i\) 拆成若干对点形如 \(a_i<x,a_i\geq x\) 即可,\(\leq,>\) 可以分别转化成 $<,\geq $。
如果对于每个 \(a_i\) 都建 \(m\) 个点,点数 \(\mathcal O(nm)\)。不行。
如果对于一个 \((i,j,k,x)\) 只对 \(a_i,a_j,a_k\) 建关于 \(x\) 的点,总点数 \(\mathcal O(m)\)。边数也是 \(\mathcal O(m)\) 的。
输出方案时注意特判 \(a_i\) 没有有用 \(x\) 的情况。
#include<bits/stdc++.h>
using namespace std;
const int N=1.2e6+5; //!!!
int n,m,x,y,z,id,tim,dfn[N],low[N],top,s[N],tmp,tot,c[N];
vector<int>v[N];
map<int,int>leq[N],geq[N];
void nd(int x,int v){
if(!leq[x][v]) leq[x][v]=++id;
if(!geq[x][v+1]) geq[x][v+1]=++id;
if(!leq[x][v-1]) leq[x][v-1]=++id;
if(!geq[x][v]) geq[x][v]=++id;
}
void add(int x,int y){if(x&&y) v[x].push_back(y);}
void tarjan(int x){
dfn[x]=low[x]=++tim,s[++top]=x;
for(int y:v[x]){
if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]);
else if(!c[y]) low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x]){
tot++;
do{c[tmp=s[top--]]=tot;}while(tmp!=x);
}
}
signed main(){
scanf("%d%d",&n,&m);
for(int i=1,v;i<=m;i++){
scanf("%d%d%d%d",&x,&y,&z,&v),nd(x,v),nd(y,v),nd(z,v);
auto get=[&](int x,int y,int z){
add(leq[x][v-1],geq[y][v]),add(leq[x][v-1],geq[z][v]);
add(geq[x][v+1],leq[y][v]),add(geq[x][v+1],leq[z][v]);
};
get(x,y,z),get(y,x,z),get(z,x,y);
}
for(int i=1;i<=n;i++){
int lst=0;
for(auto p:leq[i]) add(lst,p.second),lst=p.second;
lst=0;
for(auto p:geq[i]) add(p.second,lst),lst=p.second;
}
for(int i=1;i<=id;i++)
if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++)
for(auto p:leq[i]){
int x=p.second,y=geq[i][p.first+1];
if(c[x]==c[y]) puts("NO"),exit(0);
}
puts("YES");
for(int i=1;i<=n;i++){
for(auto p:leq[i]){
int v=p.first,x=p.second,y=geq[i][v+1];
if(c[x]<c[y]){printf("%d ",v);break;}
}
if(!leq[i].size()) printf("1 "); //!!!
}
return 0;
}
9. Gym102059B Dev, Please Add This!
2023.2.21
给出一张 \(n\times m\) 的地图,由墙
#、空地.、起点O、星星*构成。从起点开始走,每次可以选择向上下左右某个方向走,碰到边界或墙就停止重新选择方向走,否则不能停下。问能否经过所有星星。
\(1\leq n,m\leq 50\)。
对每个极长无障碍的横段和竖段建点。若从横段的一端滚到另一端,能滚到竖段上去(两段有公共点且竖段必须竖在横段的最左端或最右端,否则不能从横段拐出去)就连一条有向边,竖段到横段同理。
问题转化为,从起点所在段出发,能否经过所有星星所在的段。
- 找特殊性质:一个星星最多在两个段里,可以构成二元关系。每个段要么经过要么不经过,可以 2-SAT。
先找合法的一些必要条件:
- 星星所在横段和竖段至少选一个 \(yes\)。
- 若两个段 互相不可达 不能同时选 \(yes\)。
- 起点所在横段不可达且竖段不可达的段不能选 \(yes\)。
看是否存在合法解。
发现这也是充分条件:根据条件 2,每对选 \(yes\) 的段之间都至少存在一个到达另一个,对所有选 \(yes\) 的段建一张新图 \(G\),一个段可达另一个就连一条有向边(若是互相可达:若其中一个是起点所在段而另一个不是,由起点所在段连向另一个;否则任意),显然 \(G\) 是一张竞赛图。根据条件 3,起点所在横段或起点所在竖段有一个入度为 \(0\),而竞赛图一定存在起点入度为 \(0\) 的哈密顿路径,故一定存在一种合法方案到达所有选 \(yes\) 的段。根据条件 3,所有星星都经过了。故一定合法。
跑 2-SAT check 即可。
#include<bits/stdc++.h>
#define id(x,i) (x*2+i)
using namespace std;
const int N=60,M=5e3+5;
int n,m,num,a[N][N],b[N][N],tim,dfn[M],low[M],top,st[M],c[M],tot,tmp,sx,sy,vis[M][M];
char s[N][N];
vector<int>v[M],to[M];
void dfs(int x,int *vis){
if(vis[x]) return ;
vis[x]=1;
for(int y:to[x]) dfs(y,vis);
}
void tarjan(int x){
dfn[x]=low[x]=++tim,st[++top]=x;
for(int y:v[x]){
if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]);
else if(!c[y]) low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x]){
tot++;
do{c[tmp=st[top--]]=tot;}while(tmp!=x);
}
}
signed main(){
scanf("%d%d",&n,&m);
for(int j=1;j<=m;j++) s[0][j]=s[n+1][j]='#';
for(int i=1;i<=n;i++){
scanf("%s",s[i]+1),s[i][0]=s[i][m+1]='#';
for(int j=1;j<=m;j++) if(s[i][j]!='#')
a[i][j]=s[i][j-1]!='#'?a[i][j-1]:++num,
b[i][j]=s[i-1][j]!='#'?b[i-1][j]:++num;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) if(s[i][j]!='#'){
int x=a[i][j],y=b[i][j];
if(s[i][j]=='O') sx=x,sy=y;
if(s[i][j]=='*')
v[id(x,0)].push_back(id(y,1)),v[id(y,0)].push_back(id(x,1));
if(s[i][j-1]=='#'||s[i][j+1]=='#') to[x].push_back(y);
if(s[i-1][j]=='#'||s[i+1][j]=='#') to[y].push_back(x);
}
for(int i=1;i<=num;i++){
dfs(i,vis[i]);
for(int j=1;j<i;j++)
if(!vis[i][j]&&!vis[j][i])
v[id(i,1)].push_back(id(j,0)),v[id(j,1)].push_back(id(i,0));
}
for(int i=1;i<=num;i++)
if(!vis[sx][i]&&!vis[sy][i]) v[id(i,1)].push_back(id(i,0));
for(int i=1;i<=id(num,1);i++)
if(!dfn[i]) tarjan(i);
for(int i=1;i<=num;i++)
if(c[id(i,0)]==c[id(i,1)]) puts("NO"),exit(0);
puts("YES");
return 0;
}
10. CF587D Duff in Mafia(*3100)
2023.2.28
给出一张 \(n\) 个点 \(m\) 条边的无向图,每条边有一种颜色和一个权值。要求选出一些边,使得这些边是一个匹配,且剩下每种颜色的边也各是一个匹配。
求选出边最大权值的最小值,并输出方案。无解输出
No。\(2\leq n\leq 5\times 10^4\),\(1\leq m\leq 5\times 10^4\),\(1\leq col_i,val_i\leq 10^9\)。
限制:
- 若一条边选,则其相邻边都不能选。
- 若一条边不选,则其相邻边中与它同色的都必须选。
2-SAT + 前后缀优化建图即可。以限制 1 的向前缀连为例:
限制 2 可以类似做,但实际上不需要,如果一个点有 \(>2\) 条同色边直接 No,如果 \(=2\) 条就考虑一下这两条同色边之间的限制即可。
外层套个二分答案,check 就是强制权值 \(>mid\) 的边不能选。
#include<bits/stdc++.h>
#define id(x,o) x*2+o
using namespace std;
const int N=3e5+5;
int n,m,x,y,cnt,col[N],val[N],top,s[N],tim,dfn[N],low[N],tot,c[N],tmp;
vector<int>v[N],g[N],res;
void add(int x,int y){g[x].push_back(y);}
void tarjan(int x){
dfn[x]=low[x]=++tim,s[++top]=x;
for(int y:g[x]){
if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]);
else if(!c[y]) low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x]){
tot++;
do{c[tmp=s[top--]]=tot;}while(tmp!=x);
}
}
bool ok(int mid){
for(int i=1;i<=m;i++)
if(val[i]>mid) add(id(i,1),id(i,0));
tim=top=tot=0;
for(int i=1;i<=cnt;i++) dfn[i]=c[i]=0;
for(int i=1;i<=cnt;i++) if(!dfn[i]) tarjan(i);
for(int i=1;i<=m;i++)
if(val[i]>mid) g[id(i,1)].pop_back();
for(int i=1;i<=m;i++)
if(c[id(i,0)]==c[id(i,1)]) return 0;
return 1;
}
signed main(){
scanf("%d%d",&n,&m),cnt=id(m,1);
for(int i=1;i<=m;i++)
scanf("%d%d%d%d",&x,&y,&col[i],&val[i]),
v[x].push_back(i),v[y].push_back(i);
for(int x=1;x<=n;x++){
sort(v[x].begin(),v[x].end(),[](int i,int j){return col[i]<col[j];});
for(int l=0,r,sz=v[x].size()-1;l<=sz;l=r+1){
r=l;
while(r<sz&&col[v[x][r+1]]==col[v[x][l]]) r++;
if(r-l+1>2) puts("No"),exit(0);
if(r-l+1==2)
add(id(v[x][l],0),id(v[x][r],1)),add(id(v[x][r],0),id(v[x][l],1));
}
for(int t=0;t<2;t++){
int lst=0;
for(int i:v[x]){
int o=++cnt;
if(lst) add(id(i,1),lst),add(o,lst);
add(o,id(i,0)),lst=o;
}
reverse(v[x].begin(),v[x].end());
}
}
int l=0,r=1e9,ans=-1;
while(l<=r){
int mid=(l+r)/2;
if(ok(mid)) ans=mid,r=mid-1;
else l=mid+1;
}
if(!~ans) puts("No"),exit(0);
ok(ans);
for(int i=1;i<=m;i++)
if(c[id(i,1)]<c[id(i,0)]) res.push_back(i);
puts("Yes"),printf("%d %d\n",ans,(int)res.size());
for(int i:res) printf("%d ",i);
return 0;
}

浙公网安备 33010602011771号