图论基础部分

n遍Floyd

普通的Floyd的枚举顺序:

for (k) for (i) for (j)
	dis[i][j] = min(dis[i][k] + dis[k][j])

这样的Floyd求出的最短路是不限边数的。

而如果写成这样:

for (i) for (j) for (k)
	diss[i][j] = min(dis[i][k] + a[k][j])

(其中 \(diss\) 为初始化全为inf的矩阵,\(dis,a\) 初始为邻接矩阵,无边则为inf)并且最短路就为借助一个点的最短路

如果做 \(n\) 遍 Floyd(Floyd后注意更新dis矩阵,这时\(dis\) 矩阵表示借助 \(n-1\) 个点的最短路),则为恰好借助 \(n\) 个点的最短路(经过 \(n+1\) 条边)

遍数可以用矩阵加速(矩阵乘法的res[i][j] += a[i][k] * b[k][j]改成res[i][j] = min(res[i][j], a[i][k] + b[k][j])即可。

当然,如果真的完全按照矩阵乘法的规则进行计算的话,矩阵的 \(k\) 次方的第 \(u\) 行第 \(v\) 列就是从 \(u\)\(v\) 经过恰好 \(k\) 条边的方案数。例题:CF402E Strictly Positive Matrix

树的直径

性质:

  • 树中最长路径。

  • 不一定唯一。

  • 树中距离任意点 \(u\) 的最远点 \(v\) 一定是直径的端点之一。

  • 以上性质仅限于边权非负的情况。

求法:

求法一:两遍BFS/DFS

先随意指定一个点,BFS找出树上距离该点最远点 \(u\);再从 \(u\) 开始,BFS找出树上距离 \(u\) 的最远点 \(v\)\(u -> v\) 即为树点直径之一。

本方法适用于边权非负的情况。

求法二:树形DP

考虑在直径的 LCA 处找出该直径。对每个点维护 \(f(x)\) 表示从 \(x\) 向下走,最长路。对每个点找最大和次大加一块即可。

拓展:“三叉直径”

在一棵边权为 \(1\) (其余情况尚未研究过)的树中,\((x,y,z)\) 三个点所形成的虚树的大小的最大值称为这棵树的三叉直径。

有一些性质:

  • 三叉直径的三个点中两个为直径端点。

  • 加入一个点 \(p\) 后,三叉直径的三个点 \(x,y,z\) 最多只会变化一个。

树的重心

性质:

  • 树的重心是树中以某点为根,最大子树最小的那个节点。

  • 树的重心是树中所有点到某点的距离之和最小的那个节点。

  • 树的重心的所有子树的大小不超过整棵树大小的一半。其逆命题成立。

  • 树的重心一定不超过两个。如果树的节点数为奇数,那么重心只有一个;否则可能有两个。如果有两个,一定存在一条边,其两边的树的大小均为 \(\frac{n}{2}\)

  • 添加一个节点,树的重心最多只会移动一个位置。

  • 将两棵树连接,新树的重心一定在两棵树的原重心的连线上。

  • \(rt\) 为根的树的重心一定是 \(rt\)\(rt\) 的重儿子 或 \(rt\) 的重儿子的重儿子...

求法:

树形dp,维护最大子树的大小。选取最大子树最小的那个点作为根。

详见点分治

重心的动态维护1

支持加边,查重心。

详见:P4299 首都

LCT 动态加边;并查集维护森林;

运用后两条性质,加边后提取出两个重心的链来,在链的Splay上做树上二分即可。

重心的快速维护2

求以 \(rt\) 为根的所有子树的重心(\(n <= 2e5\))

方法一:将两棵树连接,新树的重心一定在两棵树的原重心的连线上。

我们自然可以沿用“重心的动态维护1”的方法,\(DFS\) 动态加边;但是更暴力的是,我们可以直接跳链查询。可以感性地知道这样做的复杂度不会很高。(因为重心的优美性质)(不到 \(O(n^2)\))。上 LCT 能到 \(O(nlogn)\)

方法二:以 \(rt\) 为根的树的重心一定是 \(rt\)\(rt\) 的重儿子 或 \(rt\) 的重儿子的重儿子...

我们可以利用 以 \(rt\) 为根的树的重心一定是 \(rt\)\(rt\) 的重儿子 或 \(rt\) 的重儿子的重儿子... 的性质,这样我们就知道了重心的位置在一条链上。并且,我们发现,重心的 \(n - siz <= n / 2\)(显然),并且链上这个条件是单调的最深的符合该条件的点一定是重心(可以贪心证明),其父亲可能是重心。因此倍增查找具体位置即可。

复杂度: \(O(nlogn)\)

重心的快速维护3(2的加强版)

查询:删每一条边后分成的两棵树的重心。

详见:P5666 树的重心

实际上这道题放在 \(D2T2\)\(D1T3\) 或许会有更多人写出来。

由于操作与操作之间不相关,因此我们需要动态加边,删边,查重心,这就不能用上面的 \(LCT\) 算法了(况且我当时连 \(Splay\) 都不会)

部分分55pts很好拿,考虑怎样拿到剩下的45pts.(\(n<=3e5\)) 由于需要维护所有边删掉后的信息,整个数据只有一棵树,我们应该能想到换根DP。(然而当时我并不会换根DP)

如果只删一次边,那么我们 \(O(n)\) 怎么搞都行,但是如果删很多次边,由于传统的算法里面涉及到的 \(Siz\)\(siz\) 会变,我们不能够利用传统算法里面的 \(f[]\) 之类的数组。那还有什么东西可用呢?

很显然的一条性质:"以 \(rt\) 为根的树的重心一定是 \(rt\)\(rt\) 的重儿子 或 \(rt\) 的重儿子的重儿子..." 根据这一条性质我们能够 \(O(nlogn)\) 地维护以 \(1\) 为根的所有子树的重心。(见“重心的快速维护2”)

我们可以把问题拆成两部分:“分离”出的子树重心,和整个树的残缺部分的重心。前者可以用前面的方法快速地求出;这里只讨论后者。

如果我们考虑随意删一条边的话, \(son\) 的变化比较大,不太好维护。根据换根DP的思想,我们只考虑删掉与根节点相连的一条边。这样的话, \(son\) 最多只改变一个,维护一个 \(sson\)(次重儿子)即可。

这样,我们就用 \(O(nlogn)\) 的方法解决掉了 \(CSP ~ 2019 ~ D2T3\) 了。

需要卡常。

重心的动态维护4

题目:幻想乡战略游戏

给一棵无根树,度数均不超过60,支持修改点权,以及查询:

\[min(\sum_{u}val[u] * dis(u,p)) \]

题解:

根据重心的性质:

树的重心是树中所有点到某点的距离之和最小的那个节点。

我们知道那个和式取最小值当且仅当 \(p\) 为树的重心。加权了也没关系,看作一条内部边权均为0的链即可。

那么我们需要快速地维护树的重心。设 \(F(x) = \sum_{u}val[u] * dis(u,x)\),上面 CSP2019D2T3 告诉我们可以用倍增预处理“重儿子链”的方式来快速求重心,并且我们可以换根 DP \(O(n)\) 求所有的 \(F(x)\),但是如果要支持修改点权的话,轻重边可能会改变,暴力修改“重儿子链”并修改倍增数组很可能会被卡成 \(O(qnlogn)\),且所有的 \(F(x)\) 都会改变,这个方法将沦为比暴力还暴力的暴力。我们还要想其它的法子。

不过,幸运的是,“根据重儿子判断重心的大致位置”的思想在这道题里面仍然适用。我们想,如果我们瞎选一个点,并且知道它的重儿子是谁,就可以去重儿子的子树里面寻找答案。这看起来很像是一个子问题。如果我们能够保证“去子树”的次数不是很多,就能解决这个问题了。因此我们使用点分治来维护重心。

现在还有两个问题:如何快速求 \(F(x)\),以及如何快速找重儿子。

感性地理解一下(其实理性证明也不难),\(F(heavy~son) <= F(cur)\),又因为度数 <= 60,所以两个问题转化为一个问题。这个问题交给点分治解决好了

根据套路,维护一个 \(g(x)\),表示 \(x\) 管辖区域的点权和。维护一个 \(f1(x)\),表示 \(x\) 管辖区域内所有“点”(权值为 \(v\) 的点视为 \(v\) 个点)到 \(x\) 的距离之和。维护一个 \(f2(x)\),表示 \(x\) 管辖区域内所有“点”到 \(fa(x)\) 的距离之和。其中 \(fa(x)\) 表示 \(x\)点分树上的父亲。

然后套用 震波 的方法,一层一层地算贡献。对每一层算一下当前层的贡献,减去多算的上一层的贡献。复杂度:单次 \(O(logn)\)

这样,我们不直接维护重儿子,修改就没必要 \(O(n)\) 的改所有点的重儿子了。

总复杂度:\(O(nlogn + degree * qlog^2n)\)由于度数 <= 60,因此可以通过此题。

欧拉序维护LCA

这是一种 \(O(nlogn)\) 静态维护LCA \(O(1)\) 查询的方法:

对一棵有根树进行DFS,每次进入或返回到某个点的时候都给那个点一个新编号,并且将这个点放到一个序列(欧拉序)里面。查询的时候就直接查 \(u\)\(v\) 的编号之间的最小编号即可。借助ST表,我们可以做到 \(O(1)\) 查询。

LCA的巧用

三点间路径交叉点

这是一个经典的问题:给三点 \(x, y, z\),求路径的交点(即 \(x\)\(y,z\) 路径的最近点)。

我们可以找出三个点的两两之间的LCA,记作 \(lca1, lca2, lca3\),最终答案为 \(lca1 ~ xor ~ lca2 ~ xor ~ lca3\)

这是因为,这仨LCA必定有两个是一样的,并且还是较高的那一个。然后剩下那个较低的LCA即为交点。

两路径求交

其次是一个略复杂的问题:给两条路径 \((u,v)\)\((p, q)\),询问这两条路径是否相交。

我们可以搞出 \(lca(u,p),lca(u,q),lca(v,p),lca(v,q)\)。其中一定有俩是 \(lca(u,v,p,q)\)。如果两路径相交,那么剩下俩之间的路径就是交的那一部分路径;否则,剩下俩一定相等,并且位于两路径中LCA较高的那一条,而不位于LCA较低的那一条。由此我们可以判断是否相交以及找出相交部分的路径。

一个比较简单的结论:两条路径有交当且仅当一条路径的 LCA 被另一条路径经过

一些情况:

路径交

DFS 树

DFS 树是 tarjan 算法中的强有力工具,也是解决某些(CF)图论题目的好方法。

无向图 DFS 树

定义 无向图的 DFS 树上的边 为树边,那么无向图的边只有树边和祖先-子孙边,不会出现横叉边(显然)

应用:tarjan

有向图 DFS 树

定义 有向图的 DFS 树上的边 为树边,那么有向图的边只有树边和祖先->子孙边,子孙->祖先边,横叉边(后遍历的点 -> 先遍历的点)

应用:tarjan

欧拉路 & 欧拉回路

将所有边全部经过的路径称为欧拉路,起点终点还一样,就可以称为欧拉回路。

判定:

  • 有向图欧拉路:弱连通图中每个点入度 = 出度 或者有一个点入度比出度小一,有一个点入度比出度大一。

  • 有向图欧拉回路:弱连通图中每个点入度 = 出度

  • 无向图欧拉路:联通,且最多有两个点度数为奇数。

  • 无向图欧拉回路:联通,且所有点度数均为偶数。

找方案

找到起点开始 DFS,每次找一条未遍历过的出边走过去。DFS 结束后将当前点放入栈中。最后栈内序列(的逆序列)即为一组解。

复杂度:\(O(nm)\),用弧优化能优化到 \(O(n+m)\),但是需要 &i 而不是 hcur[cur],否则仍为 \(O(nm)\)

代码:(以无向图欧拉回路为例)

int stk[N], stop;
bool evis[NN];
void dfs(int cur) {
	for (int &i = head[cur]; i; i = e[i].nxt) {
		if (evis[i])	continue;
		evis[i] = evis[i ^ 1] = true;
		dfs(e[i].to);
	}
	stk[++stop] = cur;
}

应用

对无向图跑欧拉回路能给无向边定向,并且定向后每个点的入度出度相等。借助这个特点可以做一些类似匹配的构造题(如Mike and FishTwo Trees

竞赛图与哈密顿路径

  • 竞赛图一定有哈密顿通路
  • 强连通竞赛图一定有哈密顿回路(强竞有哈回)
  • 竞赛图 tarjan 缩点后的 DAG 可以被抻成一条链,其中后面的点向前面的点都有连边

竞赛图和哈密顿回路

题:Turysta

构造哈密顿通路方法:增量法

构造哈密顿回路方法:先构造哈密顿通路,再增量法构造

//P3561
//对一个强连通分量进行哈密顿回路的构造
inline void make_chain(int c) {
	for (int i = 1; i <= n; ++i) if (col[i] == c) {
		l = r = i; break;
	}
	for (int i = l + 1; i <= n; ++i) if (col[i] == c) {
		if (e[i][l]) nxt[i] = l, l = i;
		else if (e[r][i])	nxt[r] = i, r = i;
		else {
			int p = l;
			while (1) {
				if (e[p][i] && e[i][nxt[p]]) {
					nxt[i] = nxt[p];
					nxt[p] = i;
					break;
				}
				p = nxt[p];
			}
		}
	}
}

inline void make_circle(int c) {
	r = 0;
	for (int i = nxt[l]; i; i = nxt[i]) {
		if (r) {
			for (int k = r, j = l; ; k = j, j = nxt[j]) {//k -> j
				if (e[i][j]) {
					nxt[k] = nxt[r];
					if (k != r) nxt[r] = l;
					r = i; l = j;
					break;
				}
				if (j == r)	break;
			}
		} else if (e[i][l]) r = i;
	}
	if (r) nxt[r] = l;
	else	nxt[l] = l;
}

三元环计数 四元环计数

给一张 \(n\) 个点 \(m\) 条边的无向图,问里面三元环(仅包含三个点的简单环),四元环有多少个。 \(n,m \le 100000\)

不常用的黑科技——「三元环」

我们将有向图根据端点度数定向,让小度点向大度点连边,度数相等时按照点编号连边。这样处理以后,我们能够保证每个点的出边不超过 \(\sqrt m\) 个。

然后发现三元环 \((A,B,C)\) 一定不会成环,只可能是 \(A \to B,A \to C,B \to C\) 这样的。那么我们就在被两个点同时指向的那个点处统计贡献。

复杂度:\(O(m \sqrt m)\)

题:旧试题

for (A) {
	for (B)
		mark B;
	for (B)
		for (C)
			if (C is marked)
				get a circle;
}

四元环 \((A,B,C,D)\) 一共有三种定向形式:

A -> B -> C -> D | A -> D

A -> B -> C | A -> D -> C

A -> C | B -> C | A -> D | B -> D

我们第一层遍历无向边,第二层遍历有向边,则三种情况都会统计到。

其中前两种会被算一次,第三种会被算两次。于是我们需要在计数节点处记录起始节点向中间节点的边的方向,如果发现都是反向的话需要特判。

复杂度:\(O(m \sqrt m)\)

for (A)
	for (B)//recorde direction
		for (C) {
			get ans(dir, C);
			mark(dir, C);
		}

找环

如果是基环树,那么最方便的方法是拓扑排序。没扔进过队列里的点为环上的的点。

还有一种方法是 tarjan 求 e-DCC,求完后不走割边再 DFS 一遍。比较强大,可以解决多个环以及环套环的问题,但是相对来说比较难写。

还有一种常用方法是 DFS+栈。维护当前 DFS 的栈,遇到环就弹栈。缺点是不太能解决多个环和环套环问题,并且非常容易防记 DFS 后栈的撤销

还有一种方法是 DFS 树。求出 DFS 树后,环的形态只可能是一条返祖边加上一条树链。这种方法可以简化图的模型,方便做题,同时也不难写。也可以解决环套环问题(结果是拆成了多个简单环)

posted @ 2020-07-23 09:31  JiaZP  阅读(309)  评论(0编辑  收藏  举报