算法模板(3):搜索(4):高等图论

news2024/11/17 7:45:59

高等图论

有向图的强连通分量

相关概念

  • 强连通分量:Strongly Connected Component (SCC).
  • 对于一个有向图顶点的子集 S S S,如果在 S S S 内任取两个顶点 u u u v v v,都能找到一条 u u u v v v 的路径,那么称 S S S 是强连通的。
  • 如果在强连通的顶点集合 S S S 中加入其他任意顶点集合后,它都不再是强连通的,那么称 S S S 是原图的一个强连通分量。任意有向图都可以分解成若干不相交的强连通分量,这就是强连通分量的分解。将分解后的强连通分量缩成一个顶点,就得到一个 D A G DAG DAG(有向无环图,也叫拓扑图)。

dfn[x] :结点 x 第一次被访问的时间戳 (dfs number); low[x] :结点 x 所能访问到的点的 dfn 值的最小值. 这里的树指的是 DFS 树. 所有结点按 dfn 排序即可得 dfs 序列

一个结点的子树内结点的 dfn 都大于该结点的 dfn。从根开始的一条路径上的 dfn 严格递增。一棵 DFS 树被构造出来后,考虑图中的非树边。前向边 (forward edge):祖先→儿子。后向边 (backward edge):儿子→祖先。横叉边 (cross edge):没有祖先—儿子关系的。注意:横叉边只会往 dfn 减小的方向连接。在无向图中,没有横叉边(因为无向图的横插边一定有)、

在构造 dfs 树的时候,称 d f s dfs dfs 尚未搜索到的边为树枝边

1174. 受欢迎的牛

  • Tarjan算法求强连通分量
  • x x x 是正在搜索的节点。看看它是否在强连通分量之中。情况1:存在后向边,指向祖先节点;情况2:存在横叉边,横叉边再走到祖先节点。
  • 时间戳:对于每个点,定义两个时间戳。 d f n [ u ] dfn[u] dfn[u] 表示遍历到u的时间戳; l o w [ u ] low[u] low[u] 表示从 u u u 开始走,所能遍历到的最小时间戳。 u u u 是其所在强连通分量的最高点等价于 d f n [ u ] = l o w [ u ] dfn[u] = low[u] dfn[u]=low[u].
  • 最后连通分量编号逆序一定是拓扑序,所以其实不用进行拓扑排序。
  • 题意:找到一个图中所有满足这样关系的节点的数量:从图中其他任何节点都可以到达这个节点。
  • 对于这道题,如果不是拓扑图,需要每一个点都 b f s bfs bfs 一遍,复杂度 O ( n 2 ) O(n^2) O(n2),太高。但是如果是拓扑图的话,如果存在至少两个出度为 0 0 0 的点,那么这两个点相互无法到达,答案就是 0 0 0。但是,如果只有一个出度为 0 0 0 的点,那么答案就是出度为 0 0 0 的这个连通块的大小。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<stack>
using namespace std;
const int maxn = 10010, maxm = 50010;
int h[maxn], e[maxm], ne[maxm], idx;
int N, M, dfn[maxn], low[maxn];
int id[maxn], sz[maxn], out[maxn], timestamp, scc_cnt;
bool in_stk[maxn];
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
stack<int> stk;

void tarjan(int u) {
//千万别写成 timestamp++ !!!
	dfn[u] = low[u] = ++timestamp;
	stk.push(u), in_stk[u] = true;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (!dfn[v]) {
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		else if (in_stk[v]) low[u] = min(low[u], dfn[v]);
	}
	if (dfn[u] == low[u]) {
		scc_cnt++;
		int v;
		do {
			v = stk.top(); stk.pop();
			in_stk[v] = false;
			id[v] = scc_cnt;
			sz[scc_cnt]++;
		} while (v != u);
	}
}
void solve() {
	for (int i = 1; i <= N; i++) {
		if (!dfn[i]) {
			tarjan(i);
		}
	}
	for (int u = 1; u <= N; u++) {
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			int a = id[u], b = id[v];
			if (a != b) out[a]++;
		}
	}
	int zero = 0, sum = 0;
	for (int i = 1; i <= scc_cnt; i++) {
		if (!out[i]) {
			zero++;
			sum += sz[i];
			if (zero > 1) {
				sum = 0;
				break;
			}
		}
	}
	printf("%d\n", sum);
}
int main() {
	scanf("%d%d", &N, &M);
	memset(h, -1, sizeof h);
	for (int i = 0; i < M; i++) 
    {
		int a, b;
		scanf("%d%d", &a, &b);
		add(a, b);
	}
	solve();
	return 0;
}

