强连通分量

\(\text{hdu-1269}\)

给定 \(n\) 个点 \(m\) 条边的有向图,判断是否任意两点都能互相到达。

\(1 \le n \le 10^4\)\(1 \le m \le 10^5\)


强连通分量的模板题。

\(\text{Tarjan}\) 求强连通分量,等价于判断强连通分量是否只有一个且大小为 \(n\)

注意:需要清空的变量比较多,别漏了。

#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
#define MAXN 10005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, dfn[MAXN], low[MAXN], dn, s[MAXN], is[MAXN], top;
long long scc[MAXN], cnt, si[MAXN];
vector<long long> v[MAXN];

void tarjan(long long x) {
	low[x] = dfn[x] = ++ dn, s[++ top] = x, is[x] = 1;
	for(auto y : v[x])
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(is[y]) low[x] = min(low[x], dfn[y]);
	if(dfn[x] == low[x]) {
		cnt ++;
		do { 
			scc[s[top]] = cnt, si[cnt] ++, is[s[top]] = 0; 
		} while(s[top --] != x);
	}
	return;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	while(cin >> n >> m) {
		if(!n && !m) break;
		memset(low, 0, sizeof low);
		memset(dfn, 0, sizeof dfn);
		memset(is, 0, sizeof is);
		memset(si, 0, sizeof si);
		top = cnt = dn = 0;
		for(int i = 1; i <= n; i ++) v[i].clear();
		for(int i = 1; i <= m; i ++) {
			long long x, y; cin >> x >> y;
			v[x].push_back(y);
		}
		for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i);
		if(cnt == 1 && si[cnt] == n) cout << "Yes\n";
		else cout << "No\n"; 
	}
	return 0;
}

\(\text{hdu-1827}\)

给定 \(n\) 个人的通知费用,以及他们可联系的关系,联系关系是单向的。

现在需要给所有人通知事情,求最少通知多少人使得消息互相传递之后所有人都能收到通知。

且使得通知费用最少。

\(1 \le n \le 1000\)\(1 \le m \le 2000\)


考虑同一个强连通分量中通知任意一个人效果相同,把同一个强连通分量缩成一个点。

那么只用通知入度为 \(0\) 的人即可,每个点的费用是强连通分量中的最小值。

#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
#define MAXN 1005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, a[MAXN], dfn[MAXN], low[MAXN], dn, s[MAXN], is[MAXN];
long long top, cnt, scc[MAXN], si[MAXN], minn[MAXN], d[MAXN];
vector<long long> v[MAXN];

void tarjan(long long x) {
	low[x] = dfn[x] = ++ dn, s[++ top] = x, is[x] = 1;
	for(auto y : v[x]) 
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(is[y]) low[x] = min(low[x], dfn[y]);
	if(dfn[x] == low[x]) {
		cnt ++;
		do {
			scc[s[top]] = cnt, si[cnt] ++, is[s[top]] = 0;
		} while(s[top --] != x);
	} 
	return;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	while(cin >> n >> m) {
		memset(low, 0, sizeof low);
		memset(dfn, 0, sizeof dfn);
		memset(is, 0, sizeof is);
		memset(si, 0, sizeof si);
		memset(minn, 0x3f, sizeof minn);
		memset(d, 0, sizeof d);
		top = cnt = dn = 0;
		for(int i = 1; i <= n; i ++) cin >> a[i];
		for(int i = 1; i <= n; i ++) v[i].clear();
		for(int i = 1; i <= m; i ++) {
			long long x, y; cin >> x >> y;
			v[x].push_back(y);
		}
		for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i);
		long long res = 0, ans = 0;
		for(int i = 1; i <= n; i ++) 
			minn[scc[i]] = min(minn[scc[i]], a[i]);
		for(int i = 1; i <= n; i ++) 
			for(auto j : v[i]) if(scc[i] != scc[j]) d[scc[j]] ++;
		for(int i = 1; i <= cnt; i ++) if(!d[i]) res ++, ans += minn[i];	
		cout << res << " " << ans << "\n";
	}
	return 0;
}

\(\text{hdu-2767}\)

给定 \(n\) 个点和 \(m\) 条边的有向图,求添加最少的边数使得图变成强连通图。

\(1 \le n \le 2 \times 10^4\)\(0 \le m \le 5 \times 10^4\)


首先对于原图的强连通分量显然是无影响的,所以先缩点。

如果想要使图成为强连通图,那么显然不可能有点入度或者出度为 \(0\)。考虑连一条边可以使图减少一个入度为 \(0\) 的点和一个出度为 \(0\) 的点。这样连边一定可以使这两个点加入一个强连通分量中,于是我们目标是把所有点都加入强连通分量中使得图成为强连通图,答案即为入度为 \(0\) 的点数和出度为 \(0\) 的点数取 \(\max\)

#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
#define MAXN 20005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long T, n, m, a[MAXN], dfn[MAXN], low[MAXN], dn, s[MAXN], is[MAXN], top, cnt;
long long scc[MAXN], si[MAXN], minn[MAXN], in[MAXN], out[MAXN];
vector<long long> v[MAXN];

