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

news2024/11/18 23:26:09

图论提高

最小生成树

(1)朴素版prim算法( O ( n 2 ) O(n ^ 2) O(n2)

  • 适用范围:稠密图
  • 易错:注意有向图还是无向图;注意有没有重边和负权边。
  • 从一个集合向外一个一个扩展,最开始只有 1 1 1 这个结点,然后把元素一个一个加进来,每次加的元素就是离这个集合距离最小的元素,可以设为 j j j。每次加进来后,就更新其他点到这个集合的距离,更新的数值就是其他点到 j j j 的边的长度。
int g[maxn][maxn], d[maxn], N, M;
bool vis[maxn];
int prim() {
	int res = 0;
	for (int i = 1; i <= N; i++) d[i] = INF;
	for (int i = 0; i < N; i++) {
		int t = -1;
		for (int j = 1; j <= N; j++) {
			if (!vis[j] && (t == -1 || d[t] > d[j])) {
				t = j;
			}
		}
		if (i && d[t] == INF) return INF;
		if (i) res += d[t];
		vis[t] = true;
		for (int j = 1; j <= N; j++) d[j] = min(d[j], g[t][j]);
	}
	return res;
}

(2)堆优化版prim算法( O ( m ∗ l o g n ) O(m * logn) O(mlogn)

  • 适用范围:稀疏图
  • 但是这个不怎么用得到。因为稀疏图通常用Kruskal算法,而且这个代码比较长

(2)Kruskal算法

  • 适用范围:稀疏图
int N, M, int p[maxn];
struct e{
	int u, v, w;
	bool operator <(const e& rhe) const{
		return w < rhe.w;
	}
}edges[maxm];

void init(int N) {
	for (int i = 1; i <= N; i++) p[i] = i;
}
int find(int x) {
	if (p[x] == x) return x;
	return p[x] = find(p[x]);
}
void unite(int a, int b) {
	if (find(a) == find(b)) return;
	p[find(a)] = find(b);
}

int kru() {
	init(N);
	int cnt = 0, res = 0;
	sort(edges, edges + M);
	for (int i = 0; i < M; i++) {
		auto p = edges[i];
		if (find(p.u) != find(p.v)) {
			unite(p.u, p.v);
			res += p.w;
			cnt++;
		}
	}
	if (cnt == N - 1) return res;
	return -1;
}

(3)最小生成树的应用

扩展最小生成树:346. 走廊泼水节

  • 题意:给定一棵 n ( n ≤ 6000 ) n(n \le 6000) n(n6000) 个节点的树,要求增加若干条边,把这棵树扩充为完全图,并满足图的唯一最小生成树仍然是这棵树。求增加的边的权值总和最小是多少。
  • 每次在两个集合之间连边的时候,连的边权是 w i w_i wi,那我们构造完全图的时候,新加的边长是 w i + 1 w_i + 1 wi+1
  • unite那个函数千万别写错!一定要先改变 s z sz sz,再改变 p p p,不然就会出问题!
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 6010;
int p[maxn], sz[maxn], N, M;
void init(int N) 
{
	for (int i = 1; i <= N; i++) 
    {
		p[i] = i, sz[i] = 1;
	}
}
int find(int x) 
{
	if (p[x] == x) return x;
	return p[x] = find(p[x]);
}
void unite(int a, int b) 
{
	if (find(a) == find(b)) return; 
	sz[find(b)] += sz[find(a)];
	p[find(a)] = find(b);
}
struct P 
{
	int u, v, cost;
	bool operator<(const P& rhp) 
    {
		return cost < rhp.cost;
	}
}G[maxn];
void solve() 
{
	init(N);
	sort(G, G + M);
	int ans = 0;
	for (int i = 0; i < M; i++) 
    {
		int u = G[i].u, v = G[i].v, c = G[i].cost;
		if (find(u) == find(v)) continue;
		ans += (sz[find(u)] * sz[find(v)] - 1) * (c + 1);
		unite(u, v);
	}
	printf("%d\n", ans);
}
int main() 
{
	int T;
	scanf("%d", &T);
	while (T--) 
    {
		scanf("%d", &N);
		M = N - 1;
		for (int i = 0; i < M; i++) 
        {
			scanf("%d%d%d", &G[i].u, &G[i].v, &G[i].cost);
		}
		solve();
	}
	return 0;
}

次小生成树:1148. 秘密的牛奶运输

n ( n ≤ 500 ) n(n \le 500) n(n500) 个点, m ( m ≤ 1 0 4 ) m(m \le 10^4) m(m104) 条边,边长不超过 1 0 9 10^9 109,问次小生成树的值是多少.

  • 次小生成树:把一个带权的图,把图的所有生成树按权值从小到大排序,第二小的成为次小生成树。严格次小生成树是边权和严格大一点,非严格次小生成树是指边权和可以相等。
  • 法一(只能求非严格的次小生成树):先求最小生成树,再枚举删去最小生成树中的边求解。时间复杂度。 O ( m l o g m + n m ) O(mlogm + nm) O(mlogm+nm)
  • 法二:先求最小生成树(严格与非严格的都可以求),然后依次枚举非树边,然后将该边加入树中,同时从树中去掉一条边, 使得最终的图仍是一棵树。则一定可以求出次小生成树。 O ( m + n 2 + m l o g m ) O(m + n^2 + mlogm) O(m+n2+mlogm)
  • 法二的原理:设T为图G的一棵生成树,对于非树边a和树边b,插入边a,并删除边b的操作记为 ( + a , − b ) (+a, -b) (+a,b)。如果T+a-b之后,仍然是一棵生成树,称 ( + a , − b ) (+a,-b) (+a,b) T T T 的一个可行交换。称由 T T T 进行一次可行变换所得到的新的生成树集合称为 T T T 的邻集。定理:(严格与非严格)次小生成树一定在最小生成树的邻集中。
  • 步骤:
  1. 求最小生成树,统计标记每条边是树边,还是非树边;同时把最小生成树建立出来。
  2. 预处理任意两点间的边权最大值 d 1 [ a ] [ b ] d1[a][b] d1[a][b]与次大值 d 2 [ a ] [ b ] d2[a][b] d2[a][b]
  3. 依次枚举所有非树边,求 m i n ( s u m + w − d 1 [ a ] [ b ] ) min(sum +w - d1[a][b]) min(sum+wd1[a][b]), 满足 w > d 2 [ a ] [ b ] w > d2[a][b] w>d2[a][b],其中 d i s t [ a ] [ b ] dist[a][b] dist[a][b]是节点a和节点b的在树上的所经路径最大边的边权,w是非树边。若 w = = d 2 [ a ] [ b ] w == d2[a][b] w==d2[a][b],则去处理次大边。
    (注:如果加上的边和两点之间最大边长度相等,那么只能求非严格次小生成树。若要求严格次小生成树,还要在上述情况出现的情况下,改为去掉次大边)。
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 510, maxm = 10010;
typedef long long ll;
int h[maxn], ne[maxn * 2], e[maxn * 2], w[maxn * 2], idx;
int N, M, p[maxn], d1[maxn][maxn], d2[maxn][maxn];
ll sum;
struct P {
	int u, v, c;
	bool f;
	bool operator<(const P& rhp)const {
		return c < rhp.c;
	}
}G[maxm];
void init(int N) {
	for (int i = 1; i <= N; i++) p[i] = i;
}
int find(int x) {
	if (p[x] == x) return x;
	return p[x] = find(p[x]);
}
void unite(int a, int b) {
	if (find(a) == find(b)) return;
	p[find(a)] = find(b);
}
void add(int a, int b, int c) {
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
void mst() {
	init(N);
	sort(G, G + M);
	for (int i = 0; i < M; i++) {
		int u = G[i].u, v = G[i].v, c = G[i].c;
		if (find(u) == find(v)) continue;
		unite(u, v);
		G[i].f = 1;
		sum += c;
		add(u, v, c), add(v, u, c);
	}
}
void dfs(int u, int fa, int max1, int max2, int d1[], int d2[]) {
	//一旦出现最大值,就把最大值变成次大值。
	d1[u] = max1, d2[u] = max2;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (v != fa) {
			int t1 = max1, t2 = max2;
			if (w[i] > max1) t2 = max1, t1 = w[i];
			else if (w[i] < max1 && w[i] > max2) t2 = w[i];
			dfs(v, u, t1, t2, d1, d2);
		}
	}
}
int main() {
	memset(h, -1, sizeof h);
	scanf("%d%d", &N, &M);
	for (int i = 0; i < M; i++) {
		scanf("%d%d%d", &G[i].u, &G[i].v, &G[i].c);
	}
	mst();
	for (int i = 1; i <= N; i++) dfs(i, -1, 0, 0, d1[i], d2[i]);
	ll ans = 1e18;
	for (int i = 0; i < M; i++) {
		int u = G[i].u, v = G[i].v, c = G[i].c;
		//printf("### %d %d %d %d %d %d\n", u, v, c, G[i].f, d1[u][v], d2[u][v]);
		if (!G[i].f) {
			if(c > d1[u][v]) ans = min(ans, sum + c - d1[u][v]);
			else if (c == d1[u][v] && c > d2[u][v]) ans = min(ans, sum + c - d2[u][v]);
		}
	}
	printf("%lld\n", ans);
	return 0;
}

二分图

概念

  • 一个图是二分图,等价于图不含有奇数环(环的边数是奇数),也等价于染色的过程一定是没有矛盾。
  • 二分图:就是顶点集 V V V 可分割为两个互不相交的子集,并且图中每条边依附的两个顶点都分属于这两个互不相交的子集,两个子集内的顶点不相邻。
  • 二分图的匹配:给定一个二分图 G G G,在 G G G 的一个子图M中, M M M 的边集 { E } \{E\} {E} 中的任意两条边都不依附于同一个顶点,则称M是一个匹配。
  • 二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数
  • 匹配点:二分图匹配中,被匹配的点。
  • 增广路径:若P是图G中一条连通两个未匹配顶点的路径,并且属于M的边和不属于M的边(即已匹配和待匹配的边)在P上交替出现,则称P为相对于M的一条增广路径。
  • 最大匹配等价于不存在增广路径。
  • 在二分图中,最大匹配数 = 最小点覆盖 = 总点数 - 最大独立集 = 总点数 - 最小路径覆盖
  • 点覆盖,对于图G=(V,E),选出一个点集S⊆V,使得E中每一条边至少有一个端点在S中。而最小点覆盖是点集最小的情况。在二分图中,最大匹配数 = 最小点覆盖。
  • 最大独立集:选出最多的点,使得选出的点之间没有边
  • 最大团:选出最多的点,使得选出的点之间都有边
  • 原图的最大独立集就是补图的最大团。
  • 在二分图中求最大独立集 ( n − m ) (n - m) (nm) ⇔ \Leftrightarrow 去掉最少的点,将这些点拿去之后,图中所有的边都会去掉 ( m ) (m) (m) ⇔ \Leftrightarrow 找到最小点覆盖 ( m ) (m) (m) ⇔ \Leftrightarrow 找到最大匹配 ( m ) (m) (m).
  • 最小不相交路径覆盖(最小路径点覆盖):给一个有向图 G = ( V , E ) G=(V, E) G=(V,E),找一些没有任何重复点的路径,使得这些路径经过了每一个点。注意路径的长度可以是 0 0 0,即只包含一个节点。
  • 我们关心的时求 D A G DAG DAG 的最小路径点覆盖。
  • 拆点:把每个点拆成入点和出点两个,原图中每一条 i → j i \rightarrow j ij,都变成 i → j ′ i \rightarrow j' ij。新图就变成二分图了(左部的点是入点,右部的点是出点)。
  • 每一条路径终点与左侧非匹配点一一对应。我们的目的是让左侧非匹配点最少 ( n − m ) (n - m) (nm),等价于让左侧匹配点最多(m),那么就是最大匹配 ( m ) (m) (m)
  • 最小可相交路径覆盖(最小路径重复点覆盖):与上面的有点不同的是,这个问题的不同路径可以包含相同的点和边,
  • 这种问题,可以先求出传递闭包 G ′ G' G,然后再 G ′ G' G 上求最小不相交路径覆盖。

(1)染色法(dfs)(O(m + n))

  • 注意啊,二分图的重要性是在概念与性质,染色只是判定方法。
  • 易错:h别忘初始化,以及初始化的位置一定要在输入边之前。另外一定把color涂成1或-1,别涂成0或1,否则不能区分有哪些点还没涂。
int N, M, color[maxn];
int h[maxn], e[maxm], ne[maxm], idx;
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool dfs(int u, int c) {
	color[u] = c;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (!color[v] && !dfs(v, -c)) return false;
		else if (color[v] == c) return false;
	}
	return true;
}
void check() {
	for (int i = 1; i <= N; i++) {
		if (!color[i]) {
			if (!dfs(i, 1)) {
				printf("No\n");
				return;
			}
		}
	}
	printf("Yes\n");
}

257. 关押罪犯

  • 题意:有 n n n 个罪犯,某些罪犯之间有怨气 c c c(关在同一监狱会产生规模大小 c c c 的冲突),现在要把罪犯分开关到两个监狱中,通过合理分配,问他们产生冲突最大值 最小是多少?
法一
  • 我们希望把冲突值不大于x的可以关在一个房间里,把大于x的必须关在两个房间里。因此,就是边权不大于x的可以在二分图的一侧,但是大于x的两个顶点必须属于二分图的两侧。因此图中只需要保留不大于x的边就行。
  • 新图中没有边代表关在两个监狱之中,没有边的话会冲突,那么关在同一监狱之中。我们希望冲突都是小于 a n s ans ans 的,那么大于 a n s ans ans 时都认为没有冲突,那么大于 a n s ans ans 的边的两个顶点必须在两个监狱之中。那么,问题变成了,把所有大于 a n s ans ans 的边全部筛选出来,看看这个新图能否分成两部分。
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 20010, maxm = 200010;
int h[maxn], e[maxm], ne[maxm], w[maxm], idx;
int N, M, color[maxn];
void add(int a, int b, int c) {
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
bool dfs(int u, int c, int x) {
	color[u] = c;
	for (int i = h[u]; i != -1; i = ne[i]) {
		if (w[i] <= x) continue;
		int v = e[i];
		//小心这里的!dfs(v, -c, x), 千万别写错!
		if (!color[v] && !dfs(v, -c, x)) return false;
		else if (color[v] == c) return false;
	}
	return true;
}
bool C(int x) {
	memset(color, 0, sizeof color);
	for (int i = 1; i <= N; i++) {
		if (!color[i]) {
			if (!dfs(i, 1, x)) return false;
		}
	}
	return true;
}
int main() {
	scanf("%d%d", &N, &M);
	memset(h, -1, sizeof h);
	for (int i = 0; i < M; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c), add(b, a, c);
	}
	int lb = -1, ub = 1e9 + 1;
	while (ub - lb > 1) {
		int mid = (lb + ub) / 2;
		if (C(mid)) ub = mid;
		else lb = mid;
	}
	printf("%d\n", ub);
	return 0;
}
法二
  • 贪心+二分、并查集

  • 这个还是依附于二分图。我们从大到小把边的权值进行排序。然后,由于我们尽可能让矛盾大的两个罪犯关到不同的监狱里,因此,把边的权值从大到小遍历,并分别加到二分图的两个不同的位置。一旦发现冲突的时候,就说明可以输出答案了。

  • 可以画一个只有 1 , 2 , 3 1,2,3 1,2,3 的例子, 1 1 1 2 2 2 冲突, 2 2 2 3 3 3 冲突, 1 1 1 3 3 3 冲突,那么我们只能保证其中的两个不会发生冲突,那么第三个冲突发生的时候,我们会发现他们两个的虚点已经在同一个并查集中了.

  • 这样,我们利用虚点就可以判断了。注意处理边界!如N == 1的时候。

  • 什么是虚点?虚点怎么用?AcWing 257题解

#include<cstdio>
#include<algorithm>
using namespace std;
const int maxm = 100010;
int p[40010];
void init(int N) {
	for (int i = 1; i <= N; i++) p[i] = i;
}
int find(int x) {
	if (x == p[x]) return x;
	return p[x] = find(p[x]);
}
void unite(int x, int y) {
	if (find(x) == find(y)) return;
	p[find(x)] = find(y);
}
struct e {
	int u, v, w;
	bool operator < (const e& rhe)const {
		return w > rhe.w;
	}
}G[maxm];
int main() {
	int N, M;
	scanf("%d%d", &N, &M);
	for (int i = 0; i < M; i++) {
		scanf("%d%d%d", &G[i].u, &G[i].v, &G[i].w);
	}
	sort(G, G + M);
	init(2 * N);
	for (int i = 0; i < M; i++) {
		int px = find(G[i].u), py = find(G[i].v);
		if (px == py) {
			printf("%d\n", G[i].w);
			return 0;
		}
		p[px] = find(G[i].v + N);
		p[py] = find(G[i].u + N);
	}
	printf("%d\n", 0);
	return 0;
}

(2)匈牙利算法O(mn)

必须要指出,匈牙利算法建立的都是单向边!

861. 二分图的最大匹配

  • 给定一个二分图,其中左半部包含 n 1 n1 n1 个点(编号 1 ∼ n 1 1∼n_1 1n1),右半部包含 n 2 n_2 n2 个点(编号 1 ∼ n 2 1∼n_2 1n2),二分图共包含 m m m 条边。数据保证任意一条边的两个端点都不可能在同一部分中。请你求出二分图的最大匹配数。

  • 一般运行时间都远小于 O ( m n ) O(mn) O(mn)

  • 这个算法就是求二分图的最大匹配数,即匹配的边数。尽管 d f s dfs dfs 是从顶点出发,但是最后的 a n s ans ans 就是最大匹配的边数。

  • 有一个需要强调一下,尽管二分图是无向图问题,但是我们处理的时候都是将一条无向边转化为一条有向边

  • 一定要小心,这个算法的节点编号是从1开始的,如果从0开始,就得把match数组初始化为-1,在改动一下相关部分

int h[maxn], e[maxm], ne[maxm], idx;
int N1, N2, M, match[maxn];
bool vis[maxn];
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool find(int u) {
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (!vis[v]) {
			vis[v] = true;
			//小心这个地方是find(match[v]), 极易错!
			if (match[v] == 0 || find(match[v])) {
				match[v] = u;
				return true;
			}
		}
	}
	return false;
}
int main() {
	scanf("%d%d%d", &N1, &N2, &M);
	memset(h, -1, sizeof h);
	while(M--) {
		int a, b;
		scanf("%d%d", &a, &b);
		add(a, b);
	}
	int ans = 0;
	for (int i = 1; i <= N1; i++) {
		memset(vis, false, sizeof vis);
		if (find(i)) ans++;
	}
	printf("%d\n", ans);
	return 0;
}