无向图的双连通分量

  • 双连通分量又被称为重连通分量。双连通分量分为两种:边双连通分量(e-DCC)、点双连通分量(v-DCC)。
  • 桥(割边):假设有连通图G,e是其中一条边,如果G-e是不连通的,则边e是图G的一条割边。此情形下,G-e必包含两个连通分支。
  • 割点:在一个无向图中,如果有一个顶点集合,删除这个顶点集合以及这个集合中所有顶点相关联的边以后,图的连通分量增多,就称这个点集为割点集合。如果某个割点集合只含有一个顶点X(也即{X}是一个割点集合),那么X称为一个割点注意割点至少属于两个连通分量。
  • 若一个无向图中的去掉任意一个节点(一条边)都不会改变此图的连通性,即不存在割点(桥),则称作点(边)双连通图。
  • 一个无向图中的每一个极大点(边)双连通子图称作此无向图的点(边)双连通分量。
  • 用tarjan算法可以求边双连通分量和点双连通分量。

395. 冗余路径

  • 边双连通分量算法
  • 给一个无向连通图,问至少加几条边,可以把这个图变成边双连通分量
  • 求桥:dfn[u] < low[v]
  • 一个图是边双连通分量,等价于任意两点之间有至少两条相互分离的路径(即两条路径没有一条重合的道路)。
  • 双连通分量内任何两点间都是至少有两条相互分离的路径,因此缩点之后,变成了一个树。设图中度数为1的节点有cnt个,那么最后要加的边是 (cnt + 1) / 2 个。其实就是把叶节点两两相连(如果cnt是奇数就把多余的叶节点随便连一下)。这样,就变成了一个双连通图。
#include<cstdio>
#include<algorithm>
#include<stack>
#include<cstring>
using namespace std;
const int maxn = 5010, maxm = 20010;
int h[maxn], ne[maxm], e[maxm], idx;
int id[maxn], low[maxn], dfn[maxn], timestamp, dcc_cnt;
int N, M, d[maxn];
bool is_bridge[maxm];
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
stack<int> stk;
// from 记载从哪条边来的。
void tarjan(int u, int from) {
	low[u] = dfn[u] = ++timestamp;
	stk.push(u);
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (!dfn[v]) {
			tarjan(v, i);
			low[u] = min(low[u], low[v]);
			
			//dfn[u] < low[v] 等价于 u 与 v 之间的边是桥。
			if (dfn[u] < low[v]) {
				//双向边的编号是两两成对的。
				is_bridge[i] = is_bridge[i ^ 1] = true;
			}
		}
		
		else if (i != (from ^ 1)) low[u] = min(low[u], dfn[v]);
	}
	if (dfn[u] == low[u]) {
		++dcc_cnt;
		int v;
		do {
			v = stk.top(); stk.pop();
			id[v] = dcc_cnt;
		} while (v != u);
	}
}
int main() {
	scanf("%d%d", &N, &M);
	memset(h, -1, sizeof h);
	for (int i = 0; i < M; i++) {
		int a, b;
		scanf("%d%d", &a, &b);
		add(a, b), add(b, a);
	}
	tarjan(1, -1);
	for (int i = 0; i < idx; i++) {
		if (is_bridge[i]) d[id[e[i]]]++;
	}
	int cnt = 0;
	for (int i = 1; i <= dcc_cnt; i++) {
		if (d[i] == 1) cnt++;
	}
	printf("%d\n", (cnt + 1) / 2);
	return 0;
}

