强连通分量与2-SAT

强连通分量与2-SAT问题

强连通分量

思路的话,因为环肯定是一个强连通分量,那么我们的思路就在于不断找到构成环的节点,于是可以把访问中的节点标记,再次访问则表明有环,然后为了不重复统计,我们选择了整个强连通分量里\(DFS\)序最小的一个作为代表,栈则起到了存储作用

板子

void tarjan(int u){
	t.push(u);vis[u]=1;//t是栈
	dfn[u]=low[u]=++num;
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(vis[v])low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u]){
		int v;
		scc_cnt++;
		do{
			v=t.top();t.pop();
			vis[v]=0;scc[scc_cnt].push_back(v);
			c[v]=scc_cnt;siz[scc_cnt]++;
		}while(u!=v);
	}
}

DAG的必经点必经边

对于一个\(DAG\)的图\(G\),建立反图\(G'\),对于\(s,t\)路径上的必经点必经边可以如下处理:在\(G\)上进行动态规划求出\(fs\)数组,表示该节点到\(s\)的路径条数,\(G'\)上进行动态规划求出\(ft\)数组,表示该节点到\(t\)的路径条数,那么对于一个节点\(x\),若\(fs[x]*ft[x]=ft[s]\),则\(x\)为必经点,若对于任意边\((x,y)\),若\(fs[x]*ft[y]=fs[t]\),则\((x,y)\)为必经边。

由于路径条数非常大,一般会将其取模

2-SAT

2-SAT问题是这样的,给定\(n\)个形如若\(P\)\(Q\)的命题,每个变量有两种取值,求是否存在一种取值满足所有条件.需要注意的是\(若P则Q蕴含着若非Q则非P\)。对于这个问题的求解,主要方法是:对于每个点的两种取值,分别扩展一倍点,\([1,n]\)分别对应\([n+1,2n]\),设命题\(p\)的逆否命题为\(s[p]\)则对于限制条件\(p->q\)连边\((p,q),(s[q],s[p])\)(和逆否命题也得建边),那么这个图论关系:从一个节点出发能够到达的节点即为若此节点代表的取值成立,则其余都成立.那么此时如果\(x,s[x]\)在同一个强连通分量中,即代表无解(自相矛盾)

当有解的情况,考虑如何给出一组合法的解

显然,同一个强连通分量的取值应该是绑在一起的,并且一个强连通分量会影响它所链接的所有强连通分量,考虑零出度的强连通分量不会影响任何强连通分量,那么可以一步步的按照拓扑排序来不断找零出度的强连通分量更新即可,那么我们在Tarjan算法中求出的强连通分量本就满足拓扑序,编号小的肯定先确定,所以直接比较两种取值所在强连通分量的编号大小即可
模版代码:

void add(int u,int v){
	nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void tarjan(int u){
	t.push(u);vis[u]=1;
	dfn[u]=low[u]=++num;
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(vis[v])low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u]){
		int v;
		cnt++;
		do{
			v=t.top();t.pop();
			vis[v]=0;
			c[v]=cnt;
		}while(u!=v);
	}
}
//在main函数中
    for(int i=1;i<=n<<1;i++)if(!dfn(i))tarjan(i);
    for(int i=1;i<=n;i++){
			if(c[i]==c[s[i]]){
				puts("无解");
				return 0;
			}
		}
    for(int i=1;i<=n;i++){  
        val[i]=c[i]>c[opp[i]];//取i+n*val[i]
    }

题目

杀人游戏