372. 棋盘覆盖

  • 给定一个 N 行 N 列的棋盘,已知某些格子禁止放置。求最多能往棋盘上放多少块的长度为 2、宽度为 1 的骨牌,骨牌的边界与格线重合(骨牌占用两个格子),并且任意两张骨牌都不重叠。

  • i + j i + j i+j 是奇数时与 i + j i + j i+j 是偶数时的格子是两两相邻的。而奇偶性相同时是不相邻的。这就是一个二分图。

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 110;
typedef pair<int, int> P;
P match[maxn][maxn];
int N, M;
bool vis[maxn][maxn], g[maxn][maxn];
int dx[] = { 1, -1, 0, 0 }, dy[] = { 0, 0, 1, -1 };
bool find(int x, int y) {
	for (int i = 0; i < 4; i++) {
		int nx = x + dx[i], ny = y + dy[i];
		if (nx < 1 || nx > N || ny < 1 || ny > N) continue;
		if (vis[nx][ny] || g[nx][ny]) continue;
		vis[nx][ny] = true;
		P t = match[nx][ny];
		if (!t.first || find(t.first, t.second)) {
			//小心!make_pair(x, y)别写错!
			match[nx][ny] = make_pair(x, y);
			return true;
		}
	}
	return false;
}
int main() {
	scanf("%d%d", &N, &M);
	while (M--) {
		int a, b;
		scanf("%d%d", &a, &b);
		g[a][b] = true;
	}
	int ans = 0;
	for (int i = 1; i <= N; i++) {
		for (int j = 1; j <= N; j++) {
			if ((i + j) % 2 && !g[i][j]) {
				memset(vis, 0, sizeof vis);
				if (find(i, j)) ans++;
			}
		}
	}
	printf("%d\n", ans);
	return 0;
}

