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

高等图论

有向图的强连通分量

相关概念

  • 强连通分量: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编码

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++多态和文件读写

C黑马&#xff0c;每天1.5倍速2个视频&#xff08;1小时&#xff09;&#xff0c;看到9月1日完成314个视频 目录 &#x1f511;多态 &#x1f333;基本语法 &#x1f333;原理剖析 &#x1f333;案例1 -- 计算器类 &#x1f333;纯虚函数和抽象类 &#x1f333;案例2 --…

redis知识复习

redis知识复习 redis基础知识一. redis的认识1. 非关系型数据库 与 传统数据库 的区别2. 安装redis并设置自启动3. 熟悉命令行客户端4. 熟悉图形化工具RDM 二. redis的命令与数据结构1. 数据结构介绍2. redis通用命令&#xff08;熟练掌握&#xff09; 三. redis的Java客户端1.…

SpringBoot整合Flyway实现数据库的初始化和版本管理

文章目录 一、Flyway1、介绍2、业务痛点3、个人理解 二、SpringBoot整合flyway1、整合2、SQL文件命名3、版本号校验算法4、工作流程5、注意事项 一、Flyway 1、介绍 Flyway 是一款开源的数据库版本管理工具。它可以很方便的在命令行中使用&#xff0c;或者在Java应用程序中引入…

【MySQL】数据表的基本操作

目录 1. 创建表 2. 创建表案例 2.1 创建一个users表 2.2 查看表结构 2.3 修改表 3. 删除表 MySQL&#x1f337; 1. 创建表 语法&#xff1a; CREATE TABLE table_name (field1 datatype,field2 datatype,field3 datatype ) character set 字符集 collate 校验规则 engine 存储…

chatgpt赋能python:如何升级Python的pip版本

如何升级Python的pip版本 如果你使用Python来进行程序开发&#xff0c;那么你一定需要用到pip&#xff0c;它是Python的包管理器&#xff0c;用于安装和管理各种Python库。 不过&#xff0c;一旦你开始使用pip&#xff0c;你可能会遇到一个问题&#xff1a;你的pip版本可能会…

几种技巧让大模型(ChatGPT、文心一言)帮你提高写代码效率!

代码神器 自从大模型推出来之后&#xff0c;似乎没有什么工作是大模型不能做的。特别是在文本生成、文案写作、代码提示、代码生成、代码改错等方面都表现出不错的能力。下面我将介绍运用大模型写代码的几种方式&#xff0c;帮助程序员写出更好的代码&#xff01;&#xff08;…

利用AI点亮副业变现:5个变现实操案例的启示

AI变现副业实操案例 宝宝起名服务AI科技热点号头像壁纸职业头像收徒&#xff1a;萌娃头像定制头像平台挂载 小说推广号流量营销号百家号AI共创计划公众号流量主 知识付费知识星球小报童&#xff1a; 整体思维导图&#xff1a; 在这里先分享五个实操案例: 宝宝起名服务AI科技热…

cvte 前端一面 凉经

cvte 前端一面 凉经 原文面试题地址&#xff1a;https://www.nowcoder.com/discuss/353159272857018368?sourceSSRsearch 1. vuex原理 和vuerouter的原理差不多 2. vuerouter的原理 ​ 首先在main.js中&#xff0c;import router from ‘./router’ 引入在router文件夹下面…

学习WooCommerce跨境电商社交媒体营销

WooCommerce 长期以来一直为电子商务店主提供多样化的服务。大约 500 万家商店啓用安装了免费的 WooCommerce 插件。 官方 WooCommerce 插件从 WordPress.org 下载了161,908,802次&#xff0c;并且还在增加。 超过5,106,506 个网站正在使用 WooCommerce。 本文网址: https…

一文搞懂什么是Docker

一、什么是Docker 微服务虽然具备各种各样的优势&#xff0c;但服务的拆分通用给部署带来了很大的麻烦。 分布式系统中&#xff0c;依赖的组件非常多&#xff0c;不同组件之间部署时往往会产生一些冲突。在数百上千台服务中重复部署&#xff0c;环境不一定一致&#xff0c;会遇…

LVS+Keepalived 群集