[中山市选]杀人游戏
题目描述一位冷血的杀手潜入Na-wiat,并假装成平民。警察希望能在\(N\)个人里面,查出谁是杀手。警察能够对每一个人进行查证,假如查证的对象是平民,他会告诉警察,他认识的人,谁是杀手,谁是平民。假如查证的对象是杀手,杀手将会把警察干掉。现在警察掌握了每一个人认识谁。每一个人都有可能是杀手,可看作他们是杀手的概率是相同的。
问:根据最优的情况,保证警察自身安全并知道谁是杀手的概率最大是多少?
输入格式
第一行有两个整数 \(N,M\)
接下来有 \(M\) 行,每行两个整数 \(x,y\),表示 \(x\) 认识 \(y\)\(y\) 不一定认识 \(x\) ,例如President同志) 。
注:原文zz敏感内容已替换
输出格式
仅包含一行一个实数,保留小数点后面 \(6\) 位,表示最大概率。
样例 #1

样例输入 #1

5 4 
1 2 
1 3 
1 4 
1 5

样例输出 #1

0.800000

提示

警察只需要查证\(1\)。假如\(1\)是杀手,警察就会被杀。假如\(1\)不是杀手,他会告诉警察\(2,3,4,5\)谁是杀手。而\(1\)是杀手的概率是\(0.2\),所以能知道谁是杀手但没被杀的概率是\(0.8\)

对于\(100\%\)的数据有\(1≤N≤100000,0≤M≤300000\)
题意简述:给定一张有向图上有\(n\)个点,每个点都有同等的概率为黑点,黑点只有一个,从一个节点出发可以知道所有可达点度数,求确定黑点的概率。

如何想到图论建模呢,事实上,当我们面对类似于这种单向的二元关系问题,并且需要借助关系确定某些信息时,便可以想图论建模的思路

那么梳理一下思路,如果我们将每个人认识的人进行连一条有向边,记作\((u,v)\),那么我们再来考虑这张图

很明显,如果若干个点处于同一个\(SCC\)之中,问哪一个元素都是一样的,这启发我们将图缩点,变成一个\(DAG\),因为是在最优情况下,于是我们可以思考如何用最少的次数确定每一个元素

引理

一般情况下,确定每一个元素的颜色,最少的操作次数一定为零入度点的个数,
先证必要性:

显然,零入度点不可能被其他点所确定,故至少需要零入度点个数的操作次数才能覆盖整张图

再证充分性:

一个点\(v\)被覆盖当且仅当至少存在一条边\((u,v)\)\(u\)被覆盖,那么我们对于每一个点类似递归思想一直追溯到不存在这样的边,此时这个最后追溯到的点确定之后\(v\)也就确定了,故数学归纳法易证

然后我们来思考有无特殊情况,考虑原图中的一个点\(p\)在怎样的情况下才可以不选择而被确定。若\(p\)点是一个孤立点,亦或者它所连的点的度数均大于等于2,此时这个点就可以先不急着需要它的颜色,那么我们可以将其放在最后解决,那么在所有满足要求的点\(p\)中,有一个点会因为其他所有点都被确定了而无需确定,这样就可以少选一次
\(QED.\)

结合引理,我们得到了本题的算法流程

  1. 建图,执行缩点
  2. 对于缩点后的\(DAG\),统计零入度点数量,记为\(c\),统计是零入度点,所在强连通分量大小为1并且满足其所连接的强连通分量的入度均大于1的\(SCC\)数量,记为\(p\)
  3. 最终答案为:\(ans=\frac{n-c+min(p,1)}{n}\)

对于本题需要注意的点是,在缩点建立新图的时候,很有可能出现重边的情况,大部分使用\(map\)进行优化,速度较慢,这里有一个更快的方式:

注意我们的\(Tarjan\)已经可以求出每一个\(SCC\)包含哪些点了,我们可以设一个大小为\(n\)的一维数组\(vis\),对于编号为\(i\in[1,cnt]\)的强连通分量进行考虑

  1. 清空\(vis\),这一步可以开一个\(vector\)记录上一个强连通分量所连接的强连通分量进行撤销,保证复杂度
  2. 遍历第\(i\)个强连通分量的元素\(u\),对\(u\)执行操作3
  3. 遍历\(u\)的所有出边,在同一个强连通分量的不管,不在同一个强连通分量的,设后继点为\(v\),则若\(vis[c[v]]\)未曾标记,将其入度加一并标记,若被标记,则不管