1183. 电力

  • 点双连通分量算法
  • 删除图中的一个点之后,连通块最多有多少。
  • 求割点:(1)当u不是根节点时, l o w ( v ) > = d f n ( u ) low(v) >= dfn(u) low(v)>=dfn(u),则u是割点;当u是根节点时,至少有两个子节点 y i y_i yi,有 l o w ( y i ) > = d f n ( u ) low(y_i) >= dfn(u) low(yi)>=dfn(u)
  • 统计原本的连通块儿数量blocks,以及枚举每个连通块儿删掉一个点可以得到的最大的连通块儿数量res,答案就是blocks + res - 1.
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 10010, maxm = 30010;
int h[maxn], e[maxm], ne[maxm], idx;
int N, M, res, blocks, root;
int dfn[maxn], low[maxn], timestamp;
void tarjan(int u) {
	dfn[u] = low[u] = ++timestamp;
	int cnt = 0;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (!dfn[v]) {
			tarjan(v);
			low[u] = min(low[u], low[v]);
			if (low[v] >= dfn[u]) cnt++;
		}
		else low[u] = min(low[u], dfn[v]);
	}
	if (u != root) cnt++;
	res = max(res, cnt);
}
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int main() {
	while (scanf("%d%d", &N, &M) && N) {
		memset(dfn, 0, sizeof dfn);
		memset(h, -1, sizeof h);
		timestamp = idx = 0;
		for (int i = 0; i < M; i++) {
			int a, b;
			scanf("%d%d", &a, &b);
			add(a, b), add(b, a);
		}
		res = blocks = 0;
		for (root = 0; root < N; root++) {
			if (!dfn[root]) {
				blocks++;
				tarjan(root);
			}
		}
		printf("%d\n", blocks + res - 1);
	}
	return 0;
}

396. 矿场搭建

  • 题意:对于一个无向图,在图中找到一些点集{S},使得不管取下图中哪一个点,其他点都可以到达{S}中的至少一个点。求问点集大小最小是多少,以及在此条件下的方案总数。
  • 点集大小至少为2;
  • 情况1. 若无割点,不管哪下那个点,图都是连通的,那么点集大小为2;
  • 情况2:有割点的话需要缩点,每个割点单独作为一个点,从每个 v-DCC 向其包含的每个割点连一条边。若v-DCC度数为1,那么需要在改分量非割点的地方设置一个出口;若度数大于1,则无需设置出口。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<stack>
#include<vector>
using namespace std;
typedef unsigned long long ll;
const int maxn = 1010, maxm = 1010;
int h[maxn], e[maxm], ne[maxm], idx;
int dfn[maxn], low[maxn], dcc_cnt, timestamp;
int N, M, root, kase;
bool is_cut[maxn];
vector<int> dcc[maxn];  //这个存的是每一个连通分量含哪些节点。
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
stack<int> stk;
void tarjan(int u) {
	dfn[u] = low[u] = ++timestamp;
	stk.push(u);
	if (u == root && h[u] == -1) {
		dcc_cnt++;
		dcc[dcc_cnt].push_back(u);
		return;
	}
	int cnt = 0;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (!dfn[v]) {
			tarjan(v);
			low[u] = min(low[u], low[v]);
			if (dfn[u] <= low[v]) {
				cnt++;
				if (u != root || cnt > 1) is_cut[u] = true;
				dcc_cnt++;
				int y;
				do {
					y = stk.top(); stk.pop();
					dcc[dcc_cnt].push_back(y);
				} while (y != v);
				dcc[dcc_cnt].push_back(u);
			}
		}
		else low[u] = min(low[u], dfn[v]);
	}
}
int main() {
	while (scanf("%d", &M) && M) {
		for (int i = 1; i <= dcc_cnt; i++) dcc[i].clear();
		memset(h, -1, sizeof h);
		memset(dfn, 0, sizeof dfn);
		memset(is_cut, 0, sizeof is_cut);
		while (stk.size()) stk.pop();
		idx = N = dcc_cnt = timestamp = 0;
		for (int i = 0; i < M; i++) {
			int a, b;
			scanf("%d%d", &a, &b);
			add(a, b), add(b, a);
			N = max(a, N), N = max(b, N);
		}
		for (root = 1; root <= N; root++) {
			if (!dfn[root]) tarjan(root);
		}
		ll num = 1;
		int res = 0;
		for (int i = 1; i <= dcc_cnt; i++) {
			int cnt = 0, sz = dcc[i].size();
			for (int j = 0; j < sz; j++) {
				if (is_cut[dcc[i][j]]) cnt++;
			}
			
			if (cnt == 0) {
				if (sz > 1) res += 2, num *= (sz - 1) * sz / 2;
				else res++;
				
			}
			else if (cnt == 1) res++, num *= sz - 1;
		}
		printf("Case %d: %d %llu\n", ++kase, res, num); 
	}
	return 0;
}