(3)二分图最小点覆盖

  • 最小点覆盖 = 最大匹配数
  • 其实要注意到,二分图的匹配,匹配点的数量是匹配边数量的两倍。因为每个匹配点有且仅有一条匹配的边与之相连。
  • 最小点覆盖可以这样构造:求最大匹配;从左部的每个非匹配点出发,做一遍增广路径,标记所有经过的点;选出左部所有未被标记的点,右部所有被标记的点。
  • 原理:左部所有非匹配的点,一定被标记;右边所有非匹配的点,一定不被标记。对于每个匹配边,要么两个端点同时被标记,要么同时不被标记;对于所有非匹配边,两种情况:(1)左边非匹配点 -> 右边匹配点,此时右边一定会被标记,因此会被选出;(2)左边匹配点 -> 右边非匹配点,左边的一定不被标记,仍然可以选出。

376. 机器任务

  • 有两台机器 A , B A,B A,B 以及 K K K 个任务。机器 A A A 有 N 种不同的模式(模式 0 ∼ N − 1 0∼N−1 0N1),机器 B B B M M M 种不同的模式(模式 0 ∼ M − 1 0∼M−1 0M1)。两台机器最开始都处于模式 0。每个任务既可以在 A A A 上执行,也可以在 B B B 上执行。对于每个任务 i i i,给定两个整数 a [ i ] a[i] a[i] b [ i ] b[i] b[i],表示如果该任务在 A A A 上执行,需要设置模式为 a [ i ] a[i] a[i],如果在 B B B 上执行,需要模式为 b [ i ] b[i] b[i]。任务可以以任意顺序被执行,但每台机器转换一次模式就要重启一次。求怎样分配任务并合理安排顺序,能使机器重启次数最少。

  • 仔细分析,这个类似最小点覆盖的问题。但是有两点需要注意:

  1. 编号是从 0 0 0 开始的, m a t c h match match 要初始化为 − 1 -1 1
  2. a a a b b b 的编号为 0 0 0 时不用考虑,因为 0 0 0 时不用转换机器。这个要是忽略掉的话,结果有可能会比答案多 1 1 1