void tarjan(long long x) {
	low[x] = dfn[x] = ++ dn, s[++ top] = x, is[x] = 1;
	for(auto y : v[x]) 
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(is[y]) low[x] = min(low[x], dfn[y]);
	if(dfn[x] == low[x]) {
		cnt ++;
		do {
			scc[s[top]] = cnt, si[cnt] ++, is[s[top]] = 0;
		} while(s[top --] != x);
	} 
	return;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> T;
	while(T --) {
		memset(low, 0, sizeof low);
		memset(dfn, 0, sizeof dfn);
		memset(is, 0, sizeof is);
		memset(si, 0, sizeof si);
		memset(in, 0, sizeof in);
		memset(out, 0, sizeof out);
		top = cnt = dn = 0; cin >> n >> m;
		for(int i = 1; i <= n; i ++) v[i].clear();
		for(int i = 1; i <= m; i ++) {
			long long x, y; cin >> x >> y;
			v[x].push_back(y);
		}
		for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i);
		if(cnt == 1) { cout << "0\n"; continue; }
		long long res1 = 0, res2 = 0;
		for(int i = 1; i <= n; i ++) for(auto j : v[i]) 
			if(scc[i] != scc[j]) in[scc[j]] ++, out[scc[i]] ++;
		for(int i = 1; i <= cnt; i ++) 
			res1 += (!in[i]), res2 += (!out[i]);
		cout << max(res1, res2) << "\n";
	}
	return 0;
}

\(\text{loj-10091 / luogu-2341}\)

每一头牛的愿望就是变成一头最受欢迎的牛。现在有 \(N\) 头牛,给你 \(M\) 对整数 \((A,B)\),表示牛 \(A\) 认为牛 \(B\) 受欢迎。这种关系是具有传递性的,如果 \(A\) 认为 \(B\) 受欢迎,\(B\) 认为 \(C\) 受欢迎,那么牛 \(A\) 也认为牛 \(C\) 受欢迎。你的任务是求出有多少头牛被除自己之外的所有牛认为是受欢迎的。

\(1 \le N \le 10^4\)\(1 \le M \le 5 \times 10^4\)


很容易想到先缩点,最终的唯一一个叶子结点里面的所有奶牛都被其他牛喜欢。

如果有大于等于两个叶子的话,那些叶子节点的奶牛不会被其他叶子节点的奶牛喜欢,答案为 \(0\)

模版缩点跑完之后,找叶子结点包含多少只奶牛即可,对于叶子节点数不为 \(1\) 的答案特判为 \(0\)

注意:缩点之后统计出度的时候需要注意,不要统计多了。

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define MAXN 10005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, dfn[MAXN], low[MAXN], dn, s[MAXN], d[MAXN];
long long top, is[MAXN], cnt, scc[MAXN], sz[MAXN];
vector<long long> v[MAXN];

void tarjan(long long x) {
	low[x] = dfn[x] = ++ dn, s[++ top] = x, is[x] = 1;
	for(auto y : v[x]) 
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(is[y]) low[x] = min(low[x], dfn[y]);
	if(dfn[x] == low[x]) {
		cnt ++; 
		do {
			scc[s[top]] = cnt, sz[cnt] ++, is[s[top]] = 0;
		} while(s[top --] != x);
	}
	return;
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= m; i ++) {
		long long x = read(), y = read();
		v[x].push_back(y);
	}
	for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i);
	for(int i = 1; i <= n; i ++) for(auto j : v[i]) 
		if(scc[i] != scc[j]) d[scc[i]] ++;
	long long res = 0, ans = 0; 
	for(int i = 1; i <= cnt; i ++) if(!d[i]) res ++, ans = sz[i];
	if(res > 1) cout << "0\n";
	else cout << ans << "\n";
	return 0;
}

\(\text{loj-10092 / luogu-2272}\)

一个有向图 \(G = (V,E)\) 称为半连通的,如果满足:\(\forall u,v\in V\),满足 \(u\to v\)\(v\to u\),即对于图中任意两点 \(u,v\),存在一条 \(u\)\(v\) 的有向路径或者从 \(v\)\(u\) 的有向路径。