时间复杂度\(O(M)\)

本题总时间复杂度:\(O(M+N)=O(M)\)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
#include<stack>
using namespace std;
#define N 150000
#define M 650000
int head[N],n,m,ans,p,num,ver[M],nxt[M],c[N],siz[N],in[N],cnt[N],tot,scc_cnt,vis[N],dfn[N],low[N],s[N],inn[N];
int shead[N],sver[M],snxt[M],stot;
void add_s(int u,int v){
	snxt[++stot]=shead[u],sver[shead[u]=stot]=v;
}
vector<int>scc[N];
stack<int>t;
void add(int u,int v){
	nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void tarjan(int u){
	t.push(u);vis[u]=1;
	dfn[u]=low[u]=++num;
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(vis[v])low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u]){
		int v;
		scc_cnt++;
		do{
			v=t.top();t.pop();
			vis[v]=0;scc[scc_cnt].push_back(v);
			c[v]=scc_cnt;siz[scc_cnt]++;
		}while(u!=v);
	}
}
bool check(int a){
	for(int i=0;i<scc[a].size();i++){
		int u=scc[a][i];
		for(int i=head[u];i;i=nxt[i]){
			int v=ver[i];
			if(c[u]==c[v])continue;
			if(in[c[v]]<2)return true;
		}
	}
	return false;
}
void solve(){
	if(n==1){
		printf("1.000000\n");
		return ;
	};
	for(int i=1;i<=scc_cnt;i++){
		int num=0;
		cnt[i]=1;
		for(int j=0;j<siz[i];j++){
			int u=scc[i][j];
			for(int k=head[u];k;k=nxt[k]){
				if(cnt[c[ver[k]]])continue;
				in[c[ver[k]]]++;
				cnt[c[ver[k]]]=1;
				s[++num]=c[ver[k]];
			}
		} 
		cnt[i]=0;
		while(num)cnt[s[num--]]=0;
	}
	int ans1=0,flag=1;
	for(int i=1;i<=scc_cnt;i++){
		if(in[i]==0){
			ans1++;
		}
	}
	if(ans1==1){
		printf("%.6f\n",1.0-1.0/n);
		return ;
	}
	for(int i=1;i<=scc_cnt;i++)if(in[i]==0&&!check(i)){ans1--;break;}
	printf("%.6f\n",1.0*(n-ans1)/n);
	return ;
}
void init(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		add(u,v);
		inn[v]++;
	}
	for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
	solve();
}
int main(){
	init();
}

例题:银河

银河中的恒星浩如烟海,但是我们只关注那些最亮的恒星。

我们用一个正整数来表示恒星的亮度,数值越大则恒星就越亮,恒星的亮度最暗是 1。

现在对于 N 颗我们关注的恒星,有 M 对亮度之间的相对关系已经判明。

你的任务就是求出这 N 颗恒星的亮度值总和至少有多大。

输入格式
第一行给出两个整数 N 和 M。

之后 M 行,每行三个整数 T,A,B,表示一对恒星 (A,B) 之间的亮度关系。恒星的编号从 1 开始。

如果 T=1,说明 A 和 B 亮度相等。
如果 T=2,说明 A 的亮度小于 B 的亮度。
如果 T=3,说明 A 的亮度不小于 B 的亮度。
如果 T=4,说明 A 的亮度大于 B 的亮度。
如果 T=5,说明 A 的亮度不大于 B 的亮度。

思路:这是一个很明显的差不大于1的差分约束系统,考虑借助这个最优性质进行优化。不妨思考,因为只有大/等于的会向小的连边,并且构成了一个强连通分量,即代表这些点取值相同,那么题目就变为了求强连通分量,然后缩点差分约束。,因为缩点后是DAG,所以直接可以从0出度点开始一步步往回拓扑排序