#include<bits/stdc++.h>
using namespace std;
const int maxn = 110, maxm = 2010;
int h[maxn], e[maxm], ne[maxm], idx;
int N1, N2, M, match[maxn];
bool vis[maxn];
void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool find(int u) {
    for (int i = h[u]; i != -1; i = ne[i]) {
        int v = e[i];
        if (!vis[v]) {
            vis[v] = true;
            if (match[v] == -1 || find(match[v])) {
                match[v] = u;
                return true;
            }
        }
    }
    return false;
}
int main() {
    while (scanf("%d", &N1) && N1) {
        memset(h, -1, sizeof h);
        memset(match, -1, sizeof match);
        idx = 0;
        scanf("%d%d", &N2, &M);
        for (int i = 0; i < M; i++) {
            int id, a, b;
            scanf("%d%d%d", &id, &a, &b);
            //0的时候不用转换状态。
            if (a == 0 || b == 0) continue;
            add(a, b);
        }
        int ans = 0;
        for (int i = 1; i < N1; i++) {
            memset(vis, 0, sizeof vis);
            if (find(i)) ans++;
        }
        printf("%d\n", ans);
    }
    return 0;
}

(4)二分图最大独立集

378. 骑士放置

  • 给定一个 N×M 的棋盘,有一些格子禁止放棋子。问棋盘上最多能放多少个不能互相攻击的骑士(国际象棋的“骑士”,类似于中国象棋的“马”,按照“日”字攻击,但没有中国象棋“别马腿”的规则)。

  • 在一个N * M的棋盘上,可以放多少个国际象棋中的骑士。骑士攻击

  • 由此可以看出,骑士每跳一次,格子的奇偶性就会改变。这个问题就等价于,能选出多少个格子,使得两两之间没有边。最终答案就是:N * M - k - 最大匹配数量

  • 一般的图中求最大独立集是一个 NP-Hard 问题,只有二分图才可以用匈牙利算法。

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 110;
typedef pair<int, int> P;
P match[maxn][maxn];
bool g[maxn][maxn], vis[maxn][maxn];
int N, M, K;
int dx[] = { -1, -2, -2, -1, 1, 2, 2, 1 }, dy[] = { -2, -1, 1, 2, 2, 1, -1, -2 };
bool find(int x, int y) {
	for (int i = 0; i < 8; i++) {
		int nx = x + dx[i], ny = y + dy[i];
		if (nx < 1 || nx > N || ny < 1 || ny > M) continue;
		if (vis[nx][ny] || g[nx][ny]) continue;
		vis[nx][ny] = true;
 		P t = match[nx][ny];
		if (t.first == 0 || find(t.first, t.second)) {
			match[nx][ny] = { x, y };
			return true;
		}
	}
	return false;
}
int main() {
	scanf("%d%d%d", &N, &M, &K);
	for(int i = 0; i < K; i++) {
		int x, y;
		scanf("%d%d", &x, &y);
		g[x][y] = true;
	}
	int res = 0;
	for (int i = 1; i <= N; i++) {
		for (int j = 1; j <= M; j++) {
		//这个地方写成 (i + j) % 2 == 1 也是可以过的。
			if ((i + j) % 2 || g[i][j]) continue;
			memset(vis, 0, sizeof vis);
			if (find(i, j)) res++;
		}
	}
	printf("%d\n", N * M - K - res);
	return 0;
}

(5)有向无环图的最小路径点覆盖

379. 捉迷藏

  • 题意:求一个 D A G DAG DAG 中最多可以挑出多少个点,使得这些点两两不可达。
  • 有向无环图本身就是二分图,而且图都给你搭建好了,无需再考虑怎么把点集分成两个集合然后连边了(那个是建图的时候才需要考虑的问题),跑一遍传递闭包算法然后就可以直接匹配了。
  • 答案就是这个图的最小路径重复点覆盖为 m m m,则答案就是 N − m N-m Nm.
#include<bits/stdc++.h>
using namespace std;
const int maxn = 210;
bool d[maxn][maxn], vis[maxn];
int N, M, match[maxn];
void Floyd_Warshall() {
	for (int k = 1; k <= N; k++) {
		for (int i = 1; i <= N; i++) {
			for (int j = 1; j <= N; j++) d[i][j] |= d[i][k] & d[k][j];
		}
	}
}
bool find(int u) {
	for (int i = 1; i <= N; i++) {
		if (d[u][i] && !vis[i]) {
			vis[i] = true;
			//一开始又双叒叕把 find(match[i]) 写错了!看清楚!
			if (!match[i] || find(match[i])) {
				match[i] = u;
				return true;
			}
		}
	}
	return false;
}
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 0; i < M; i++) {
		int a, b;
		scanf("%d%d", &a, &b);
		d[a][b] = true;
	}
	Floyd_Warshall();
	int res = 0;
	for (int i = 1; i <= N; i++) {
		memset(vis, 0, sizeof vis);
		if (find(i)) res++;
	}
	printf("%d\n", N - res);
	return 0;
}

差分约束

  • 对于最短路问题,那么图中有一条从 u u u v v v 的边,等价于不等式 d i s v ≤ d i s u + w ( u , v ) dis_v \le dis_u + w(u,v) disvdisu+w(u,v). 因此一个最短路问题等价于一大堆 ≤ \le 不等式,而最短路的话,就是一个最小的上界。
  • 对于最长路问题,那么图中有一条从 u u u v v v 的边,等价于不等式 d i s v ≥ d i s u + w ( u , v ) dis_v \ge dis_u + w(u,v) disvdisu+w(u,v). 因此一个最长路问题等价于一大堆 ≥ \ge 不等式,而最长路的话,就是一个最大的下界。

步骤

求不等式组的可行解

  • 源点需要满足的条件:从源点出发,一定可以走到所有的边.
  • 步骤:
  1. 先将每个不等式 x i ≤ x j + c k x_i \le x_j + c_k xixj+ck,转化成一条 x j x_j xj 走到 x i x_i xi,长度为 c k c_k ck 的一条边。注意一定要转化为不等关系。
  2. 找一个超级源点,使得该源点一定可以遍历到所有边。
  3. 从源点求一遍单源最短路:如果存在负环,则原不等式组一定无解;如果没有负环,则 d i s t [ i ] dist[i] dist[i] 就是原不等式组的一个可行解。
    注:如果是转化为最长路问题的话,那么就是寻找是否存在正环。存在正环就是无解。