\(G'=(V',E')\) 满足,\(E’\)\(E\) 中所有和 \(V’\) 有关的边,则称 \(G’\)\(G\) 的一个导出子图。若 \(G’\)\(G\) 的导出子图,且 \(G’\) 半连通,则称 \(G’\)\(G\) 的半连通子图。若 \(G’\)\(G\) 所有半连通子图中包含节点数最多的,则称 \(G’\)\(G\) 的最大半连通子图。

给定一个有向图 \(G\),请求出 \(G\) 的最大半连通子图拥有的节点数 \(K\),以及不同的最大半连通子图的数目 \(C\)。由于 \(C\) 可能比较大,仅要求输出 \(C\)\(X\) 的余数。

\(1 \le N \le 10^5\)\(1 \le M \le 10^6\)\(X \le 10^8\)


首先缩点嘛,因为强连通分量一定是半联通的。

其次,对于一条链,肯定是半连通图,从入度为 \(0\) 的点开始,依次可以遍历到它之后的点。

于是考虑缩点之后拓扑排序,拓扑过程中 \(\text{dp}\) 求最长链长度和最长链个数。

需要注意的是,在缩完点建新图前将每条边记录下来,去重,防止个数记录重复。

注意:链式前向星大小要和 \(M\) 同阶而非和 \(N\)

#include<iostream>
#include<cstdio>
#include<vector>
#include<map>
#include<queue>
#include<cstring>
using namespace std;
#define MAXN 1000005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, MOD, dfn[MAXN], low[MAXN], dn, s[MAXN], d[MAXN], top, is[MAXN];
long long cnt, scc[MAXN], sz[MAXN], hd[MAXN], tot, dp[MAXN], f[MAXN];
struct node { long long to, nxt; } e[MAXN];
map<long long, map<long long, bool> > mp;
vector<long long> v[MAXN];
queue<long long> q;

void add(long long x, long long y) {
	e[++ tot] = {y, hd[x]};
	hd[x] = tot; return;
}

void tarjan(long long x) {
	low[x] = dfn[x] = ++ dn, s[++ top] = x, is[x] = 1;
	for(auto y : v[x]) 
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(is[y]) low[x] = min(low[x], dfn[y]);
	if(dfn[x] == low[x]) {
		cnt ++; 
		do {
			scc[s[top]] = cnt, sz[cnt] ++, is[s[top]] = 0;
		} while(s[top --] != x);
	}
	return;
}

int main() {
	n = read(), m = read(), MOD = read();
	memset(hd, -1, sizeof hd);
	for(int i = 1; i <= m; i ++) {
		long long x = read(), y = read();
		v[x].push_back(y);
	}
	for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i);
	for(int i = 1; i <= n; i ++) for(auto j : v[i]) {
		long long x = scc[i], y = scc[j];
		if(x != y && !mp[x][y]) 
			add(x, y), mp[x][y] = 1, d[y] ++;
	}
	for(int i = 1; i <= cnt; i ++) if(!d[i])
		q.push(i), dp[i] = sz[i], f[i] ++;
//	cout << dp[5] << " ";
	long long ans = 0, res = 0;
	while(!q.empty()) {
		long long x = q.front(); q.pop();
		ans = max(ans, dp[x]);
		for(int i = hd[x]; i != -1; i = e[i].nxt) {
			long long y = e[i].to; 
			if(dp[y] == dp[x] + sz[y]) f[y] = (f[y] + f[x]) % MOD;
			else if(dp[y] < dp[x] + sz[y])
				dp[y] = dp[x] + sz[y], f[y] = f[x];
			if(!(-- d[y])) q.push(y);
		}
	}
	cout << ans << "\n";
	for(int i = 1; i <= cnt; i ++) 
		if(dp[i] == ans) res = (res + f[i]) % MOD;
	cout << res << "\n";
	return 0;
}

\(\text{loj-10093 / luogu-2746}\)

一些学校连接在一个计算机网络上。学校之间存在软件支援协议。每个学校都有它应支援的学校名单(学校 \(a\) 支援学校 \(b\),并不表示学校 \(b\) 一定支援学校 \(a\))。当某校获得一个新软件时,无论是直接得到还是网络得到,该校都应立即将这个软件通过网络传送给它应支援的学校。因此,一个新软件若想让所有连接在网络上的学校都能使用,只需将其提供给一些学校即可。

  1. 请编一个程序,根据学校间支援协议(各个学校的支援名单),计算最少需要将一个新软件直接提供给多少个学校,才能使软件通过网络被传送到所有学校;
  2. 如果允许在原有支援协议上添加新的支援关系。则总可以形成一个新的协议,使得此时只需将一个新软件提供给任何一个学校,其他所有学校就都可以通过网络获得该软件。编程计算最少需要添加几条新的支援关系。

\(2 \le n \le 100\)


首先第一问,直接缩点之后看入度 \(0\) 的个数即可。

第二问目的就是把缩点之后的图变成所有点构成一个环,那么入度为 \(0\) 的点和出度为 \(0\) 的点连边。

如果个数不一样的话,那么一一连边剩下的也要连,所以至少要 \(\max(in, out)\) 条边。

这种题貌似之前写过一道?

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define MAXN 10005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, dfn[MAXN], low[MAXN], dn, s[MAXN], in[MAXN], out[MAXN];
long long top, is[MAXN], cnt, scc[MAXN];
vector<long long> v[MAXN];

void tarjan(long long x) {
	low[x] = dfn[x] = ++ dn, s[++ top] = x, is[x] = 1;
	for(auto y : v[x]) 
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(is[y]) low[x] = min(low[x], dfn[y]);
	if(dfn[x] == low[x]) {
		cnt ++; 
		do {
			scc[s[top]] = cnt, is[s[top]] = 0;
		} while(s[top --] != x);
	}
	return;
}

int main() {
	n = read();
	for(int i = 1; i <= n; i ++) {
		long long x;
		while(x = read()) v[i].push_back(x);
	}
	for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i);
	for(int i = 1; i <= n; i ++) for(auto j : v[i]) 
		if(scc[i] != scc[j]) out[scc[i]] ++, in[scc[j]] ++;
	long long c1 = 0, c2 = 0;
	for(int i = 1; i <= cnt; i ++) c1 += !in[i], c2 += !out[i];
	cout << c1 << "\n";
	if(cnt == 1) cout << "0\n";
	else cout << max(c1, c2) << "\n";
	return 0;
}

\(\text{loj-10094}\)

我们的郭嘉大大在曹操这过得逍遥自在,但是有一天曹操给了他一个任务,在建邺城内有 \(N\) 个袁绍的奸细,将他们从 \(1\)\(N\) 进行编号,同时他们之间存在一种传递关系,即若\(C_{i,j}=1\),则奸细 \(i\) 能将消息直接传递给奸细 \(j\)

