「算法笔记」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 问题 为例。

  1. 对于每个限制 \((a\vee b)\)(变量 \(a,b\) 至少满足一个),节点 \(\neg a\) 向节点 \(b\) 连边,从节点 \(\neg b\) 向节点 \(a\) 连边。

  2. 用 Tarjan 算法对建出的图缩点。

  3. 对于 \(i\in [1,n]\),若 \(i\)\(i+n\) 在同一个强连通分量中,则不存在一组合法的赋值方案。

  4. 否则,根据 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/CB/C 这样的两种选择。

  • 保留有用的点。比如一些变量 \(a_i\geq x\),很多 \(x\) 其实是无用的。

    注意输出方案时要注意某个 \(a_i\) 没有有用 \(x\) 时的情况。

二、例题

1. P3825 [NOI2017] 游戏

2022.3.23

\(n\) 个地图和三辆车 A,B,C。如果地图为 a 表示 A 不能在这个地图上开,b - Bc - 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\) 种选择。

暴力枚举 xA/B/C \(3^d\) 无法承受。发现 a 已经包含了选 B/Cb 已经包含了选 A/C,暴力 \(2^d\) 枚举 xa/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\) 种涂色方案。要求涂色的部分不能重叠的方案。

image

\(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。

先找合法的一些必要条件:

  1. 星星所在横段和竖段至少选一个 \(yes\)
  2. 若两个段 互相不可达 不能同时选 \(yes\)
  3. 起点所在横段不可达且竖段不可达的段不能选 \(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\)

限制:

  1. 若一条边选,则其相邻边都不能选。
  2. 若一条边不选,则其相邻边中与它同色的都必须选。

2-SAT + 前后缀优化建图即可。以限制 1 的向前缀连为例:

image

限制 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;
}
posted @ 2021-02-06 12:24  maoyiting  阅读(307)  评论(0)    收藏  举报