**解释:如果是求最大值,那么我们有一系列 x n ≤ x 0 + ∑ a i c i x_n \le x_0 + \sum\limits a_ic_i xnx0+aici 不等式,那么要满足所有不等式的话,就要满足最小上界,因此是求最小值,即求最短路. **

  • 如何求最大值或者最小值(最大的可行解或是最小的可行解):

  • 结论:如果是求最小值,则应该求最长路;如果是求最大值,则应该求最短路。

  • 问题:如何转化 x i ≤ C x_i \le C xiC,其中 c c c 是一个常数,这类的不等式

  • 方法:建立一个超级源点, 0,然后建立 0 → i 0 \rightarrow i 0i, 长度是 c c c 的边即可。

  • 以求 x i x_i xi 的最大值为例:求所有从 x i x_i xi 出发,构成的不等式链 x i ≤ x j + c 1 ≤ x k + c 2 + c 1 ≤ . . . ≤ c 1 + c 2 + . . . + c k x_i \le x_j+ c_1 \le x_k+c_2+c_1 \le... \le c_1+c_2+... + c_k xixj+c1xk+c2+c1...c1+c2+...+ck
    所计算出的上界,最终 x i x_i xi 的最大值等于所有上界的最小值。

  • 还是要强调一下,求负环的话还是要优先选择朴素的方法,用队列与cnt数组来实现。不过TLE的话还是改成栈吧。

  • 总之:最小值问题,转化为一系列大于等于不等式,并求最长路;最大值问题,转化为一系列小于等于不等式,并求最短路。

1.1170. 排队布局

n ( n ≤ 1000 ) n(n \le 1000) n(n1000) 个点摆放位置(这些点要按照编号在数轴上排成一队),但是点与点之间的距离需要满足一定的条件: M L ( 1 ≤ M L ≤ 10 , 000 ) ML(1 \le ML \le 10,000) ML(1ML10,000) 描述了某两个点之间的距离不能超过多少; M D ( 1 ≤ M D ≤ 10 , 000 ) MD(1 \le MD \le 10,000) MD(1MD10,000) 描述了某两个点之间的距离不能少于多少。计算 1 1 1 号点和 n n n 号点的最大距离.输出一个整数,如果不存在满足要求的方案,输出-1;如果 1 号奶牛和 N 号奶牛间的距离可以任意大,输出-2;
x 0 ≤ x i − 1 x j ≤ x i + c ( i < j ) x i ≤ x j − c ( i < j ) x i − 1 ≤ x i x_0 \le x_i - 1 \\ x_j \le x_i + c \quad (i < j) \\ x_i \le x_j - c \quad (i < j) \\ x_{i-1} \le x_i x0xi1xjxi+c(i<j)xixjc(i<j)xi1xi

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1010, maxm = 21010;
const ll INF = 1e18;
int h[maxn], e[maxm], ne[maxm], w[maxm], idx;
int N, M1, M2, cnt[maxn];
ll d[maxn];
bool vis[maxn];
void add(int a, int b, int c) {
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
ll spfa() {
	queue<int> que;
	fill(d, d + maxn, INF);
	d[1] = 0;
	for (int i = 1; i <= N; i++) 
    {
		vis[i] = true;
		que.push(i);
	}
	while (que.size()) 
    {
		int u = que.front(); que.pop();
		vis[u] = false;
		for (int i = h[u]; i != -1; i = ne[i]) 
        {
			int v = e[i];
			if (d[v] > d[u] + w[i]) {
				d[v] = d[u] + w[i];
				cnt[v] = cnt[u] + 1;
				if (cnt[v] >= N) return -1;
				if (!vis[v]) {
					vis[v] = true;
					que.push(v);
				}
			}
		}
	}
	if (d[N] >= INF / 2) return -2;
	return d[N];
}
int main() {
	memset(h, -1, sizeof h);
	scanf("%d%d%d", &N, &M1, &M2);
	for (int i = 0; i < M1; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		if (a > b) swap(a, b);
		add(a, b, c);
	}
	for (int i = 0; i < M2; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		if (a > b) swap(a, b);
		add(b, a, -c);
	}
	for (int i = 2; i <= N; i++) {
		add(i, i - 1, 0);
	}
	ll ans = spfa();
	printf("%lld\n", ans);
	return 0;
}

1169. 糖果

在这里插入图片描述

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<stack>
using namespace std;
typedef long long ll;
//这个maxm必须开到300010,因为x=1时会加双向边,而且每一个节点与0都要连一条边。
const int maxn = 100010, maxm = 300010;
const ll INF = 1e16;
int h[maxn], e[maxm], ne[maxm], w[maxm], idx;
int N, M, cnt[maxn];
bool vis[maxn];
ll d[maxn];
void add(int a, int b, int c) {
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
bool spfa() {
	stack<int> s;
	fill(d, d + maxn, -INF);
	//当时spfa求负环把所有点都加到队列里面的一个重要原因是图可能是不连通的。
	//但是这个图从0节点一定可以遍历完所有节点,因此只把0节点加入栈中就可以。
	d[0] = 0, s.push(0), vis[0] = true;
	while (s.size()) {
		int u = s.top(); s.pop();
		vis[u] = false;
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			if (d[v] < d[u] + w[i]) {
				d[v] = d[u] + w[i];
				cnt[v] = cnt[u] + 1;
				//注意,因为加上0号节点了,所以是N + 1个点。
				if (cnt[v] >= N + 1) return true;
				if (!vis[v]) {
					s.push(v);
					vis[v] = true;
				}
			}
		}
	}
	return false;
}
int main() {
	memset(h, -1, sizeof h);
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; i++) add(0, i, 1);
	for (int i = 0; i < M; i++) {
		int x, a, b;
		scanf("%d%d%d", &x, &a, &b);
		if (x == 1) add(a, b, 0), add(b, a, 0);
		else if (x == 2) add(a, b, 1);
		else if (x == 3) add(b, a, 0);
		else if (x == 4) add(b, a, 1);
		else if (x == 5) add(a, b, 0);
	}
	if (spfa()) printf("-1\n");
	else {
		ll ans = 0;
		for (int i = 1; i <= N; i++) ans += d[i];
		printf("%lld\n", ans);
	}
	return 0;
}

最近公共祖先

在线求LCA:1172. 祖孙询问

  • 题意:给两个点,询问两个点是否一个是另一个的祖先。
  • 倍增法:
  • f a [ i , j ] fa[i, j] fa[i,j] 表示从 i i i 开始,向上走 2 j 2^j 2j 能走到的节点 ( 0 ≤ j ≤ log ⁡ n ) (0 \le j \le \log n) (0jlogn); d e p t h [ i ] depth[i] depth[i] 表示 i i i 的深度。
  • 哨兵:从 i i i 开始跳 2 j 2^j 2j 会跳过根节点,则定义 f [ i , j ] = 0 f[i, j] = 0 f[i,j]=0, 且 d e p t h [ 0 ] = 0 depth[0] = 0 depth[0]=0.
  • 步骤:先将两个点跳到同一层。若两个点不是同一点让两个点同时往上跳,知道跳到最近公共祖先的下一层(即最近公共祖先的儿子)。
  • 预处理: O ( n ∗ l o g n ) O(n*logn) O(nlogn);查询: O ( l o g n ) O(logn) O(logn)
  • f a [ n ] [ k ] fa[n][k] fa[n][k] 第二维数组大小: l o g 2 ( m a x n ) log_2(maxn) log2(maxn) 向下取整,比如 l o g 2 ( 100000 ) = 16 log_2(100000)=16 log2(100000)=16,所以 k 取值是 0~16,所以第二维开17即可。
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
const int maxn = 40010, maxm = maxn * 2;
int h[maxn], ne[maxm], e[maxm], idx;
int N, M, root;
int depth[maxn], fa[maxn][16];
void bfs() {
	memset(depth, 0x3f, sizeof depth);
	depth[0] = 0, depth[root] = 1;
	queue<int> que;
	que.push(root);
	while (que.size()) {
		int u = que.front(); que.pop();
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			if (depth[v] > depth[u] + 1) {
				depth[v] = depth[u] + 1;
				que.push(v);
				fa[v][0] = u;
				for (int k = 1; k <= 15; k++) fa[v][k] = fa[fa[v][k - 1]][k - 1];
			}
		}
	}
}