现在曹操要发布一个假消息,需要传达给所有奸细,而我们的郭嘉大大则需要传递给尽量少的奸细使所有的奸细都知道这一个消息,问我们至少要传给几个奸细?

\(1 \le N \le 1000\)


上一题的弱化版,只用求第一问即可。需要改一下输入方式。

\(\text{loj-10095 / luogu-1262}\)

由于外国间谍的大量渗入,国家安全正处于高度的危机之中。如果 A 间谍手中掌握着关于 B 间谍的犯罪证据,则称 A 可以揭发 B。有些间谍收受贿赂,只要给他们一定数量的美元,他们就愿意交出手中掌握的全部情报。所以,如果我们能够收买一些间谍的话,我们就可能控制间谍网中的每一分子。因为一旦我们逮捕了一个间谍,他手中掌握的情报都将归我们所有,这样就有可能逮捕新的间谍,掌握新的情报。

我们的反间谍机关提供了一份资料,包括所有已知的受贿的间谍,以及他们愿意收受的具体数额。同时我们还知道哪些间谍手中具体掌握了哪些间谍的资料。假设总共有 \(n\) 个间谍(\(n\) 不超过 \(3000\)),每个间谍分别用 \(1\)\(3000\) 的整数来标识。

请根据这份资料,判断我们是否有可能控制全部的间谍,如果可以,求出我们所需要支付的最少资金。否则,输出不能被控制的一个间谍。

\(1 \le n \le 3000\)\(1 \le p \le n\)\(1 \le r \le 8000\),收买费用为非负数且不超过 \(20000\)


首先如果答案是 YES,那么答案就是缩点之后的入度为 \(0\) 的点中收买费用的最小值的和。

下面我们来解决答案为 NO 的情况。我们要明确能被控制的间谍有哪几种情况。

  • 对于会被能收买的间谍供出来的,显然是能被控制的。
  • 但是对于能被单独收买的,其实也是能被控制的,即使他不被其他人供出来。

于是我们需要对能被收买的人搜一遍,把搜到的人打上标记,这些人都是能被控制的。

对于没有被打上标记的人即为不能被控制的人,搜一遍看最小编号即可。

细节比较多,注意实现思路。

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define MAXN 10005
#define INF 0x3f3f3f3f3f3f3f3f

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, p, dfn[MAXN], low[MAXN], dn, s[MAXN], in[MAXN], minn[MAXN];
long long top, is[MAXN], cnt, scc[MAXN], f[MAXN], g[MAXN];
vector<long long> v[MAXN], e[MAXN];
bool vis[MAXN];

void tarjan(long long x) {
	low[x] = dfn[x] = ++ dn, s[++ top] = x, is[x] = 1;
	for(auto y : v[x]) 
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(is[y]) low[x] = min(low[x], dfn[y]);
	if(dfn[x] == low[x]) {
		cnt ++; 
		do {
			scc[s[top]] = cnt, is[s[top]] = 0;
		} while(s[top --] != x);
	}
	return;
}

void dfs(long long x) {
	vis[x] = true;
	for(auto y : e[x]) if(!vis[y]) dfs(y);
	return;
}

int main() {
	n = read(), p = read();
	for(int i = 1; i <= n; i ++) f[i] = INF;
	for(int i = 1; i <= p; i ++) {
		long long x = read(), y = read();
		f[x] = y;
	}
	m = read();
	for(int i = 1; i <= m; i ++) {
		long long x = read(), y = read();
		v[x].push_back(y);
	}
	for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i);
	for(int i = 1; i <= n; i ++) for(auto j : v[i]) 
		if(scc[i] != scc[j]) in[scc[j]] ++, e[scc[i]].push_back(scc[j]);
	for(int i = 1; i <= cnt; i ++) g[i] = minn[i] = INF;
	for(int i = 1; i <= n; i ++) 
		minn[scc[i]] = min(minn[scc[i]], 1ll * i), 
		g[scc[i]] = min(g[scc[i]], f[i]);
	long long res = 0, ans = INF, fg = 0;
	for(int i = 1; i <= cnt; i ++) if(!in[i]) 
		if(g[i] == INF) fg = 1;
		else res += g[i];
	if(fg) {
		for(int i = 1; i <= cnt; i ++) 
			if(g[i] != INF) dfs(i);
		for(int i = 1; i <= cnt; i ++) 
			if(!vis[i]) ans = min(ans, minn[i]);
	}
	if(!fg) cout << "YES\n" << res << "\n";
	else cout << "NO\n" << ans << "\n";
	return 0;
}

\(\text{loj-10096}\)

\(n\) 个路口,有 \(p\) 个路口有酒吧,也有 \(m\) 条单向道路连接路口。

每个路口都有一台 ATM,其中有 \(a_i\) 元。你准备从 \(s\) 出发,经过道路抢劫这些 ATM。

一个路口可以重复经过,但一台 ATM 只能抢劫一次。最终,你将会在一个酒吧庆祝胜利。

求你最多能抢劫多少钱。

\(1 \le n,m \le 5 \times 10^5\)\(0 \le a_i \le 4000\)\(1 \le s,p \le n\)


对于一个强连通分量中的点,只要能经过其中一个则所有点都能被抢劫。

于是考虑先缩点,之后就只用处理有向无环图的情况。

直接从 \(s\) 开始跑 \(\text{spfa}\) 即可,因为 \(dfs\) 会超时。注意细节。