目录 一、keepalived概述 1.keepalived工作原理 2.keepalived体系主要模块及其作用 3.判断服务器主备&#xff0c;及如何配置浮动IP 二、keepalived的抢占与非抢占模式 三、部署LVSkeepalived 1.配置负载调度器&#xff08;主备相同&#xff09; 1.1配置keepalived&…

NVM安装教程

我是小荣&#xff0c;给个赞鼓励下吧&#xff01; NVM安装教程 简介 nvm 是node.js的版本管理器&#xff0c;设计为按用户安装&#xff0c;并按 shell 调用。nvm适用于任何符合 POSIX 的 shell&#xff08;sh、dash、ksh、zsh、bash&#xff09;&#xff0c;特别是在这些平台…

chatgpt赋能python:Python编程:如何删除前面的代码?

Python编程&#xff1a;如何删除前面的代码&#xff1f; 在Python编程中&#xff0c;我们有时会需要删除之前写的一些代码&#xff0c;以便更好地组织我们的代码结构和逻辑。那么&#xff0c;Python中如何删除前面的代码呢&#xff1f;在本文章中&#xff0c;我们将为您详细介…

工程训练 -江苏海洋大学-mooc-最终答案

这不点赞评论一下嘛&#xff1f;&#xff1f;&#xff1f;呜呜呜 判断题&#xff08;共217道&#xff09; 1.舂实模样周围及砂箱边或狭窄部分的型砂&#xff0c;通常采用砂舂的平头端舂砂。 2.造型时&#xff0c;分型面上通常使用的是面砂&#xff0c;覆盖模样的则使用背砂。 3…

软件测试正在面试银行的可以看下这些面试题

前言 最近呢有很多的小伙伴问我有没有什么软件测试的面试题&#xff0c;由于笔者之前一直在忙工作上的事情&#xff0c;没有时间整理面试题&#xff0c;刚好最近休息了一下&#xff0c;顺便整理了一些面试题&#xff0c;现在就把整理的面试题分享给大家&#xff0c;废话就不多说…

网络层:IPv4地址

网络层&#xff1a;IPv4地址 笔记来源&#xff1a; 湖科大教书匠&#xff1a;IPv4地址概述 湖科大教书匠&#xff1a;分类编址的IPv4地址 湖科大教书匠&#xff1a;划分子网的IPv4地址 湖科大教书匠&#xff1a;无分类编址的IPv4地址 IPv4地址就是给因特网(Internet)上的每一…

利用WinDbg查看堆栈中方法入参的值4(C#)

由于作者水平有限&#xff0c;如有写得不对的地方&#xff0c;请指正。 使用WinDbg的过程中&#xff0c;坑特别的多&#xff0c;对版本要求比较严格&#xff0c;如&#xff1a; 1 32位应用程序导出的Dump文件要用32位的WinDbg打开&#xff0c;想要没有那么多的问题&#xf…

python字符串格式化通过占位符拼接

我之前写了python字符串拼接 但我们会发现 它不太好用 第一个 当变量很多的时候 会写的很长 第二个 是python中字符串不能直接和其他类型的变量拼接 字符串格式化 也属于是字符串拼接的一种方法 语法上不是使用加号 我们打开编辑器 编写代码如下 weight 8.70; age 2; name…

JVM零基础到高级实战之Java内存区域虚拟机栈

JVM零基础到高级实战之Java内存区域虚拟机栈 JVM零基础到高级实战之Java内存区域虚拟机栈 文章目录 JVM零基础到高级实战之Java内存区域虚拟机栈前言JVM内存模型之虚拟机栈总结 前言 JVM零基础到高级实战之Java内存区域虚拟机栈 JVM内存模型之虚拟机栈 虚拟机栈是什么&#x…

Python给一个exe执行文件注册持续性的快捷键(热键)的代码实例

本篇文章主要讲解通过python给一个exe文件绑定一个快捷键、并取消快捷键(热键)的实操方法。 日期:2023年6月11日 作者:任聪聪 实现按下快捷键即可启动软件的效果说明 启动软件注册热键呼出其他软件或本体的效果说明: 演示材料说明:在download文件目录下存放一个可执行的…