void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int lca(int a, int b) {
	if (depth[a] < depth[b]) swap(a, b);
	//二次方拼凑一定要倒着拼凑。
	for (int k = 15; k >= 0; k--) {
		if (depth[fa[a][k]] >= depth[b]) a = fa[a][k];
	}
	if (a == b) return a;
	for (int k = 15; k >= 0; k--) {
		if (fa[a][k] != fa[b][k]) {
			a = fa[a][k];
			b = fa[b][k];
		}
	}
	//返回a的节点,即a的公共祖先。
	return fa[a][0];
}
int main() {
	scanf("%d", &N);
	memset(h, -1, sizeof h);
	for (int i = 0; i < N; i++) {
		int a, b;
		scanf("%d%d", &a, &b);
		if (b == -1) root = a;
		else add(a, b), add(b, a);
	}
	bfs();
	scanf("%d", &M);
	while (M--) {
		int a, b;
		scanf("%d%d", &a, &b);
		int p = lca(a, b);
		if (p == a) printf("1\n");
		else if (p == b) printf("2\n");
		else printf("0\n");
	}
	return 0;
}

离线求LCA:1171. 距离

  • 题意:多次询问树上两点之间的距离(树上两个节点的距离是唯一的)。边是无向的,节点的编号是从 1 ∼ n 1 \sim n 1n.
  • 在线做法:读入一个询问,输出一个结果。离线做法:读入所有询问,统一输出结果。
  • T a r j a n Tarjan Tarjan——离线LCA: O ( n + m ) O(n + m) O(n+m)
  • 这个算法的思想是:
    • 在深度优先搜索的过程中,把所有节点分为三大类:已经遍历过,且回溯过的点(标记为2);正在搜索的分支(标记为1);还未搜索到的点(标记为0)。
    • 保存下来所有询问,每次询问的时候,一定是一个结点是第2类,一个结点是第1类

在这里插入图片描述

  • 正在搜索的节点 u u u,与询问相关 v v v,最近公共祖先记为 p [ v ] p[v] p[v],距离就是 d [ u ] + d [ v ] − d [ p [ v ] ] ∗ 2 d[u] + d[v] - d[p[v]] * 2 d[u]+d[v]d[p[v]]2
#include<cstdio>
#include<algorithm>
#include<vector>
#include<cstring>
using namespace std;
typedef pair<int, int> P;
const int maxn = 10010, maxm = maxn  * 2;
int h[maxn], ne[maxm], e[maxm], w[maxm], idx;
int st[maxn], N, M, p[maxn], ans[maxm], d[maxn];
vector<P> query[maxn];
void add(int a, int b, int c) {
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
void dfs(int u, int fa) {
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (v == fa) continue;
		d[v] = d[u] + w[i];
		dfs(v, u);
	}
}
void init(int N){
    for(int i = 1; i <= N; i++) p[i] = i;
}
int find(int x) {
	if (p[x] == x) return x;
	return p[x] = find(p[x]);
}
void tarjan(int u) {
	st[u] = 1;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (!st[v]) {
			tarjan(v);
			//只有状态为 2 的点往上走,走到第一个状态为 1 的点才可以算作并查集中的 parent。因此只有这个点完全变成状态为2的点的时候,才可以让 p[v] 变为 u.
			p[v] = u;
		}
		
	}
	for (auto p : query[u]) {
		int v = p.first, id = p.second;
		if (st[v] == 2) {
			int anc = find(v);
			ans[id] = d[u] + d[v] - 2 * d[anc];
		}
	}
	st[u] = 2;
}
int main() {
	scanf("%d%d", &N, &M);
	memset(h, -1, sizeof h);
	for (int i = 1; i < N; i++) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c), add(b, a, c);
	}
	for (int i = 0; i < M; i++) {
		int a, b;
		scanf("%d%d", &a, &b);
		if (a != b) {
			query[a].push_back({ b, i });
			query[b].push_back({ a, i });
		}
	}
	init(N);
	dfs(1, -1);
	tarjan(1);
	for (int i = 0; i < M; i++) printf("%d\n", ans[i]);
	return 0;
}

