【笔记】联通分量/2-SAT
\(\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;
}
\(\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\)。
P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G
缩点,唯一一个没有出度的强连通分量中的奶牛为答案。如果有多个没有出度的强连通分量则答案为 \(0\) 。
P2746 [USACO5.3]校园网Network of Schools
缩点。
第一问显然是没有入度的点数。
第二问我们要加入最小的边使得整个图缩成一个强连通分量。直接贪心,每次从没有出度的点连向没有入度的点,那么答案就是没有出度和没有入度的点数的较大值。
原配男向女连边,新配女向男连边,如果成环说明环中的婚姻都不稳定。
快速判断原配是否在同一个环中,直接求强连通分量即可。
题面看上去非常高深,实际就是缩点后的最长路和最长路条数。
直接\(\rm DP\)即可。
点双连通分量。
不同于强连通分量,一个割点可以属于多个不同的双连通分量,一个双连通分量可以包含多个割点。
我们可以先一遍\(\rm DFS\)求出所有割点,然后对所有不是割点的点\(\rm DFS\)求得每个双连通分量。
求使得\(A,B\)不连通的割点数量。
我们以\(A\)为根建立\(\rm DFS\)树。
无向图的\(\rm DFS\)树中,任何非树边的两端都是祖孙关系。
所以我们只用求 \(A\) 到 \(B\) 的路径上的所有的割点即可。
基环树上背包。
先\(\rm Tarjan\)缩点,缩点后一定是一棵树。然后树形背包即可。
\(\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;
}
给定的条件是二者至少选一个,同模板。
掉紫了,爷青结。
乍一看这不是\(\rm 3-SAT\)问题。
但是我们发现 \(3\) 个中一定有一个不能选,所以仍然是 \(\rm 2-SAT\) 问题。至于任选的变量,我们\(3^d\)枚举一下即可。
这里的模型是若 \(a\) 则 \(b\),即 \(a\to b\) 。但是我们不能只连一条边,还要考虑它的逆反问题,若非 \(b\) 则非 \(a\),即 \(\neg b \to \neg a\)。
两者互斥的模型,转换为 \(a\to \neg b\ \ \land\ \ b\to\neg a\) 。
首先在这道题中,一个部分恰好选一个点,等价于一个部分至多选一个点。
考虑反证,如果有满足条件的情况当前部分一个点都不选,那么当前部分的生成子图一定不存在任何边,所以我们任意选择一个点仍然能满足条件。
对于一条边两端至少选一个,转化为 \(\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\),所以直接前后缀优化建图即可。
当然线段树/倍增优化建图也不是问题。