#include<iostream>
#include<cstdio>
#include<stack>
#include<vector>
#include<cstring>
using namespace std;
#define N 1005000 
#define M 6005000
#define int long long
int tot,cnt,head[N],nxt[M],ver[M],n,m,dfn[N],num,low[N],c[N],in[N],out[N],vis[N],cost[M],siz[N],d[N],ans;
int shead[N],snxt[M],sver[M],stot,scost[M]; 
stack<int>t;
void add(int u,int v,int w){
	nxt[++tot]=head[u],ver[head[u]=tot]=v,cost[tot]=w;
}
void add_s(int u,int v,int w){
	snxt[++stot]=shead[u],sver[shead[u]=stot]=v,scost[stot]=w;
}
void tarjan(int u){
	dfn[u]=low[u]=++tot;
	t.push(u);
	vis[u]=1;
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(vis[v])low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]){
		int v;cnt++;
		do{
			v=t.top();t.pop();
			c[v]=cnt;vis[v]=0;siz[cnt]++;
		}while(v!=u);
	}
}
bool init(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;i++){
		int u,v,w;
		scanf("%lld%lld%lld",&w,&v,&u);
		if(w==1)add(u,v,0),add(v,u,0);
		if(w==2)add(v,u,1);
		if(w==3)add(u,v,0);
		if(w==4)add(u,v,1);
		if(w==5)add(v,u,0);
	}
	n++;
	for(int i=1;i<n;i++){
		add(n,i,1);
	}
	tarjan(n); 
	for(int u=1;u<=n;u++){
		for(int j=head[u];j;j=nxt[j]){
			int v=ver[j];
			if(c[v]==c[u]){
				if(cost[j]>0)return false;
			}
			else {
				add_s(c[u],c[v],cost[j]);
			}
		}
	}
	d[c[n]]=0;
	for(int u=cnt;u>0;--u){
		for(int i=shead[u];i;i=snxt[i]){
			int v=sver[i];
			d[v]=max(d[v],d[u]+scost[i]);
		}
	}
	for(int i=1;i<=cnt;i++)ans+=d[i]*siz[i];
	printf("%lld",ans);
	return true;
}
signed main(){
	if(!init())printf("-1");
	return 0;
}

例题3.北大ACM队的远足

链接

给定一张 N 个点 M 条边的有向无环图,点的编号从 0 到 N−1,每条边都有一个长度。

给定一个起点 S 和一个终点 T。

若从 S 到 T 的每条路径都经过某条边,则称这条边是有向图的必经边或桥。

北大 ACM 队要从 S 点到 T 点。

他们在路上可以搭乘两次车。

每次可以从任意位置(甚至是一条边上的任意位置)上车,从任意位置下车,但连续乘坐的长度不能超过 q 米。

除去这两次乘车外,剩下的路段步行。

定义从 S 到 T 的路径的危险程度等于步行经过的桥上路段的长度之和。

求从 S 到 T 的最小危险程度是多少。

首先求\(DAG\)的桥可以使用路径条数取模法,防止冲突的话用孪生素数\(10^9+7,10^9+9\),很明显的是最短路一定不会比其他路更差,于是我们求出一条最短路,问题就变成了用两条长度为\(q\)的线段在这条最短路上扫(最短路一定是一条链),使得扫到的桥的长度最大。

但因为\(S->T\)的最短路不止一条,于是我们现在来证明选择其中任意一条即可

引理1:任意两条最短路经过的桥的顺序相同

证明:反证法,如果不相同,我们假设最短路1是从\(i\)\(j\),最短路2是从\(j\)\(i\),那么一定存在一条从\(i\)\(j\)的路径,也一定存在一条从\(j\)\(i\)的路径,这就形成了环,与DAG定义不符,故假设不成立,原命题成立