356. 次小生成树

  • 给定一张 N N N 个点 M M M 条边的无向图,求无向图的严格次小生成树。设最小生成树的边权之和为 s u m sum sum,严格次小生成树就是指边权之和大于 s u m sum sum 的生成树中最小的一个。 N ≤ 1 0 5 , M ≤ 3 × 1 0 5 N≤10^5,M≤3×10^5 N105,M3×105

  • f a ( i , j ) fa(i, j) fa(i,j):从 i i i 开始,往上跳 2 j 2^j 2j 步是哪个节点。

  • d 1 ( i , j ) d_1(i, j) d1(i,j):从 i i i 开始,往上跳 2 j 2^j 2j 步,这些 2 j 2^j 2j 边中边权最大值。

  • d 2 ( i , j ) d_2(i, j) d2(i,j):从 i i i 开始,往上跳 2 j 2^j 2j 步,这些 2 j 2^j 2j 边中边权次大值。

  • 思路:

    • 先构建出最小生成树,标注每条边是否是当前最小生成树的边.
    • 然后剩余的每一条边尝试往上加,然后在形成的环中对应最小生成树中的边中删掉最大边;若最大边和新加进来的边相同,就加上次大边.
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 100010, maxm = 300010, INF = 0x3f3f3f3f;
int h[maxn], e[maxm], ne[maxm], w[maxm], idx;
int p[maxn], N, M;
int d1[maxn][17], d2[maxn][17], fa[maxn][17], depth[maxn];
ll sum;
struct edge {
	int u, v, w;
	bool used;
	bool operator<(const edge& rhe)const 
    {
		return w < rhe.w;
	}
}G[maxm];
void init(int N) 
{
	for (int i = 1; i <= N; i++) p[i] = i;
}
int find(int x) {
	if (p[x] == x) return x;
	return p[x] = find(p[x]);
}
void unite(int a, int b) {
	if (find(a) == find(b)) return;
	p[find(a)] = find(b);
}
void add(int a, int b, int c) {
	e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
void kruskal() {
	init(N);
	sort(G, G + M);
	for (int i = 0; i < M; i++) {
		int u = G[i].u, v = G[i].v, w = G[i].w;
		if (find(u) != find(v)) {
			unite(u, v);
			G[i].used = true;
			sum += w;
		}
	}
}

//建出最小生成树构成的数。
void build() {
	memset(h, -1, sizeof h);
	for (int i = 0; i < M; i++) {
		if (G[i].used) {
			add(G[i].u, G[i].v, G[i].w);
			add(G[i].v, G[i].u, G[i].w);
		}
	}
}
queue<int> que;
//计算出depth, d1, d2, fa.
void bfs() {
	memset(depth, 0x3f3f3f3f, sizeof depth);
	depth[0] = 0, depth[1] = 1;
	que.push(1);
	while (que.size()) {
		int u = que.front();
		que.pop();
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];

			if (depth[v] > depth[u] + 1) {
				depth[v] = depth[u] + 1;
				que.push(v);
				fa[v][0] = u;
				d1[v][0] = w[i], d2[v][0] = -INF;
				for (int k = 1; k <= 16; k++) {
					int anc = fa[v][k - 1];
					fa[v][k] = fa[anc][k - 1];
					d1[v][k] = d2[v][k] = -INF;
					
					int dis[4] = { d1[v][k - 1], d2[v][k - 1], d1[anc][k - 1], d2[anc][k - 1] };
					
					for (int j = 0; j < 4; j++) {
						if (dis[j] > d1[v][k]) 
							d2[v][k] = d1[v][k], d1[v][k] = dis[j];
						else if (dis[j] != d1[v][k] && dis[j] > d2[v][k]) 
							d2[v][k] = dis[j];
					}
				}
				
			}
		}
	}
}
int lca(int a, int b, int w) {
	static int dis[maxn * 2];
	int cnt = 0;
	if (depth[a] < depth[b]) swap(a, b);
	for (int k = 16; k >= 0; k--) {
		if (depth[fa[a][k]] >= depth[b]) {
			dis[cnt++] = d1[a][k];
			dis[cnt++] = d2[a][k];
			a = fa[a][k];
		}
	}
	if (a != b) {
		for (int k = 16; k >= 0; k--) {
			if (fa[a][k] != fa[b][k]) {
				dis[cnt++] = d1[a][k];
				dis[cnt++] = d2[a][k];
				dis[cnt++] = d1[b][k];
				dis[cnt++] = d2[b][k];
				a = fa[a][k], b = fa[b][k];
			}
			dis[cnt++] = d1[a][0];
			dis[cnt++] = d1[b][0];
		}
	}
	int dist1 = -INF, dist2 = -INF;
	for (int i = 0; i < cnt; i++) {
		if (dis[i] > dist1) dist2 = dist1, dist1 = dis[i];
		else if (dis[i] != dist1 && dis[i] > dist2) dist2 = dis[i];
	}
	if (w > dist1) return w - dist1;
	if (w > dist2) return w - dist2;
	return INF;
}

int main() {
	scanf("%d%d", &N, &M);
	for (int i = 0; i < M; i++) {
		scanf("%d%d%d", &G[i].u, &G[i].v, &G[i].w);
	}
	kruskal();
	build();
	bfs();

	ll ans = 1e18;
	for (int i = 0; i < M; i++) {
		if (!G[i].used) {
			int u = G[i].u, v = G[i].v, w = G[i].w;
			ans = min(ans, sum + lca(u, v, w));
		}
	}
	printf("%lld\n", ans);
	return 0;
}

树上差分:352. 闇の連鎖

  • 题意:Dark 有 N–1 条主要边,并且 Dark 的任意两个节点之间都存在一条只由主要边构成的路径。另外,Dark 还有 M 条附加边。你的任务是把 Dark 斩为不连通的两部分。第一步需要切断一条主要边,第二步需要切断一条附加边.

  • 我们定义题目所说的主要边为树边,附加边为非树边。任意一条非树边都可以和树边构成一个环。

  • 假如一条树边不在图中的任何环上,那么把这条树边切断图就会不连通;若一条树边在一个环上,在切掉形成那个环的非树边就能变成不连通的图。但是在两个环上的话,就不可能切成不连通的图。

  • 因此,我们枚举每条非树边,把构成的环上的每条树边的值 + 1 +1 +1。最后枚举每条树边,若 c = 0 c = 0 c=0,则 a n s + = M ans += M ans+=M;若 c = 1 c = 1 c=1,则 a n s + = 1 ans += 1 ans+=1;若 c > 1 c > 1 c>1,则 a n s + = 0 ans += 0 ans+=0.

  • 问题就变成了:怎样快速给树上的一些边都增加一个值,怎样快速求出树上的边的值。

  • 树上差分:设 x , y x, y x,y 是非树边的两个端点, x , y x, y x,y 的最近公共祖先是 p p p。则 d [ x ] + = c , d [ y ] + = c , d [ p ] − = 2 c d[x] += c, d[y] += c, d[p] -= 2c d[x]+=c,d[y]+=c,d[p]=2c。树上差分的原理和数组差分差不多,都是为了方便修改某一个区间的值。其实沿着路径将对应的d数组的值加起来,就是这个地方对应的原本数组的值。

#include<bits/stdc++.h>
using namespace std;

const int maxn = 100010, maxm = 2 * maxn;
int h[maxn], ne[maxm], e[maxm], idx;
int N, M, ans;
int d[maxn], depth[maxn], fa[maxn][17];

queue<int> que;
void bfs() 
{
	memset(depth, 0x3f, sizeof depth);
	depth[0] = 0, depth[1] = 1;
	que.push(1);
	while (que.size()) {
		int u = que.front(); que.pop();
		for (int i = h[u]; i != -1; i = ne[i]) {
			int v = e[i];
			if (depth[v] > depth[u] + 1) {
				depth[v] = depth[u] + 1;
				que.push(v);
				fa[v][0] = u;
				for (int k = 1; k <= 16; k++) fa[v][k] = fa[fa[v][k - 1]][k - 1];
			}
		}
		
	}
}
void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int lca(int a, int b) {
	if (depth[a] < depth[b]) swap(a, b);
	for (int k = 16; k >= 0; k--) {
		if (depth[fa[a][k]] >= depth[b]) {
			a = fa[a][k];
		}
	}
	if (a == b) return a;
	for (int k = 16; k >= 0; k--) {
		if (fa[a][k] != fa[b][k]) {
			a = fa[a][k];
			b = fa[b][k];
		}
	}
	return fa[a][0];
}
int dfs(int u, int father) {
	int res = d[u];
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (v == father) continue;
		int s = dfs(v, u);
		if (s == 0) ans += M;
		else if (s == 1) ans++;
		res += s;
	}
	return res;
}
int main() {
	scanf("%d%d", &N, &M);
	memset(h, -1, sizeof h);
	for (int i = 1; i < N; i++) {
		int a, b;
		scanf("%d%d", &a, &b);
		add(a, b), add(b, a);
	}
	bfs();
	for (int i = 0; i < M; i++) {
		int a, b;
		scanf("%d%d", &a, &b);
		int p = lca(a, b);
		d[a]++, d[b]++, d[p] -= 2;
	}
	dfs(1, -1);
	printf("%d\n", ans);
	return 0;
}

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

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

相关文章

(文章复现)面向配电网韧性提升的移动储能预布局与动态调度策略(1)-灾前布局matlab代码

参考文献&#xff1a; [1]王月汉,刘文霞,姚齐,万海洋,何剑,熊雪君.面向配电网韧性提升的移动储能预布局与动态调度策略[J].电力系统自动化,2022,46(15):37-45. 1.基本原理 1. 1 目标函数 本文以最恶劣光伏出力场景下的移动储能配置成本与负荷削减成本最小为目标&#xff0c;建…