欧拉路径和欧拉回路

  • 如果图 G G G 中的一个路径包括每个边恰好一次,则该路径称为欧拉路径 ( E u l e r   p a t h ) (Euler\ path) (Euler path)

  • 如果一个回路是欧拉路径,则称为欧拉回路 ( E u l e r   c i r c u i t ) (Euler\ circuit) (Euler circuit)

  • 具有欧拉回路的图称为欧拉图(简称 E E E 图)。具有欧拉路径但不具有欧拉回路的图称为半欧拉图。

  • 无向图(图必须是连通图):

  1. 存在欧拉路径的充要条件:度数为奇数的点只有0个(起点终点重合)或2个(起点终点不重合)。
  2. 存在欧拉回路的充要条件:度数为奇数的点是0个。
  • 有向图(图必须是连通图):
  1. 存在欧拉路径的充要条件:要么所有点的入度都等于出度(起点终点重合),要么除了两个点之外,其余所有点的入度等于出度,剩余两个点:一个点出度比入度多1(起点),一个点入度比出度多1(终点)。
  2. 存在欧拉回路的充要条件:所有点的入度均等于出度。

1123. 铲雪车

  • 所有的边都是双向边,而且要看成两条边,因此所有点入读与出度相等,其实就是欧拉回路。就算铲雪车停在一条路的中间,总时间是不会变的。因此只需要算一下所有路长度的两倍就行。
  • 输出的话,格式有要求,要求分钟四舍五入、两位、保留前导零,可以这么做:printf("%.f:%02.f\n", hours, minutes);

1184. 欧拉回路

  • 给定一张图,请你找出欧拉回路,即在图中找一个环使得每条边都在环上出现恰好一次。输入:第一行包含一个整数 t t t t ∈ { 1 , 2 } t \in \lbrace 1,2 \rbrace t{1,2},如果 t = 1 t = 1 t=1,表示所给图为无向图,如果 t = 2 t = 2 t=2,表示所给图为有向图。第二行包含两个整数 n , m n,m n,m,表示图的结点数和边数。接下来 m m m 行中,第 i i i 行两个整数 v i , u i v_i,u_i vi,ui,表示第 i i i 条边(从 1 1 1 开始编号)。如果 t = 1 t = 1 t=1 则表示 v i v_i vi u i u_i ui 有一条无向边。如果 t = 2 t = 2 t=2 则表示 v i v_i vi u i u_i ui 有一条有向边。图中可能有重边也可能有自环。
  • 复杂度是 O ( m ) O(m) O(m).
  • 复原欧拉回路边的编号
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 100010, maxm = 400010;
int h[maxn], e[maxm], ne[maxm], idx;
bool used[maxm];
int N, M, ans[maxm / 2], cnt, type, din[maxn], dout[maxn];
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u) {
	//引用千万不要忘!用引用的目的其实是为了针对自环特别多的情况,可以把父结点的那条边也删掉。
	for (int& i = h[u]; i != -1;) {
		if (used[i]) {
			h[u] = ne[i];  ///注意这个删除节点的操作还是很简洁的!
			continue;
		}
		/*
		这个used[i] = true 必须要带上。因为尽管看似这条边已经删掉了,但是只是对于子节点
		而言。而对于父结点还没有删掉。
		*/
		used[i] = true;
		if (type == 1) used[i ^ 1] = true;
		int t;
		if (type == 1) {
			t = i / 2 + 1;
			if (i & 1) t = -t;
		}
		else t = i + 1;
		int v = e[i];
		i = ne[i];
		dfs(v);
		ans[cnt++] = t;
	}
}
int main() {
	scanf("%d%d%d", &type, &N, &M);
	memset(h, -1, sizeof h);
	for (int i = 0; i < M; i++) {
		int a, b;
		scanf("%d%d", &a, &b);
		add(a, b);
		if (type == 1) add(b, a);
		dout[a]++, din[b]++;
	}
	if (type == 1) {
		for (int i = 1; i <= N; i++) {
			if (din[i] + dout[i] & 1) {
				printf("NO\n");
				return 0;
			}
		}
	}
	else {
		for (int i = 1; i <= N; i++) {
			if (din[i] != dout[i]) {
				printf("NO\n");
				return 0;
			}
		}
	}
	//要从至少有一条边节点开始搜索
	for (int i = 1; i <= N; i++) {
		if (h[i] != -1) {
			dfs(i);
			break;
		}
	}
	//cnt < M 意味着图中的边不连通。
	if (cnt < M) {
		printf("NO\n");
		return 0;
	}
	printf("YES\n");
	for (int i = cnt - 1; i >= 0; i--) printf("%d%c", ans[i], i == 0 ? '\n' : ' ');
	return 0;
}