引理2:任意两条最短路经过的两个桥中间相隔的距离相同

证明:反证法,假设最短路1经过的距离为\(D1\),最短路2经过的距离为\(D2\),不妨设\(D1>D2\),那么最短路2可以用\(D1\)替换掉\(D2\),这与最短路的定义不符,故假设不成立,原命题成立

由此,我们任选一条最短路即可,至于\(DAG\)的最短路,可以直接拓扑排序,\(DP\)求出,然后\(pre\)递归回溯即可。顺带我们还可以求出路径条数以寻找桥

现在我们将最短路抽离出来单独放进数组里面,并且做一个路径长度前缀和,桥的长度前缀和,这时候这条最短路就成为了一条线段,其上有若干个点
那么我们回到解决这个问题:求用两条长度为\(q\)的线段在这条最短路上扫(最短路一定是一条链),使得扫到的桥的长度最大。

这个扫描,因为两条不太好处理,我们不妨考虑如果只有一条的情况。设\(ds[i]\)表示线段上从第一个点到第\(i\)个点用一条\(q\)所能覆盖掉桥的最大长度

考虑转移:这里的\(i\)表示边\((i-1,i)\)

若这条边不是必经边,直接从\(ds[i-1]\)继承
若这条边是必经边,那么我们令这条覆盖线段的右端点放在\(i\),此时它覆盖桥的最大长度为\(q-dis(i,j)+len(i,j)\),其中\(dis\)表示两点距离,\(len\)表示两点间桥的总长度,\(j\)是满足\(dis(i,j)\le q\)并且\((j-1,j)\)是桥的最小的\(j\),很明显\(j\)具有单调性,所以我们可以采用双指针扫描的方式更新答案,类似的我们处理出\(dt[i]\)表示线段上从第\(i\)个点到最后个点用一条\(q\)所能覆盖掉桥的最大长度,此时我们便可以枚举断边\((i-1,i)\)了,更新答案即可

最后我们还需要考虑两个线段连在一起的情况,类比上面以一条长度为\(2q\)的线段扫就是

#include<bits/stdc++.h>
using namespace std;
const int N = 100005, M = 200005, mod = 1000000007;
int ver[M*2], edge[M*2], nxt[M*2], head[N], tot;
int f[2][N], deg[2][N], d[N], pre[N], n, m, s, t, bus;
bool bridge[M*2];
int a[N], b[N], cnt; // 长度、是不是桥
int sum[N], sum_bri[N], ds[N], dt[N], ds_min[N];
int occur[N], first_occur[N];
queue<int> q;

void add(int x, int y, int z) {
    ver[++tot] = y, edge[tot] = z, nxt[tot] = head[x], head[x] = tot;
}

void topsort(int s, int bit) {
    if (bit == 0) { // 只有正图需要求最短路
        memset(d, 0x3f, sizeof(d));
        d[s] = 0;
    }
    f[bit][s] = 1;
    for (int i = 1; i <= n; i++)
        if (deg[bit][i] == 0) q.push(i);
    while (!q.empty()) {
        int x = q.front(); q.pop();
        for (int i = head[x]; i; i = nxt[i])
            if ((i & 1) == bit) {
                int y = ver[i];
                f[bit][y] = (f[bit][y] + f[bit][x]) % mod; // 路径条数
                if (bit == 0 && d[y] > d[x] + edge[i]) {  // 最短路
                    d[y] = d[x] + edge[i];
                    pre[y] = i;
                }
                if (--deg[bit][y] == 0) q.push(y);
            }
    }
}