(数组) 724. 寻找数组的中心下标 ——【Leetcode每日一题】

❓724. 寻找数组的中心下标 难度&#xff1a;简单 给你一个整数数组 nums &#xff0c;请计算数组的 中心下标 。 数组 中心下标 是数组的一个下标&#xff0c;其左侧所有元素相加的和等于右侧所有元素相加的和。 如果中心下标位于数组最左端&#xff0c;那么左侧数之和视为…

2023-6-9

1.网络训练&#xff1a; 在训练前先要看看读取数据的时间&#xff08;常见的性能瓶颈&#xff09;2.import dis dis 是 Python 内置的一个模块&#xff0c;其全称为 “Disassembler for Python bytecode”&#xff0c;用于反汇编 Python 字节码。它可以将 Python 代码编译成字…

视频换天造物实践秒变科幻大片实践记录

视频换天造物实践秒变科幻大片实践记录&#xff0c;过程中遇到些坑&#xff0c;结果还是相当震撼 预装软件&#xff1a; matplotlib scikit-image scikit-learn scipy numpy torch torchvision opencv-python opencv-contrib-python 安装使用的时候可能碰上scikit-image 新版…

傅里叶级数简介

先看动图 将函数f(x) 用 sin(nx) cos(nx) 的形式表示出来的方式就是傅里叶级数 这里有几个使用条件 收敛性&#xff1a;符合迪力克雷收敛条件。简单理解为 f(x) 必须是一个丝滑的曲线。周期性&#xff1a; f(x) 必须是一个周期函数 还有一个基础条件&#xff0c;三角函数具…

element-plus布局排版问题总结(更新ing)

文章目录 el-container空隙修改app组件 el-container空隙 源码-更改了容器的显示&#xff0c;占满屏幕 <template><div class"common-layout"><el-container><el-header><el-row class"el-row1"><el-col :span"12&…

oppo r11 升级8.1系统 图文教程

Time: 2023年6月11日13:39:25 By:MemroyErHero 1 预留一定的空间,存放刷机包. 2 导入刷机包 r11.ozip 到手机上 3 手机文件管理器 打开 r11.ozip 文件 4 点击立即更新即可 5 重要的事情说三遍,刷机过程中 不能关机 不能断电 否则会变成砖头 重要的事情说三遍,刷机过程中 …

cmake 基本使用

目录 CMake都有什么? 使用cmake一般流程为&#xff1a; 1 生成构建系统 使用命令在build外编译代码: cmake基本语法 指定使用最低版本的cmake 指定项目名称 指定生成目标文件的名称 指定C版本 cmake配置文件使用 cmake配置文件生成头文件 版本号定义方法一: 版本号定…

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

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

C 语言实现简单工厂模式

文章目录 1. 背景介绍2. 设计实现3. 运行测试4. 总结 1. 背景介绍 印象中&#xff0c;设计模式是由面向对象的语言(C、JAVA)才能完成的&#xff0c;而 C 语言是面向过程的语言&#xff0c;不能实现设计模式。但C 语言中有 函数指针、回调函数 等机制&#xff0c;使用这些机制便…

Java中线程的生命周期

Java中线程的生命周期 Java中线程的声明周期与os中线程的生命周期不太一样&#xff0c;java中线程有6个状态&#xff0c;见下&#xff1a; NEW: 初始状态&#xff0c;线程被创建出来但没有被调用 start() 。RUNNABLE: 运行状态&#xff0c;线程被调用了 start()等待运行的状态…

Elasticsearch:使用 Redis 让 Elasticsearch 更快

Elasticsearch 是一个强大的搜索引擎&#xff0c;可让你快速轻松地搜索大量数据。但是&#xff0c;随着数据量的增长&#xff0c;响应时间可能会变慢&#xff0c;尤其是对于复杂的查询。在本文中&#xff0c;我们将探讨如何使用 Redis 来加快 Elasticsearch 搜索响应时间。 Re…

【数据结构】常见排序算法——常见排序介绍、归并排序、各大排序复杂度和稳定性

文章目录 1.常见排序2.归并排序2.1归并排序基本思想2.2归并排序的实现2.3归并排序特性总结 3.各大排序复杂度和稳定性 1.常见排序 2.归并排序 归并排序&#xff08;MERGE-SORT&#xff09;是建立在归并操作上的一种有效的排序算法,该算法是采用分治法&#xff08;Divide andCon…

商业图表工具推荐,热门商业图表工具有哪些?

在如今的商业环境下&#xff0c;数据分析和可视化是非常重要的一环。不仅可以帮助企业更好地了解自身情况&#xff0c;还能为决策提供有力支持。因此&#xff0c;选择一个好用的商业图表工具对于报表开发人员来说是非常重要的。下面将为大家介绍7款热门商业图表工具&#xff0c…

Mac电脑删除第三方软件工具CleanMyMac X

经常使用Mac的人都知道&#xff0c;Mac除了可以在AppStore下载应用程序&#xff0c;还有许多软件是需要在网页上搜索下载的第三方软件。那么这类第三方软件软件除了下载方式不同之外还有什么是和从App store下载的软件有区别的吗&#xff1f;答案是肯定的&#xff0c;那就是这些…

Docker容器进入的4种方式

Docker容器进入的4种方式 Docker容器进入的4种方式 在使用Docker创建了容器之后&#xff0c;大家比较关心的就是如何进入该容器了&#xff0c;其实进入Docker容器有好几多种方式&#xff0c;这里我们就讲一下常用的几种进入Docker容器的方法。 进入Docker容器比较常见的几种…

带你了解自动化测试只需要一分钟

目前自动化测试并不属于新鲜的事物&#xff0c;或者说自动化测试的各种方法论已经层出不穷&#xff0c;但是&#xff0c;能够明白自动化测试并很好落地实施的团队还不是非常多&#xff0c;我们接来下用通俗的方式来介绍自动化测试…… 首先我们从招聘岗位需求说起。看近期的职业…

软件测试什么样的技术才能拿20K薪资?

年少不懂面试经&#xff0c;读懂已是测试人。 大家好&#xff0c;我叫木江&#xff0c;一名历经沧桑&#xff0c;看透互联网行业百态的测试从业者&#xff0c;经过数年的勤学苦练&#xff0c;精钻深研究&#xff0c;终于从初出茅庐的职场新手成长为现在的测试老鸟&#xff0c;早…

MySQL数据库基础 09

第九章 子查询 1. 需求分析与问题解决1.1 实际问题1.2 子查询的基本使用1.3 子查询的分类 2. 单行子查询2.1 单行比较操作符2.2 代码示例2.3 HAVING 中的子查询2.4 CASE中的子查询2.5 子查询中的空值问题2.5 非法使用子查询 3. 多行子查询3.1 多行比较操作符3.2 代码示例3.3 空…

深入浅出对话系统——自然语言理解模块

自然语言理解 首先回顾一下自然语言理解的概念。 自然语言理解(Natural Language Understanding)包含三个子模块&#xff1a; 其中领域识别和意图识别都是分类问题&#xff0c;而语义槽填充属于序列标注问题。所以&#xff0c;在自然语言理解中&#xff0c;我们要解决两个分类…