拓扑排序

  • 有向无环图至少存在一个入度为0的点,有环图一定不存在拓扑序。

  • 若一个由图中所有点构成的序列A满足:对于图中的每条边(x, y),x在A中都出现在y之前,则称A是该图的一个拓扑序列。

  • 有向无环图的拓扑序不一定是唯一的。

  • 可以拓扑排序的图,等价于拓扑图,等价于有向无环图(DAG)

  • 步骤:

  1. 将所有入度为0的点入队q。
  2. while( q.sizse() > 0 ):
    t = q.front(); q.pop();
    for( t 的每一个边 t -> j ):
    d[ j ] --;
    if(d[ j ] == 0) j 入队 q.
  3. 最后,队列中的顺序就是拓扑序。
  • f i l l ( h , h + N , − 1 ) fill(h, h + N, -1) fill(h,h+N,1),这样不对,因为节点的编号是从1开始的。老老实实用memset,小心 sizeof 问题,以及测试数据组不能特别多。

1191. 家谱树

  • 输出一个 DAG 的拓扑序
#include<iostream>
#include<queue>
#include<cstring>
const int maxn = 110, maxm = 5010;
using namespace std;
int h[maxn], e[maxm], ne[maxm], idx;
int N, ans[maxn], din[maxn], cnt;
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void toposort() {
	queue<int> que;
	//首先,把所有入度为0的点加入队列。
	for (int i = 1; i <= N; i++) {
		if (!din[i]) {
			ans[++cnt] = i;
			que.push(i);
		}
	}
	while (que.size()) {
		int u = que.front(); que.pop();
		//接着,每遍历一条u指出的边,就把u指向的顶点的入度减1。直到v的入度为0时再把v加进队列。
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			din[v]--;
			if (!din[v]) que.push(v), ans[++cnt] = v;
		}
	}
	//若题目数据不能保证一定有解,则最后需要判断cnt == N(总共加进去了N个点),是的话就存在拓扑序,否则就不存在。
}
int main() {
	scanf("%d", &N);
	memset(h, -1, sizeof h);
	for (int i = 1; i <= N; i++) {
		int t;
		while (cin >> t, t) add(i, t), din[t]++;
	}
	toposort();
	for (int i = 1; i <= cnt; i++) printf("%d%c", ans[i], i == cnt ? '\n' : ' ');
	return 0;
}

1192. 奖金

在这里插入图片描述

  • 关于差分约束的问题:
  1. 若边权任意,则需要用spfa判断环,跑最短路或最长路;
  2. 若边权非负,可以用tarjan算法
  3. 若边权均正,那么可以用拓扑排序跑一遍最长路即可。
    但是还是要掌握下面的,已知拓扑序怎样求最长路。
  • 这道题也让我们明白为什么求最小值要求最长路。而且这道题边不要连反。