int main() {
    int C; cin >> C;
    while (C--) {
        memset(head, 0, sizeof(head));
        memset(deg, 0, sizeof(deg));
        memset(f, 0, sizeof(f));
        memset(bridge, 0, sizeof(bridge));
        memset(occur, 0, sizeof(occur));
        tot = 1; cnt = 0;
        cin >> n >> m >> s >> t >> bus;
        s++; t++;
        for (int i = 1; i <= m; i++) {
            int x, y, z;
            scanf("%d%d%d", &x, &y, &z);
            x++, y++;
            add(x, y, z); // 偶数边是正边(邻接表2, 4, 6,...位置)
            add(y, x, z); // 奇数边是反边
            deg[0][y]++; // 入度
            deg[1][x]++; // 出度
        }
        topsort(s, 0);
        if (f[0][t] == 0) { puts("-1"); continue; }
        topsort(t, 1);
        for (int i = 2; i <= tot; i += 2) {
            int x = ver[i ^ 1], y = ver[i];
            if ((long long)f[0][x] * f[1][y] % mod == f[0][t]) {
                bridge[i] = true;
            }
        }
        // O(M)判重边,用map可能超时
        for (int x = 1; x <= n; x++) {
            for (int i = head[x]; i; i = nxt[i]) {
                if (i & 1) continue; // 只考虑正边
                int y = ver[i];
                if (occur[y] == x) {
                    bridge[i] = false;
                    bridge[first_occur[y]] = false;
                } else {
                    occur[y] = x;
                    first_occur[y] = i;
                }
            }
        }
        while (t != s) {
            a[++cnt] = edge[pre[t]];
            b[cnt] = bridge[pre[t]];
            t = ver[pre[t] ^ 1];
        }
        // reverse(a + 1, a + cnt + 1); 不反过来也可以
        // reverse(b + 1, b + cnt + 1);
        for (int i = 1; i <= cnt; i++) {
            sum[i] = sum[i - 1] + a[i]; // 以i这条边为结尾(包含i)的前缀总长度
            sum_bri[i] = sum_bri[i - 1] + (b[i] ? a[i] : 0);
        }
        ds_min[0] = 1 << 30;
        for (int i = 1, j = 0; i <= cnt; i++) { // 恰好在i这条边的结尾处下车,前面的最小危险程度:ds[i]
            // 双指针扫描,让j+1~i这些边乘车,j这条边有可能部分乘车
            while (sum[i] - sum[j] > bus) j++;
            ds[i] = sum_bri[j];
            if (j > 0 && b[j]) ds[i] -= min(a[j], bus - (sum[i] - sum[j]));
            ds_min[i] = min(ds[i], ds_min[i - 1] + (b[i] ? a[i] : 0)); // i之前搭一次车:ds_min[i],即书上的"ds[i]"
        }
        for (int i = cnt, j = cnt + 1; i; i--) { // 恰好在i这条边的开头处上车,后面的最小危险程度:ds[i]
            // 双指针扫描,让i~j-1这些边乘车,j这条边有可能部分乘车
            while (sum[j - 1] - sum[i - 1] > bus) j--;
            dt[i] = sum_bri[cnt] - sum_bri[j - 1];
            if (j <= cnt && b[j]) dt[i] -= min(a[j], bus - (sum[j - 1] - sum[i - 1]));
        }
        // 两段乘车分开的情况
        int ans = 1 << 30;
        for (int i = 1; i <= cnt; i++)
            ans = min(ans, dt[i] + ds_min[i - 1]);
        // 两段乘车接在一起,2*bus覆盖一次的情况
        for (int i = 1, j = 0; i <= cnt; i++) {
            while (sum[i] - sum[j] > 2 * bus) j++;
            int temp = sum_bri[j];
            if (j > 0 && b[j]) temp -= min(a[j], 2 * bus - (sum[i] - sum[j]));
            ans = min(ans, temp + sum_bri[cnt] - sum_bri[i]);
        }
        cout << ans << endl;
    }
}

一些总结。

  • \(col_{i}<col_{i+1}\),变量为真。
  • 平面图欧拉定理
posted @ 2022-11-30 22:40  spdarkle  阅读(33)  评论(0编辑  收藏  举报