【笔记】联通分量/2-SAT

P3387 【模板】缩点

\(\rm Tarjan\) 算法求线性求强连通分量。

算法的核心在于时间戳和栈的维护。

我们将每个强连通分量缩成一个点,将得到一个有向无环图\(\rm DAG\),就可以在上面跑\(\rm DP\)。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 100005
using namespace std;
int n,m,u[N],h[N],tot;
struct edge{
	int to,nxt;
}e[N<<1];
void add(int x,int y){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;
}
int dfn[N],col,idx,c[N],mat[N],low[N],v[N],top,sta[N];
void dfs(int x){
	dfn[x]=low[x]=++idx;sta[++top]=x;v[x]=1;
	for(int i=h[x];i;i=e[i].nxt){
		if(!dfn[e[i].to])dfs(e[i].to),low[x]=min(low[x],low[e[i].to]);
		else if(v[e[i].to])low[x]=min(low[x],dfn[e[i].to]);
	}
	if(dfn[x]==low[x]){
		col++;
		while(true){
			int y=sta[top--];v[y]=0;
			mat[y]=col;c[col]+=u[y];
			if(x==y)break;
		}
	}
}
vector<int>a[N];
int f[N];
int calc(int x){
	if(~f[x])return f[x];
	f[x]=0;
	for(int i=0;i<(int)a[x].size();i++)f[x]=max(f[x],calc(a[x][i]));
	return f[x]+=c[x];
}
int main(){
	scanf("%d%d",&n,&m);
	rep(i,1,n)scanf("%d",&u[i]);
	rep(i,1,m){
		int x,y;scanf("%d%d",&x,&y);
		add(x,y);
	}
	rep(i,1,n)if(!dfn[i])dfs(i);
	int ans=0;
	rep(x,1,n)for(int i=h[x];i;i=e[i].nxt)if(mat[x]!=mat[e[i].to])
			a[mat[x]].push_back(mat[e[i].to]);
	memset(f,-1,sizeof(f));
	rep(i,1,col)ans=max(ans,calc(i));
	printf("%d\n",ans);
	return 0;
}

P3388 【模板】割点(割顶)

\(\rm Tarjan\) 同样可以求割点。

一个点为割点的充要条件是 \(dfn[u]\ge low[son[u]]\) ,注意根节点要特判,根节点为割点的充要条件是有多个儿子。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 100005
using namespace std;
int n,m,h[N],tot;
struct edge{
	int to,nxt;
	}e[N<<1];
void add(int x,int y){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;
	}
int dfn[N],low[N],idx,sta[N],top,son[N];
void dfs(int x,int fa){
	dfn[x]=low[x]=++idx;bool flag=false;int son=0;
	for(int i=h[x];i;i=e[i].nxt){
		if(!dfn[e[i].to]){
			dfs(e[i].to,x),low[x]=min(low[x],low[e[i].to]);
			son++;flag|=(low[e[i].to]>=dfn[x]);
		}
		else low[x]=min(low[x],dfn[e[i].to]);
	}
	if((fa&&flag)||(!fa&&son>1))sta[++top]=x;
}
int main(){
	scanf("%d%d",&n,&m);
	while(m--){
		int x,y;scanf("%d%d",&x,&y);
		add(x,y);add(y,x);
		}
	rep(i,1,n)if(!dfn[i])dfs(i,0);printf("%d\n",top);
	sort(sta+1,sta+top+1);
	rep(i,1,top)printf("%d ",sta[i]);
	return 0;
}

P2863 [USACO06JAN]The Cow Prom S

模板,直接\(\rm Tarjan\)。

Code


P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G

缩点,唯一一个没有出度的强连通分量中的奶牛为答案。如果有多个没有出度的强连通分量则答案为 \(0\) 。

Code


P2746 [USACO5.3]校园网Network of Schools

缩点。

第一问显然是没有入度的点数。

第二问我们要加入最小的边使得整个图缩成一个强连通分量。直接贪心,每次从没有出度的点连向没有入度的点,那么答案就是没有出度和没有入度的点数的较大值。

Code


P1407 [国家集训队]稳定婚姻

原配男向女连边,新配女向男连边,如果成环说明环中的婚姻都不稳定。

快速判断原配是否在同一个环中,直接求强连通分量即可。

Code


P2272 [ZJOI2007]最大半连通子图

题面看上去非常高深,实际就是缩点后的最长路和最长路条数。

直接\(\rm DP\)即可。

Code


P3225 [HNOI2012]矿场搭建

点双连通分量。