法一:先拓扑排序再求最长路

#include<cstdio>
#include<queue>
#include<cstring>
using namespace std;
const int maxn = 100010, maxm = 200010;
int N, M;
int h[maxn], e[maxm], ne[maxm], idx;
int d[maxn], din[maxn], topo[maxn], cnt;
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool toposort() {
	queue<int> que;
	for (int i = 1; i <= N; i++) {
		if (!din[i]) {
			que.push(i);
			topo[++cnt] = i;
		}
	}
	while (que.size()) {
		int u = que.front(); que.pop();
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			din[v]--;
			if (!din[v]) {
				que.push(v);
				topo[++cnt] = v;
			}
		}
	}
	return cnt == N;
}
void topo_longest_path() {
	for (int i = 1; i <= N; i++) d[i] = 100;
	/*
	在拓扑序数组中求最长路一定小心!
	一个是要遍历拓扑序数组,而不是遍历1到N的节点,另一个是,拓扑序中的下标也是1到N,别弄错!
	*/
	for (int j = 1; j <= cnt; j++) {
		int u = topo[j];
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			d[v] = max(d[v], d[u] + 1);
		}
	}
}
int main() {
    
	memset(h, -1, sizeof h);
	scanf("%d%d", &N, &M);
    
	for (int i = 0; i < M; i++) 
    {
		int a, b;
		scanf("%d%d", &a, &b);
		add(b, a);
		din[a]++;
	}
    
	if (toposort()) 
    {
		topo_longest_path();
		int ans = 0;
		//注意这里还是1~N哈,因为节点的编号是从1到N的。
		for (int i = 1; i <= N; i++) ans += d[i];
		printf("%d\n", ans);
	}
	else printf("Poor Xed\n");
    
	return 0;
}

法二:边拓扑排序边求最长路

void toposort() {
	queue<int> que;
	for (int i = 1; i <= N; i++) {
		if (!din[i]) {
			d[i] = 100;
			que.push(i);
			cnt++;
		}
	}
	while (que.size()) {
		int u = que.front(); que.pop();
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			din[v]--;
			if (!din[v]) {
				que.push(v);
				d[v] = d[u] + 1;
				cnt++;
			}
		}
	}
	if (cnt != N) printf("Poor Xed\n");
	else {
		int ans = 0;
		for (int i = 1; i <= N; i++) ans += d[i];
		printf("%d\n", ans);
	}
}

164. 可达性统计

  • 给定一张 N N N 个点 M M M 条边的有向无环图,分别统计从每个点出发能够到达的点的数量。 1 ≤ N , M ≤ 30000 1≤N,M≤30000 1N,M30000
  • 用一个 01 01 01 的二进制串可以很好的表示哪些点可以到这个点。
  • b i t s e t bitset bitset 代替 b o o l bool bool 数组,可以把时间减少到 1 / 32 1 / 32 1/32,因为 b i t s e t bitset bitset 的位数比较少,按位或运算比较快。
  • 这个我是建了反向图,然后宽搜(用到了拓扑排序)。

法一

  • 建反向图,边拓扑排序边求解。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<bitset>
using namespace std;
const int maxn = 30010, maxm = 30010;
int h[maxn], e[maxm], ne[maxm], idx;
int N, M, din[maxn];
bitset<maxn> ans[maxn];
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void toposort() {
	queue<int> que;
	for (int i = 1; i <= N; i++) ans[i].set(i, 1);
	for (int i = 1; i <= N; i++) {
		if (!din[i]) {
			que.push(i);
		}
	}
	while (que.size()) {
		int u = que.front(); que.pop();
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			ans[v] |= ans[u], din[v]--;
			if (din[v] == 0) que.push(v);
		}
	}
}
int main() {
	memset(h, -1, sizeof h);
	scanf("%d%d", &N, &M);
	for (int i = 0; i < M; i++) {
		int a, b;
		scanf("%d%d", &a, &b);
		add(b, a);
		din[a]++;
	}
	toposort();
	for (int i = 1; i <= N; i++) printf("%d\n", ans[i].count());
	return 0;
}