#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
using namespace std;
#define MAXN 500005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, dfn[MAXN], low[MAXN], dn, s[MAXN], is[MAXN], top;
long long cnt, scc[MAXN], a[MAXN], b[MAXN], S, P, dis[MAXN];
vector<long long> v[MAXN], e[MAXN];
bool f[MAXN], g[MAXN], vis[MAXN];

void tarjan(long long x) {
	dfn[x] = low[x] = ++ dn, s[++ top] = x, is[x] = 1;
	for(auto y : v[x]) 
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(is[y]) low[x] = min(low[x], dfn[y]);
	if(dfn[x] == low[x]) {
		cnt ++;
		do {
			scc[s[top]] = cnt, is[s[top]] = 0;
		} while(s[top --] != x);
	}
	return;
}

void spfa(long long S) {
	queue<long long> q; q.push(S);
	dis[S] = b[S], vis[S] = 1;
	while(!q.empty()) {
		long long x = q.front(); q.pop();
		vis[x] = 0;
		for(auto y : e[x]) if(dis[y] < dis[x] + b[y]) {
			dis[y] = dis[x] + b[y];
			if(!vis[y]) vis[y] = 1, q.push(y);
		}
	}
	return;
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= m; i ++) {
		long long x = read(), y = read();
		v[x].push_back(y);
	}
	for(int i = 1; i <= n; i ++) a[i] = read();
	S = read(), P = read();
	for(int i = 1; i <= P; i ++) f[read()] = true;
	for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i);
	for(int i = 1; i <= n; i ++) for(auto j : v[i])
		if(scc[i] != scc[j]) e[scc[i]].push_back(scc[j]);
	for(int i = 1; i <= n; i ++) if(f[i]) g[scc[i]] = true;
	for(int i = 1; i <= n; i ++) b[scc[i]] += a[i];
	spfa(scc[S]); long long ans = 0;
	for(int i = 1; i <= cnt; i ++) if(g[i]) ans = max(ans, dis[i]);
	cout << ans << "\n";
	return 0;
}

\(\text{luogu-4782}\)

\(n\) 个布尔变量 \(x_1\sim x_n\),另有 \(m\) 个需要满足的条件,每个条件的形式都是 「\(x_i\)true / false\(x_j\)true / false」。比如 「\(x_1\) 为真或 \(x_3\) 为假」、「\(x_7\) 为假或 \(x_2\) 为假」。

2-SAT 问题的目标是给每个变量赋值使得所有条件得到满足。

\(1 \le n,m \le 10^6\)


什么是 2-SAT?

首先,把「2」和「SAT」拆开。SAT 是 Satisfiability 的缩写,意为可满足性。即一串布尔变量,每个变量只能为真或假。要求对这些变量进行赋值,满足布尔方程。