不同于强连通分量,一个割点可以属于多个不同的双连通分量,一个双连通分量可以包含多个割点。

我们可以先一遍\(\rm DFS\)求出所有割点,然后对所有不是割点的点\(\rm DFS\)求得每个双连通分量。

Code


P5058 [ZJOI2004]嗅探器

求使得\(A,B\)不连通的割点数量。

我们以\(A\)为根建立\(\rm DFS\)树。

无向图的\(\rm DFS\)树中,任何非树边的两端都是祖孙关系。

所以我们只用求 \(A\) 到 \(B\) 的路径上的所有的割点即可。

Code


P2515 [HAOI2010]软件安装

基环树上背包。

先\(\rm Tarjan\)缩点,缩点后一定是一棵树。然后树形背包即可。

Code


P4782 【模板】2-SAT 问题

\(\rm 2-SAT\) 是强连通分量的经典应用。

对于每个变量我们拆成 \(x_i\) 和 \(\neg x_i\) 两个点。\(x,y\)二者中至少有一个为真,等价于\(\neg x \to y\ \ \land\ \neg y \to x\) 。

如果成环则说明无解,否则一定有解。

如何构造解?我们将缩点后的图进行拓扑排序,如果 \(x\) 在 \(\neg x\) 前面,不可能有\(\neg x \to x\) 的路径,\(x\) 为假,否则 \(x\) 为真。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 2000005
using namespace std;
int n,m,x,h[N],tot;
struct edge{
	int to,nxt;
}e[N<<1];
void add(int x,int y){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;
}
int get(int x,int y){
	return x+y*n;
}
int idx,dfn[N],low[N],v[N],sta[N],top,col[N],cnt;
void dfs(int x){
	dfn[x]=low[x]=++idx;v[x]=1;sta[++top]=x;
	for(int i=h[x];i;i=e[i].nxt){
		if(!dfn[e[i].to])dfs(e[i].to),low[x]=min(low[x],low[e[i].to]);
		else if(v[e[i].to])low[x]=min(low[x],dfn[e[i].to]);
	}
	if(low[x]==dfn[x]){
		++cnt;
		while(true){
			int y=sta[top--];
			v[y]=0;col[y]=cnt;
			if(x==y)return;
		}
	}
}
int main(){
	scanf("%d%d",&n,&m);
	rep(i,1,m){
		int x,a,y,b;scanf("%d%d%d%d",&x,&a,&y,&b);
		add(get(x,!a),get(y,b));
		add(get(y,!b),get(x,a));
	}
	rep(i,1,n<<1)if(!dfn[i])dfs(i);
	rep(i,1,n)if(col[i]==col[i+n]){puts("IMPOSSIBLE");return 0;}
	puts("POSSIBLE");
	rep(i,1,n)printf("%d ",col[i]>col[i+n]);putchar('\n');
	return 0;
}

P4171 [JSOI2010] 满汉全席

给定的条件是二者至少选一个,同模板。

Code


P3825 [NOI2017] 游戏

掉紫了,爷青结。

乍一看这不是\(\rm 3-SAT\)问题。

但是我们发现 \(3\) 个中一定有一个不能选,所以仍然是 \(\rm 2-SAT\) 问题。至于任选的变量,我们\(3^d\)枚举一下即可。

这里的模型是若 \(a\) 则 \(b\),即 \(a\to b\) 。但是我们不能只连一条边,还要考虑它的逆反问题,若非 \(b\) 则非 \(a\),即 \(\neg b \to \neg a\)。

Code


P5782 [POI2001] 和平委员会

两者互斥的模型,转换为 \(a\to \neg b\ \ \land\ \ b\to\neg a\) 。

Code


P6378 [PA2010] Riddle

首先在这道题中,一个部分恰好选一个点,等价于一个部分至多选一个点。

考虑反证,如果有满足条件的情况当前部分一个点都不选,那么当前部分的生成子图一定不存在任何边,所以我们任意选择一个点仍然能满足条件。

对于一条边两端至少选一个,转化为 \(\neg x \to y\ \ \land\ \neg y \to x\) 。

对于一个部分至多选一个的条件,转化为 \(x \to \neg y_1\ \ \land\ \ x \to \neg y_2\ \ \land\ \ \cdots\ \ \land\ \ x \to \neg y_w\) 。

之间建图是 \(\rm O(N^2)\) 级别的,所以考虑数据结构优化建图。

由于\(\sum k=n\),所以直接前后缀优化建图即可。

当然线段树/倍增优化建图也不是问题。

Code

上一篇:【复习笔记】2021-12 字符串


下一篇:homebrew常用命令