法二:

  • 先拓扑排序,然后按照拓扑序逆序求解。
toposort();
for (int j = N; j; j--) {
	int u = topo[j];
	f[u][u] = 1;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		f[u] |= f[v];
	}
}

2-SAT

朱刘算法

Prufer编码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/634516.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JVM零基础到高级实战之Java程序员不可不知的对象创建底层步骤细节

JVM零基础到高级实战之Java程序员不可不知的对象创建底层步骤细节 JVM零基础到高级实战之Java程序员不可不知的对象创建底层步骤细节 文章目录 JVM零基础到高级实战之Java程序员不可不知的对象创建底层步骤细节前言Java对象创建的流程步骤包括哪些&#xff1f;总结 前言 JVM零…

【云原生 | 53】Docker三剑客之Docker Compose应用案例一:Web负载均衡

&#x1f341;博主简介&#xff1a; &#x1f3c5;云计算领域优质创作者 &#x1f3c5;2022年CSDN新星计划python赛道第一名 &#x1f3c5;2022年CSDN原力计划优质作者 &#x1f3c5;阿里云ACE认证高级工程师 &#x1f3c5;阿里云开发者社区专…

基于Echarts构建停车场数据可视化大屏(文末送书)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

【部署LVS-DR 群集】

目录 一、DR模式 LVS负载均衡群集1、数据包流向分析2、DR 模式的特点 二、DR模式 LVS负载均衡群集部署1、1.配置负载调度器&#xff08;192.168.80.30&#xff09;&#xff08;1&#xff09;配置虚拟 IP 地址&#xff08;VIP&#xff1a;192.168.102.188&#xff09;&#xff0…

《设计模式》之装饰器模式

文章目录 1、定义2、动机3、类结构4、优缺点5、注意事项6、总结7、代码实现(C) 1、定义 动态&#xff08;组合&#xff09;地给一个对象增加一些额外的职责。就增加功能而言&#xff0c;Decorator模式比生成子类&#xff08;继承&#xff09;更为灵活&#xff08;消除重复代码…

PPT中这8个隐藏技巧-掌握了马上让你幸福感满满

开篇 一个好的PPT需要精雕细琢。即使我们使用了AIGC特别是时下流行的用GPT书写大纲,然后把大纲内的内容放到一些自动GC PPT内容的生成器里生成后的PPT其实也不是马上可以拿来用的。工作上一份大领导、公司、集团级别的PPT不可能90%使用GPT GC生成就可以直接交付的。比如说我们…

Trie树模板与应用

文章和代码已经归档至【Github仓库&#xff1a;https://github.com/timerring/algorithms-notes 】或者公众号【AIShareLab】回复 算法笔记 也可获取。 文章目录 Trie树&#xff08;字典树&#xff09;基本思想例题 Trie字符串统计code关于idx的理解 模板总结应用 最大异或对分…

初探BERTPre-trainSelf-supervise

初探Bert 因为一次偶然的原因&#xff0c;自己有再次对Bert有了一个更深层地了解&#xff0c;特别是对预训练这个概念&#xff0c;首先说明&#xff0c;自己是看了李宏毅老师的讲解&#xff0c;这里只是尝试进行简单的总结复述并加一些自己的看法。 说Bert之前不得不说现在的…

ansible远程执行指令,/bin/sh: java: command not foundnon-zero return code

问题描述&#xff1a;ansible远程执行指令&#xff0c;初选指令加载不全&#xff0c; [rootVM-0-6-centos ~]# ansible all -m shell -a "java -version" 10.206.0.15 | FAILED | rc127 >> /bin/sh: java: command not foundnon-zero return code 解决方案&a…

C++(8):IO 库

IO 类 IO 库类型和头文件 iostream 定义了用于读写流的基本类型&#xff0c;fstream 定义了读写命名文件的类型&#xff0c;sstream 定义了读写内存 string 对象的类型。 其中带 w 前缀的类型用来操作宽字符语言 (wchar_t)。宽字符版本的类型和函数前都有一个 w&#xff0c;如…