举个例子:教练正在讲授一个算法,代码要给教室中的多位同学阅读,代码的码风要满足所有学生。假设教室当中有三位学生:Anguei、Anfangen、Zachary_260325。现在他们每人有如下要求:

  • Anguei: 我要求代码当中满足下列条件之一:
    1. 不写 using namespace std;\(\neg a\)
    2. 使用读入优化 (\(b\)
    3. 大括号不换行 (\(\neg c\)
  • Anfangen: 我要求代码当中满足下条件之一:
    1. using namespace std;\(a\)
    2. 使用读入优化 (\(b\)
    3. 大括号不换行 (\(\neg c\)
  • Zachary_260325:我要求代码当中满足下条件之一:
    1. 不写 using namespace std;\(\neg a\)
    2. 使用 scanf\(\neg b\)
    3. 大括号换行 (\(c\)

我们不妨把三种要求设为 \(a,b,c\),变量前加 \(\neg\) 表示「不」,即「假」。上述条件翻译成布尔方程即:\((\neg a\vee b\vee\neg c) \wedge (a\vee b\vee\neg c) \wedge (\neg a\vee\neg b\vee c)\)。其中,\(\vee\) 表示或,\(\wedge\) 表示与。(就像集合中并集交集一样)

现在要做的是,为 ABC 三个变量赋值,满足三位学生的要求。

Q: 这可怎么赋值啊?暴力?

A: 对,这是 SAT 问题,已被证明为 NP 完全 的,只能暴力。

Q: 那么 2-SAT 是什么呢?

A: 2-SAT,即每位同学只有两个条件(比如三位同学都对大括号是否换行不做要求,这就少了一个条件)不过,仍要使所有同学得到满足。于是,以上布尔方程当中的 \(c,\neg c\) 没了,变成了这个样子:\((\neg a\vee b) \wedge (a\vee b) \wedge (\neg a\vee\neg b)\)

怎么求解 2-SAT 问题?

使用强连通分量。 对于每个变量 \(x\),我们建立两个点:\(x, \neg x\) 分别表示变量 \(x\)true 和取 false。所以,图的节点个数是两倍的变量个数在存储方式上,可以给第 \(i\) 个变量标号为 \(i\),其对应的反值标号为 \(i + n\)。对于每个同学的要求 \((a \vee b)\),转换为 \(\neg a\rightarrow b\wedge\neg b\rightarrow a\)。对于这个式子,可以理解为:「若 \(a\) 假则 \(b\) 必真,若 \(b\) 假则 \(a\) 必真」然后按照箭头的方向建有向边就好了。综上,我们这样对上面的方程建图:

原式 建图
\(\neg a\vee b\) \(a\rightarrow b\wedge\neg b\rightarrow\neg a\)
\(a \vee b\) \(\neg a\rightarrow b\wedge\neg b\rightarrow a\)
$\neg a\vee\neg b\space \space $ \(a\rightarrow\neg b\wedge b\rightarrow\neg a\)

于是我们得到了这么一张图:

built

可以看到,\(\neg a\)\(b\) 在同一强连通分量内,\(a\)\(\neg b\) 在同一强连通分量内。同一强连通分量内的变量值一定是相等的。也就是说,如果 \(x\)\(\neg x\) 在同一强连通分量内部,一定无解。反之,就一定有解了。

找解很简单,只需要 \(x\) 所在的强连通分量的拓扑序在 \(\neg x\) 所在的强连通分量的拓扑序之后取 \(x\) 为真 就可以了。因为若 \(x\)\(\neg x\) 之后,则说明 \(\neg x\) 成立的话 \(x\) 一定成立,矛盾,于是让 \(x\) 成立。

时间复杂度为 \(O(N + M)\)

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define MAXN 2000005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, dfn[MAXN], low[MAXN], dn, s[MAXN], is[MAXN];
long long top, cnt, scc[MAXN];
vector<long long> v[MAXN];

void tarjan(long long x) {
	dfn[x] = low[x] = ++ dn, s[++ top] = x, is[x] = 1;
	for(auto y : v[x]) 
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(is[y]) low[x] = min(low[x], dfn[y]);
	if(dfn[x] == low[x]) {
		cnt ++;
		do {
			scc[s[top]] = cnt, is[s[top]] = 0;
		} while(s[top --] != x);
	}
	return;
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= m; i ++) {
		long long x = read(), px = read(), y = read(), py = read();
		v[x + n * (px & 1)].push_back(y + n * (py ^ 1));
		v[y + n * (py & 1)].push_back(x + n * (px ^ 1));
	}
	for(int i = 1; i <= 2 * n; i ++) if(!dfn[i]) tarjan(i);
	for(int i = 1; i <= n; i ++) if(scc[i] == scc[i + n]) { 
		cout << "IMPOSSIBLE\n"; 
		return 0; 
	}
	cout << "POSSIBLE\n";
	for(int i = 1; i <= n; i ++)
		cout << (scc[i] < scc[i + n]) << " ";
	cout << "\n";
	return 0;
}

\(\text{luogu-3209}\)

若能将无向图 \(G=(V, E)\) 画在平面上使得任意两条无重合顶点的边不相交,则称 \(G\) 是平面图。判定一个图是否为平面图的问题是图论中的一个重要问题。现在假设你要判定的是一类特殊的图,图中存在一个包含所有顶点的环,即存在哈密顿回路,会给定哈密顿回路上的点。

\(1 \le T \le 100\)\(3 \le n \le 200\)\(1 \le m \le 10^4\)


由于有哈密顿回路,我们钦定回路上的点编号递增,处理一下映射关系就好了。

于是每条在环上的边都可以用一个区间 \([l,r]\) 表示,这条边连接 \(l,r\) 两个点。

对于两条边相交,当且仅当两条边都在环内或环外,且满足以下任意两种情况:

\[\begin{align} &[l_1, &&r_1] \\ [l_2, &&r_2] \end{align} \]

\[\begin{align} [l_1, &&r_1] \\ &[l_2, &&r_2] \end{align} \]

注意:任意端点一样的两个区间一定不交。

于是对于这样在同一侧会相交的边,一定需要在不同的两侧放着。

考虑用 2-SAT 刻画这个问题。

对这样的两个边编号为 \(x,y\)\(f_x=0\) 表示在内测,\(f_x = 1\) 表示在外侧。

于是就得到了两条限制,\((f_x \or f_y) \and (\neg f_x \or \neg f_y)\),于是我们连 \(4\) 条边即可。

对连边后的图跑 2-SAT,若成立则 YES,不成立则 NO

但是这样的时间复杂度是 \(O(m^2)\) 的,不可接受。

再思考一下可以发现,满足条件的情况下,边数最多是 \(3n - 6\),也就是说如果 \(m > 3n-6\) 一定不成立,特判一下即可。时间复杂度降至 \(O(n^2)\)

#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
#define MAXN 10005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long T, n, m, f[MAXN], dfn[MAXN], low[MAXN], dn, s[MAXN];
struct node { long long x, y; } e[MAXN];
long long is[MAXN], top, scc[MAXN], cnt;
vector<long long> v[MAXN];

bool check(long long l, long long r, long long L, long long R) {
	if(l < L && L < r && R > r) return true;
	if(l < R && R < r && L < l) return true;
	return false;
}

void tarjan(long long x) {
	dfn[x] = low[x] = ++ dn, s[++ top] = x, is[x] = 1;
	for(auto y : v[x]) 
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(is[y]) low[x] = min(low[x], dfn[y]);
	if(dfn[x] == low[x]) {
		cnt ++;
		do {
			scc[s[top]] = cnt, is[s[top]] = 0;
		} while(s[top --] != x);
	}
	return;
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= m; i ++) {
		e[i].x = read(), e[i].y = read();
		if(e[i].x > e[i].y) swap(e[i].x, e[i].y);
	}
	for(int i = 1; i <= m; i ++) for(int j = i + 1; j <= m; j ++) 
		if(check(e[i].x, e[i].y, e[j].x, e[j].y)) {
			v[i + m].push_back(j), v[i].push_back(j + m);
			v[j + m].push_back(i), v[j].push_back(i + m);
		}
	for(int i = 1; i <= 2 * m; i ++) if(!dfn[i]) tarjan(i);
	bool fg = true;
	for(int i = 1; i <= m; i ++) 
		if(scc[i] == scc[i + m]) { fg = false; break; }
	if(!fg) { cout << "Impossible\n"; return 0; }
	for(int i = 1; i <= m; i ++) 
		if(scc[i] < scc[i + m]) cout << "i";
		else cout << "o";
	cout << "\n";
	return 0;
}

\(\text{codeforces-27d}\)

众所周知,贝兰有 \(n\) 座城市,它们组成了银环——城市 \(i\)\(i+1\)\(1 \le i < n\))之间有条路相连,城市 \(n\)\(1\) 之间也有路相连。政府决定新建 \(m\) 条道路。道路清单已经准备好,每条新路都将连接两个城市。每条路都是一条曲线,要么完全在环的内侧,要么完全在外侧。新路除了端点外,不能与环上的路有任何交点。

现在设计师们想知道,能否安排这些新路,使得它们之间没有交叉(注意,路可以在端点相交)。如果可以,哪些路应该建在环的内侧,哪些路应该建在外侧呢?

\(4 \le n \le 100\)\(1 \le m \le 100\)


上一题的弱化版,本质上是一样的,不需要处理特判和映射关系。

需要输出一种解。

#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
#define MAXN 10005

/*跟上题一样,不贴了*/

int main() {
	n = read(), m = read();
	for(int i = 1; i <= m; i ++) {
		e[i].x = read(), e[i].y = read();
		if(e[i].x > e[i].y) swap(e[i].x, e[i].y);
	}
	for(int i = 1; i <= m; i ++) for(int j = i + 1; j <= m; j ++) 
		if(check(e[i].x, e[i].y, e[j].x, e[j].y)) {
			v[i + m].push_back(j), v[i].push_back(j + m);
			v[j + m].push_back(i), v[j].push_back(i + m);
		}
	for(int i = 1; i <= 2 * m; i ++) if(!dfn[i]) tarjan(i);
	bool fg = true;
	for(int i = 1; i <= m; i ++) 
		if(scc[i] == scc[i + m]) { fg = false; break; }
	if(!fg) { cout << "Impossible\n"; return 0; }
	for(int i = 1; i <= m; i ++) 
		if(scc[i] < scc[i + m]) cout << "i";
		else cout << "o";
	cout << "\n";
	return 0;
}

\(\text{loj-10097 / luogu-3825}\)

小 L 计划进行 \(n\) 场游戏,每场游戏使用一张地图,小 L 会选择一辆车在该地图上完成游戏。

小 L 的赛车有三辆,分别用大写字母 \(A\)\(B\)\(C\) 表示。地图一共有四种,分别用小写字母 \(x\)\(a\)\(b\)\(c\) 表示。

其中,赛车 \(A\) 不适合在地图 \(a\) 上使用,赛车 \(B\) 不适合在地图 \(b\) 上使用,赛车 \(C\) 不适合在地图 \(c\) 上使用,而地图 \(x\) 则适合所有赛车参加。

适合所有赛车参加的地图并不多见,最多只会有 \(d\) 张。

\(n\) 场游戏的地图可以用一个小写字母组成的字符串描述。例如:\(S=\texttt{xaabxcbc}\) 表示小 L 计划进行 \(8\) 场游戏,其中第 \(1\) 场和第 \(5\) 场的地图类型是 \(x\),适合所有赛车,第 \(2\) 场和第 \(3\) 场的地图是 \(a\),不适合赛车 \(A\),第 \(4\) 场和第 \(7\) 场的地图是 \(b\),不适合赛车 \(B\),第 \(6\) 场和第 \(8\) 场的地图是 \(c\),不适合赛车 \(C\)

小 L 对游戏有一些特殊的要求,这些要求可以用四元组 $ (i, h_i, j, h_j) $ 来描述,表示若在第 \(i\) 场使用型号为 \(h_i\) 的车子,则第 \(j\) 场游戏要使用型号为 \(h_j\) 的车子。

你能帮小 L 选择每场游戏使用的赛车吗?如果有多种方案,输出任意一种方案。

如果无解,输出 -1

\(1 \le n \le 5 \times 10^4\)\(0 \le d \le 8\)\(1 \le m \le 10^5\)


首先考虑 \(d=0\) 的情况。

那么每个点都有两种状态,\(i\)\(\neg i\),分别表示选用这张地图可用的第一张和第二张地图。

对于每个限制条件,令 \(u\) 表示”第 \(i\) 场使用型号为 \(h_i\) 的车“的点,\(v\) 表示”第 \(j\) 场使用型号为 \(h_j\) 的车“的点,\(\neg u\)\(\neg v\) 则相反。那么就会有下面三种情况:

  • 如果第 \(i\) 场不允许使用 \(h_i\) 的车,则不做操作。
  • 如果第 \(i\) 场可用 \(h_i\),但第 \(j\) 场不允许使用 \(h_j\),那么如果 \(u\) 为真,则一定无解,连边 \(u \to \neg u\)
  • 如果第 \(i,j\) 场都可用,则连边 \(u \to v\)\(\neg v \to \neg u\)

建完边之后就是 2-SAT 问题,可以得到一种可行解,找不到则无解。

此时再考虑 \(d \ne 0\) 的情况。

显然可以枚举 x 地图使用 A/B/C 的情况就可以得到答案。

但时间复杂度为 \(O((n+m) \times 3^d)\),不可接受,考虑优化。

我们发现其实枚举两种不允许的方案即可,比如枚举 x 地图不允许 A/B

  • 不允许 A 的话,只能使用 B/C
  • 不允许 B 的话,只能使用 A/C

实际上覆盖了三种情况,于是如果有解则一定能找到一种解,否则无解。

时间复杂度为 \(O((n+m) \times 2^d)\)

注意:细节非常多,写的时候需要思路捋顺。

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define MAXN 200005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

char get() {
    char c; while ((c = getchar()) != 'A' && c != 'B' && c != 'C');
    return c;
}

long long n, d, m, dfn[MAXN], low[MAXN], dn, s[MAXN];
struct node { long long x, y; char s, t; } e[MAXN];
long long is[MAXN], cnt, top, scc[MAXN], p[MAXN];
vector<long long> v[MAXN];
char st[MAXN], pb[MAXN];
bool fg;

void tarjan(long long x) {
	dfn[x] = low[x] = ++ dn, s[++ top] = x, is[x] = 1;
	for(auto y : v[x]) 
		if(!dfn[y]) tarjan(y), low[x] = min(low[x], low[y]);
		else if(is[y]) low[x] = min(low[x], dfn[y]);
	if(dfn[x] == low[x]) {
		cnt ++;
		do {
			scc[s[top]] = cnt, is[s[top]] = 0;
		} while(s[top --] != x);
	}
	return;
}

long long neg(long long x) { return (x > n) ? x - n : x + n; }

long long tran(long long x, char c) {
	if(st[x] == 'a') return (c == 'B') ? x : x + n;
	if(st[x] == 'b' || st[x] == 'c') return (c == 'A') ? x : x + n;
	return (c == 'C') ? x + n : x;
}

bool solve() {
	for(int i = 1; i <= 2 * n; i ++) dfn[i] = 0, v[i].clear();
	for(int i = 1; i <= m; i ++) 
		if(s[e[i].x] != 'x' && st[e[i].y] != 'x') {
			if(e[i].s - 'A' == st[e[i].x] - 'a') continue;
			long long x = tran(e[i].x, e[i].s), y;
			if(e[i].t - 'A' == st[e[i].y] - 'a') {
				v[x].push_back(neg(x)); continue;
			}
			y = tran(e[i].y, e[i].t);
			v[x].push_back(y), v[neg(y)].push_back(neg(x));
		}
		else {
			char cx = st[e[i].x], cy = st[e[i].y];
			long long x, y, px = p[e[i].x], py = p[e[i].y];
			if(cx == 'x' && cy == 'x') {
				if(e[i].s == pb[px]) continue;
				x = tran(e[i].x, e[i].s);
				if(e[i].t == pb[py]) {
					v[x].push_back(neg(x)); continue;
				}
				y = tran(e[i].y, e[i].t);
				v[x].push_back(y), v[neg(y)].push_back(neg(x));
			}
			else if(cx == 'x' && cy != 'x') {
				if(e[i].s == pb[px]) continue;
				x = tran(e[i].x, e[i].s);
				if(e[i].t - 'A' == st[e[i].y] - 'a') {
					v[x].push_back(neg(x)); continue;
				}
				y = tran(e[i].y, e[i].t);
				v[x].push_back(y), v[neg(y)].push_back(neg(x));
			}
			else {
				if(e[i].s - 'A' == st[e[i].x] - 'a') continue;
				x = tran(e[i].x, e[i].s);
				if(e[i].t == pb[py]) {
					v[x].push_back(neg(x)); continue;
				}
				y = tran(e[i].y, e[i].t);
				v[x].push_back(y), v[neg(y)].push_back(neg(x));
			}
		}
	for(int i = 1; i <= 2 * n; i ++) if(!dfn[i]) tarjan(i);
	for(int i = 1; i <= n; i ++) if(scc[i] == scc[i + n]) return 0;
	for(int i = 1; i <= n; i ++) 
		if(scc[i] < scc[i + n])
			if(st[i] == 'a') cout << "B";
			else if(st[i] == 'b' || st[i] == 'c') cout << "A";
			else if(pb[p[i]] == 'A') cout << "B";
			else cout << "A";
		else {
			if(st[i] == 'a' || st[i] == 'b') cout << "C";
			else if(st[i] == 'c') cout << "B";
			else if(pb[p[i]] == 'A') cout << "C";
			else cout << "B"; 
		}
	cout << "\n";
	return 1;
}

void dfs(long long x) {
	if(fg) return; 
	if(x > d) { fg = solve(); return; }
	pb[x] = 'A', dfs(x + 1);
	pb[x] = 'B', dfs(x + 1);
	return;
}

int main() {
	n = read(), read(); cin >> (st + 1); m = read();
	for(int i = 1; i <= n; i ++) if(st[i] == 'x') p[i] = ++ d;
	for(int i = 1; i <= m; i ++) 
		e[i].x = read(), e[i].s = get(), e[i].y = read(), e[i].t = get();
	dfs(1); if(!fg) cout << "-1";
	return 0;
}
posted @ 2025-12-24 18:33  So_noSlack  阅读(5)  评论(0)    收藏  举报