SAP从入门到放弃系列之PP/DS-part1

翻译一篇大佬文章&#xff0c;了解一下PPDS前世今生和产品功能出现的业务背景。虽然是15年的&#xff0c;但经典永流传~~~&#xff0c;感谢大佬的文章。 原文地址&#xff1a; #S4HANA 1610 use case series: 9a – Production Planning and Detailed Scheduling – PP/DS (b…

【MySQL学习笔记】子查询与联结(连接)

1.子查询 将一条select语句返回的结果用于另一条select语句的where子句中。 执行时&#xff0c;先执行子查询&#xff0c;再执行主查询。 select sid from sc where cid in (select cid from course where cname数据库应用技术);子查询一般与 IN 操作符结合使用&#xff0…

《微服务实战》 第三十二章 微服务链路跟踪-sleuth zipkin

前言 大型分布式微服务系统中&#xff0c;一个系统被拆分成N多个模块&#xff0c;这些模块负责不同的功能&#xff0c;组合成一套系统&#xff0c;最终可以提供丰富的功能。在这种分布式架构中&#xff0c;一次请求往往需要涉及到多个服务服务之间的调用错综复杂&#xff0c;对…

Lenovo Yoga-710-14IKB电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。&#xff08;下载请直接百度黑果魏叔&#xff09; 硬件配置 硬件型号驱动情况 主板Lenovo Yoga 710 (14") - 14IKB (without dGPU) 处理器Intel i5-7200U (4) 2.50GHz (IKBL)已驱动 内存48 GB ( 海盗船 DDR4 3200…

web worker创建多个 JavaScript 线程 (使用GTP写的文章)

前言 最近在优化公司的一个项目&#xff0c;使用的就是web worker去优化&#xff0c;做了那些优化&#xff0c;一个是状态的优化&#xff0c;&#xff08;通信的状态实时更新&#xff0c;以前的做法是做个定时任务实时获取它的状态&#xff0c;然后让它在页面渲染&#xff0c;这…

【Linux】 -- TCP协议 (一)

TCP协议 Tcp协议可靠性冯诺依曼体系结构 TCP的协议格式序号与确认序号窗口大小六个标志位 确认应答机制 &#xff08;ACK&#xff09;超时重传机制连接管理机制 Tcp协议 TCP全称为 “传输控制协议”&#xff08;Transmission Control Protocol&#xff09; TCP协议被广泛应用…

[linux_C语言_udp的多种实现方法及网络调试中遇到的问题]

linux_C语言_udp的多种实现方法 最基本的方式(不用组播不用sigio信号不使能广播属性)接收端发送端 使用SIGIO信号的方式(使用sigio信号使用广播使能属性)服务端客户端 使用组播模式服务端客户端 tcp和udp的使用区别调试中遇到的问题所有源码下载点这~~ 最基本的方式(不用组播不…

Unix/Linux编程:UDS 流(Stream)

〇、前言 socket 是一种 IPC &#xff08;Inter-Process Communication&#xff0c;进程间通信&#xff09;方法&#xff0c;它允许位于同一主机&#xff08;计算机&#xff09;或使用网络连接起来的不同主机上的应用程序之间交换数据。通过使用Socket&#xff0c;开发人员可以…

【C++】——栈和队列(stack、queue)及优先队列(priority_queue)的介绍和模拟实现

文章目录 1. 前言2. 容器适配器2.1 容器适配器的介绍2.2 STL标准库中stack和queue的底层结构2.3 deque的简单介绍2.4 deque的缺陷2.5 为什么选择deque作为stack和queue的底层默认容器 3. stack3.1 stack的介绍3.2 stack的使用3.3 stack模拟实现 4. queue4.1 queue的介绍4.2 que…

数据分布——长尾分布的处理

前言 长尾分布在分类任务中会提到这个名,这是因为长尾分布这个现象问题会导致在训练过程中会出现出错率高的问题&#xff0c;影响了实验结果。 这里要说的是&#xff0c;长尾分布是一种现象&#xff0c;有的地方说是一种理论或定律&#xff0c;我感觉这样说不太确切&#xff0…