初级图论

拓扑排序

  • 即在一个 DAG(有向无环图) 中,我们将图中的顶点按照某种方式进行排序,使得对于任何的顶点 u  到 v 的有向边 , 都可以有 u 在 v 的前面。
  • 推论:所有能到达点 \(u\) 的点,在拓扑序中都在 \(u\) 的前面

P7077 [CSP-S2020] 函数调用

树上问题

  • 森林(forest):每个连通分量(连通块)都是树的图。按照定义,一棵树也是森林。

  • 生成树(spanning tree):一个连通无向图的生成子图,同时要求是树。也即在图的边集中选择  n-1 条,将所有顶点连通。

  • 无根树的叶结点(leaf node):度数不超过 1  的结点。

  • 父亲(parent node):对于除根以外的每个结点,定义为从该结点到根路径上的第二个结点。 根结点没有父结点。

  • 祖先(ancestor):一个结点到根结点的路径上,除了它本身外的结点。 根结点的祖先集合为空。

  • 子结点(child node):如果  是  的父亲,那么  是  的子结点。子结点的顺序一般不加以区分,二叉树是一个例外。

  • 结点的深度(depth):到根结点的路径上的边数。

  • 树的高度(height):所有结点的深度的最大值。

  • 兄弟(sibling):同一个父亲的多个子结点互为兄弟。

  • 后代(descendant):子结点和子结点的后代。或者理解成:如果  是  的祖先,那么  是  的后代。

  • 子树(subtree):删掉与父亲相连的边后,该结点所在的子图。

  • 链(chain/path graph):满足与任一结点相连的边不超过 条的树称为链。

  • 菊花/星星(star):满足存在 使得所有除 以外结点均与 相连的树称为菊花。

  • 有根二叉树(rooted binary tree):每个结点最多只有两个儿子(子结点)的有根树称为二叉树。常常对两个子结点的顺序加以区分,分别称之为左子结点和右子结点。
    大多数情况下,二叉树 一词均指有根二叉树。

  • 完整二叉树(full/proper binary tree):每个结点的子结点数量均为 0 或者 2 的二叉树。换言之,每个结点或者是树叶,或者左右子树均非空。

  • 完全二叉树(complete binary tree):只有最下面两层结点的度数可以小于 2,且最下面一层的结点都集中在该层最左边的连续位置上。

  • 完美二叉树(满二叉树)(perfect binary tree):所有叶结点的深度均相同的二叉树称为完美二叉树。

  • 若一棵完美二叉树的树高为 \(h\),则结点个数为 \(2^h-1\)。叶子个数为 \(2^{h-1}\)。编号时,我们可以将左儿子的编号定义为 \(2i\), 右儿子为 \(2i+1\)(完全二叉树也可以这么编号)。叶子的编号不小于 \(2^{h-1}\),我们可以总结出一个小结论:完美二叉树最左边叶子的编号等于叶子个数。

  • 若二叉树中叶子数为 \(n_0\) ,度为 2 的结点数为 \(n_2\) ,则 \(n_0=n_2+1\)

    证:显然 \(n=n_0+n_1+n_2,n=e+1\) ,又因为 \(e=n_1+2n_2\) 所以 \(n_1+2n_2+1=n_0+n_1+n_2\) ,即 \(n_0=n_2+1\)

  • 一棵具有 \(n\) 个结点的完全二叉树深度为 \(log_2n+1\)

二叉树的儿子表示法

struct node{
	int lc,rc;
	node(){lc=0;rc=0;}
};

二叉树的数组表示法

int ch[N][2];

淘汰赛

\(n\) 个国家要参加比赛,每个国家的实力均不同,实力强的会击败实力弱的国家。比赛的规则为两两相比,胜者晋级。问进行了若干轮后最终的亚军哪个国家。

显然,赛程图是一个完美二叉树,我们递归求解即可。

树的遍历方式 :

前序遍历 : 根,左,右
中序遍历 : 左,根,右
后序遍历 : 左,右,根

知道中序和另一种顺序,可推出树的结构

P1364 医院设置

带权树的重心

P1229 遍历问题

容易发现,一个根只有一个儿子时才会出现不同的情况,我们考虑取统计只有一个儿子根的个数。

当点 \(A\) 的儿子为 \(B\) 时,考虑 \(A\) 的子树在前序和后序对应的区间,前序的开头一定是 AB,后序的结尾一定是 BA。反之,若前序和后序出现了一对 \((AB,BA)\) ,则考虑以 \(A\) 为根的子树,\(A\) 一定只有一个儿子 \(B\)

于是我们只需统计 \((AB,BA)\) 的对数就行了。

树、二叉树、森林的转化

树转化成二叉树:

在处理多叉树时往往不好处理,我们可以采用 “左儿子右兄弟” 的方法,把其在原树上第一个儿子结点作为新树上的左儿子,把在原树上的下一个兄弟作为新树上的右儿子

img

只需要牢记:二叉树上的左儿子是它的第一个儿子,右儿子是它的兄弟。

pro: 给定一棵有根树,对于每个非叶结点,给定其儿子的标号顺序。对于每个非根结点,若它是父亲的第 \(i\) 个儿子,输出其父亲 1 - i 儿子的子树大小之和。

森林转化成二叉树:

易发现,多叉树转化成二叉树的根结点只要左儿子。我们新建一个虚拟结点,所有树的根与之连边,再转化成一颗二叉树,最后将虚拟结点直接去掉(因为只有左儿子)就行了。

同样,二叉树转化成树/森林也并不困难。

二叉堆

具有堆性质的完全二叉树,对于每一个结点,它的权值是以该结点为根子树的最小值(即子树中的权值都大于等于该权值)。堆的最小值一定在根结点上。

插入一个数:

我们先把插入元素放在数组的第 \(n\) 个位置上,从下往上进行调整,使其满足堆性质。具体地,若父亲的权值大于该结点,交换位置,直到满足堆性质为止。

删除堆顶:

将堆顶直接删去,并将 n 号元素提到堆顶,从上往下进行调整。具体地,从根节点开始,若不满足,则将左右儿子的较小者与之交换,直到满足堆性质。

由于是完全二叉树,所以是 \(O(logn)-O(logn)\) 的。

二叉搜索树

  1. 若 x 的左子树不为空,则 x 的左子树中所有结点的权值都小于 x 的值
  2. 若 x 的右子树不为空,则 x 的右子树中所有结点的权值都大于 x 的值
  3. 任意结点的左子树和右子树依然保持二叉搜索树的性质
  4. 没有键值相等的结点

支持以下操作

  1. 插入一个数 x
  2. 查询 x 数的排名(排名定义为比当前数小的数+1,若有多个相同的数,应输出最小的排名)
  3. 查询排名为 x 的数
  4. 查询 x 的前驱
  5. 查询 x 的后继
  • 插入一个数

    每次将 x 与根节点的权值比较。如果 x 小于根节点的权值,那么把 x 插入 rt 的左儿子里;如果 x 大于根节点的权值,那么把 x 插入 rt 的右儿子里。这样操作之后这个二叉搜索树依然保持了其性质。如果此时 x 插入的那个位置的结点并不存在,则需要新建一个结点,权值为 x,来代替这个不存在的结点。

  • 查询 x 数的排名。

    每次将 x 与根结点做比较:如果 x 小于根结点的权值,那么根结点的右子树里面的所有权值都要比 x 大,所以递归下去查询左子树;如果 x 大于根结点的权值,那么根结点的左子树里面的所有权值都要比 x 小,所以递归下去查询右子树,并把左子树的大小加进答案里;如果 x 等于根结点的权值,则已经找到了答案,返回左子树大小+1即可

  • 查询排名为 x 的数。

    当前查询 rt 子树的第 x 小值时:如果 x 小于等于左子树大小的话,则排名为 x 的数一定在左子树中,递归下去查询 rt 左子树中排名为 x 的数;如果 x 大于左子树大小+rt大小,排名为 x 的数一定在右子树中,递归下去查询rt 右子树排名为 x-左子树大小-rt大小;否则排名为 x 的数一定是 rt

  • 求前驱后继

    排名为 rank(x) -1 的数是 x 的前驱,排名为 rank(x+1) 的数是 x 的后继

  • 删除一个数

    我们找到这个数所在位置

    1. 如果该结点是叶子的话,直接删除
    2. 如果是链结点的话,将唯一的儿子接上就行了
    3. 如果左右儿子都有,找到该结点的左子树最大值/右子树最小值,将这个结点提到最上面,由于该结点只可能是叶结点或链结点,所以可以直接删除
struct Node{
	int l,r,sz,cnt,val;
	void init(int _val){l=r=0;sz=cnt=1;val=_val;}
}t[N];
int num,rt;
void pu(int p){t[p].sz=t[t[p].l].sz+t[t[p].r].sz+t[p].cnt;}
int rk(int p,int x){
	if(!p)return 1;
	if(x<t[p].val)return rk(t[p].l,x);
	if(x>t[p].val)return t[t[p].l].sz+t[p].cnt+rk(t[p].r,x);
	return t[t[p].l].sz+1; 
}
int kth(int p,int x){
	if(x<=t[t[p].l].sz)return kth(t[p].l,x);
	if(x>t[t[p].l].sz+t[p].cnt)return kth(t[p].r,x-t[t[p].l].sz-t[p].cnt);
	return t[p].val;
}
void insert(int &p,int x){
	if(!p){
		p=++num;
		t[p].init(x);
		return ;
	}
	t[p].sz++;
	if(x==t[p].val){
		t[p].cnt++;return ;
	}
	if(x<t[p].val)insert(t[p].l,x);
	else insert(t[p].r,x);
}
int delmax(int &p){
	if(!t[p].r){
		int o=p;
		p=t[p].l;
		return o;
	}
	else {
		int o=delmax(t[p].r);
		t[p].sz-=t[o].cnt;
		return o;
	}
}
void del(int &p,int x){
	t[p].sz--;
	if(t[p].val==x){
		if(t[p].cnt>1){
			t[p].cnt--;return ;
		}
		if(!(t[p].l*t[p].r)){
			p=max(t[p].l,t[p].r);
		} 
		else {
			int l=t[p].l,r=t[p].r;
			p=delmax(t[p].l);
			t[p].l=l;t[p].r=r;
			pu(p);
		}
		return ;
	} 
	if(x<t[p].val)del(t[p].l,x);
	else del(t[p].r,x);
}
int pre(int x){
	return kth(rt,rk(rt,x)-1);
}
int suf(int x){
	return kth(rt,rk(rt,x+1));
}

LCA

端点移动法

  1. 判断 x 与 y 是否相等

  2. 否则,找出深度最大的一个点,向上跳一层

在随机数据下,树高期望为 \(logn\) ,复杂度比较优秀。且我们只需维护出每个点的父亲和深度即可,支持树的动态结构

倍增法

设 x 的深度为 \(d_x\),y 的深度为 \(d_y\) ,将\(d_x-d_y\)表示成一个二进制数,向上倍增跳,直到 x 和 y 在同一层。当然我们也可以直接从高位到低位枚举。

接着向上走 \(L-1\) 步使得 x 和 y 不同就行了。

欧拉序RMQ

对一棵树进行 dfs,无论是第一次访问还是回溯,每访问到一个结点都将编号记录下来,可以得到一个长度为 \(2n-1\) 的序列,这个序列被称为欧拉序列。

求出树上欧拉序,对于查询的结点 \((x,y)\) ,则对应到欧拉序上的区间 \([pos[x],pos[y]]\) 中深度最小的结点就是 lca。任选一个点即可。st表预处理。不支持在线修改。

预处理 \(O(nlogn)\),单次查询 \(O(1)\)

tarjan求lca

离线算法

dfs整棵树,每个结点 x 一开始由于属于该结点本身的集合 \(S_x\)

对于 \(x\) 的子节点 \(y\),遍历完 \(y\) 整棵子树后将 \(S_y\)\(S_x\) 合并

\(x\) 的所有子节点访问完时,将 \(x\) 标记为访问过的

遍历所有关于 \(x\) 的询问 \((x,y)\) ,若 \(y\) 已经被访问过,说明 \(y\rightarrow lca\) 的这条路已经被打通,答案为 \(fa_y\)

\(O((n+m)\alpha(n))\)

括号序

括号层层嵌套,如果把一对括号看成一个结点,其直接套住的括号对看作是其子结点,则这个结构就是一棵树。

括号序与树的结构一一对应,两棵树同构的充要条件是括号序一致

void dfs(int fa){//括号序转成一棵树
	while(p<n&&s[p]!=')'){
		++p;
		link(++num,fa);
		dfs(num);
	}
	++p;
}

树的直径 :
树上任意两点间最长的简单路径就是树的直径
1.两次dfs
2.dp

满足以下性质:

  1. 若直径有多条,则所有直径之间有至少有一个公共点,交于各直径的中点(不一定恰好是某个点,可以在边的内部)
  2. 直径的两个端点一定是叶子。
  3. (直径上的任意一点)树中任意一点,距该点距离最远的点,一定是(该)直径的某个端点。
  4. 对于两棵树,如果第一棵树的直径为 \((u,v)\) ,第二棵树的直径为 \((x,y)\) ,用两条边将两棵树连起来,新直径的端点一定在 \(x,y,u,v\) 中。
  5. 对于一棵树,如果在一个点上面接上一个叶子结点,那么最多会改变直径的一个端点。

性质 3 证明:

首先若 \(p\) 在直径上,反证法易证。

否则:

imgimg

性质 4 证明:

新树直径如果不是原来两棵树的直径,那么就一定会跨过那条连接边,新直径在原来每棵树的部分一定是距离连接点距离最长的点,根据性质 3,一定是原来每棵树各直径的端点。

性质 5 证明:

若在 \(y\) 下面接上 \(x\) ,直径变成 \((x,u)\) ,原树直径为 \((a,b)\) ,则 \(d(x,u)>d(a,b)\) ,因为 \(x\) 是叶子,所以 \(d(y,u)+1>d(a,b)\) ,若 \(d(y,u)>d(a,b)\) ,矛盾,否则 \((y,u)\) 也是一条直径。

pro: 给定一棵树,对于每个点,输出离他最远的点到它的距离。

根据性质 3 ,我们可以求出任意一条直径,对于每个点,比较它到直径两个端点的距离即可。

pro: 给定一颗树,动态加入叶子,输出每次加入叶子以后直径的长度。

首先,根据性质 3,我们直径一定只能在 \(n\) 对点 \((x,y)\) 之间选,其中 \(y\) 是距离 \(x\) 最远的点。

根据性质 2 ,我们可以维护出一条直径 \((x,y)\) ,每次加入一个叶子,我们可以快速维护出它的倍增数组,用 \((x,leaf),(y,leaf)\) 去更新 \((x,y)\) 就行了

树的中心

树的中心定义为:该点到树中其它点的最远距离最小。

满足以下性质:

  1. 中心一定在某条直径上,并且到直径两端的距离差不超过 1
  2. 即使树有多条直径,但树的中心至多只有两个,且均为直径的中点

我们可以先求出一条直径 \((x,y)\) ,在树上找出他们的 lca 为 \(z\),记直径的长度为 \(R\),若 \(x\)\(z\) 的长度大于 R/2 ,那么就在 x 到 z 上面找,否则在 z 到 y 上面找。

也可以正反做一遍 DP 搞定

树的重心

一颗具有 \(n\) 个点的无根树,若以某个结点为整棵树的根,它子节点的最大子树最小,则称这个点为树的重心。

换句话说,删除该点后,最大连通块的结点数最小,即生成的多棵树尽可能地平衡。

满足以下性质:

  1. 一个点是重心的充要条件是以这个点为根,每个子树大小都不超过 \(\frac{n}{2}\)
  2. 树的重心如果不唯一,则至多有两个,且这两个重心相邻,此时树一定有偶数个节点,且可以被划分成两个大小相等的分支。
  3. 树中所有点到重心的距离和最小;如果有两个重心,并且距离和一样。距离和最小和重心是等价的。
  4. 在一棵树上添加或删除一个结点,重心最多只移动一条边的位置。
  5. 把两棵树通过一条边相连得到的新树,新树的重心在较大的树一侧的连接点与原重心的路径上。如果两棵树大小一样,则重心就是两个连接点。

上面性质的证明:https://www.cnblogs.com/suxxsfe/p/13543253.htmlhttps://zhuanlan.zhihu.com/p/357938161

重心分解 https://codeforces.com/blog/entry/73707

pro: 给出一棵有根树,求出每棵子树的重心。

直接暴力是 n^2 的

根据重心的性质,我们可以从下往上求解。

对于结点 \(u\) ,我们先求出每个子结点的重心,那么 \(u\) 子树的重心一定在 \(u\) 到这些子树重心的路径上。假如每个子树的大小都不超过 sz[u]/2,那么 u 就是重心,否则一定会存在一个 \(v\) 使得 \(sz_v>\frac{sz_u}{2}\) ,那么重心必然在 \(u\)\(subtree(v)\) 重心的路径上,从下往上枚举就行了。

由于我们是从下往上枚举的,被标记为不是重心的点不会被枚举两次,所以复杂度是 \(O(n)\)

断边直径

给定一棵树,求对于所有断掉一条边所形成的的两颗树的直径。要求线性

https://www.luogu.com.cn/discuss/471518

首先下子树直接预处理每颗子树的最大深度和直径就行了。

\(f_{u,0/1}\)代表以 \(u\) 为根的最大深度/ \(u\) 子树的直径,\(g_{u,0/1}\) 为以 \(fa_u\) 为根、且不包含 以 \(u\) 为根子树的树的最大深度/直径,这两个都是可转移的。

树形DP

树形背包问题:

void dfs(int u,int fa){
	sz[u]=1;
	for(auto v:G[u]){
		if(v==fa)continue;
		dfs(v,u);
		F(i,0,sz[u]){
			F(j,0,sz[v]){
				f[u][i+j] <-- f[u][i] + f[v][j];
			}
		}
		sz[u]+=sz[v];
	}
} 

每一对点 \((x,y)\) 只会在 \(lca\) 处被统计一次,所以复杂度为 \(O(n^2)\)

树的统计

pro: 统计树上所有无序点对 \((x,y)\) 之间的距离和。边权为 1。 \(n\le 10^5\)

考虑每一条边 \((u,v)\) 的贡献,为以 u 为根子树的大小和以 v 为根子树的大小的乘积

pro: 一棵有根树,每个点上有颜色,有一些询问,每次询问以 \(x\) 为根的子树有多少个颜色为 \(query(x)\) 的点。\(n,q\le 10^5\)

我们先引入一个问题:有 \(n\) 个集合,每个集合的大小为 1,每次可以花费 \(\min(a,b)\) 的代价合并 \(a\) 和 $b $ 的集合,即删除 \(a,b\) 集合,并加入大小为 \(a+b\) 的集合,那么这样的代价和最大为多少呢?

一个显然的上界是 \(n^2\) ,不过还有更优秀的上界 \(nlogn\) ,因为每个元素合并它时它所在的集合大小至少乘 2,也就是说每个元素会被合并不超过 logn 次。

回到该题,我们树的形成过程可以看作是集合的合并过程,对于每个节点,它的信息是由其所有子树的信息(集合)合并起来加上自己的信息,它所代表的集合的大小可以看作是它子树的大小。所以直接将所有儿子的信息合并到它自身上就行了。

用 map 实现的。

void merge(int x,int y){
	if(mp[x].size()<mp[y].size())swap(mp[x],mp[y]);
	for(auto u:mp[y])mp[x][u.first]+=u.second;
}
void dfs(int u,int fa){
	++mp[x][col[x]];
	for(auto v:G[u]){
		if(v==fa)continue;
		dfs(v,u);
		merge(u,v);
	}
	ans[u]=mp[u][qry[u]];
}

当然,也可以 dsu on tree,思想基本一样

顺带一提,当我们合并 a,b 时需要花费 a+b 的代价,需要点(边)分治

pro: 一棵有根树,对于每个点 x,都有一个询问 \(qry[x]\) ,询问 \(x\) 子树中有多少个点到 x 的距离为 \(qry[x]\)

我们先来引入一个这样的问题:有 \(n\) 个集合,每个集合的大小为 1,每次可以花费 \(\min(a,b)\) 的代价合并 \(a\) 和 $b $ 的集合,即删除 \(a,b\) 集合,并加入大小为 \(\max(a,b)+1\) 的集合,那么这样的代价和最大为多少呢?

显然 \(\max(a,b)+1\le a+b\) 所以一个上界是 \(O(nlogn)\) 的。我们一开始支付 \(n\) 的代价让每一个点存储 1 点能量,并且时刻保持大小为 \(k\) 的集合的能量为 \(k\),当合并 \(a,b\) 时,支付 \(\min(a,b)=a\) 的代价,产生了一个 \(\max(a,b)+1\) 大小的新集合,所以多支付 1 的代价存储在合并后的集合以满足之前的定义。因为需要恰好合并 \(n-1\) 次,因此把所有支付的代价加起来,发现一共花了 \(2n-1\) 的代价,所以上界为 \(O(n)\)

还有一个跟这个思路差不多的且广为人知的名字:长链剖分

回归该题,我们树的形成过程可以看作是集合的合并过程,对于每个节点,它的信息是由其所有子树的信息(集合)合并起来加上自己的信息,它所代表的集合的大小可以看作是它子树中不同深度的个数,即最大深度减去它的深度,直接在 dfs 的过程中把儿子合并上去就行了。

void merge(int x,int y){
	if(mp[x].size()<mp[y].size())swap(mp[x],mp[y]);
	for(auto u:mp[y])mp[x][u.first]+=u.second;
}
void dfs(int u,int fa,int de){
	++mp[x][de];
	for(auto v:G[u]){
		if(v==fa)continue;
		dfs(v,u);
		merge(u,v);
	}
	ans[u]=mp[u][de+qry[u]];
}

最短路

单源最短路问题是说,给定一张有向图 \(G=(V,E)\)\(n\) 为节点数 \(|V|\)\(m\) 为边数 \(|E|\),节点以 [1,n] 的连续整数编号,\((x,y,z)\) 描述一条从 \(x\) 出发,到达 \(y\) ,长度为 \(z\) 的有向边。设 \(1\) 号点为起点,求长度为 \(n\) 的数组\(dis_u\) 表示,从源点到顶点 \(u\) 的最短路。

性质:对于不存在负环的图,任意两个顶点之间的最短路点数不会超过 n ,边数不会超过 n - 1 。否则根据抽屉原理,一定会存在一个正环,不优。

Bellman-Ford

给定一张有向图,若对于每一条边都满足 \((u,v,w)\) 都有 \(dis[v]\le dis[u]+w\) 成立,则称该边满足 三角形不等式。若所有边都满足三角形不等式,则 \(dis\) 数组就是所求的最短路。

Bellman-Ford 基于迭代思想,称一轮 松弛 为对于每一条边 \((u,v)\) ,用 \(dis_u+w_{u,v}\) 更新 \(dis_v\) 。这样做的含义是用 \(1\rightarrow u \rightarrow v\) 这条路径去更新 \(v\) 的最短路。我们断言每轮至少有一个节点的最短路被更新,松弛 \(n-1\) 轮即可,松弛完成后所有边一定满足三角不等式。

正确性证明:设源点为 1,在 \(1 \rightarrow u\) 的最短路 \(1\rightarrow p_1 \rightarrow...\rightarrow u\) 中,对于每个节点 \(p_i\)\(1\rightarrow p_1 \rightarrow...\rightarrow p_i\) 也一定是是 \(1 \rightarrow p_i\) 的最短路,反证法可证。所以一个节点的最短路一定由另一个节点扩展而来。因为最短路条数最多有 \(n-1\) 条,而第 \(i\) 轮松弛会得到所有长度为 \(i\) 的最短路,故只需松弛 \(n-1\) 轮。

其实还可以在以 \(1\) 为根的最短路树上考虑,bellman-ford 的松弛迭代操作,实际上就是按顶点距离 \(1\) 的层次,逐层生成这棵最短路树的过程。进行了第 \(i\) 轮松弛后,最短路长度为 \(i\) 的点会被加入第 \(i\) 层中。因此,每进行一轮,树上就会有一层顶点到达其最短距离,此后这层顶点的 \(dis\) 值就会保持不变,不再受后续松弛操作的影响。

比如路径 \(1\rightarrow p_1 \rightarrow...\rightarrow u\) ,第一次松弛 \((1,p_1)\) ,第二次松弛 \((p_1,p_2)\),所以长度为 \(i\) 的最短路在第 \(i\) 轮松弛后一定会被更新到。我们反复对每条边进行松弛操作,更新不满足三角形不等式的边,使得 \(dis\) 逐步逼近其最短距离,直到所有边全都收敛,即全部都满足三角形不等式。

该算法还可以判断一张图上是否存在 负环 。如果在第 \(n\) 轮松弛时仍有节点的最短路被更新(即不满足三角形不等式,存在边未收敛),那么说明该图存在负环。

时间复杂度 \(O(nm)\)

Dijkstra

Dijkstra 基于 贪心 的思想,适用于 非负权图

扩展 节点 \(u\) 为对于 \(u\) 的所有出边 \((u,v)\) ,用 \(dis_u+w_{u,v}\) 更新 \(dis_v\)。思考一下可知 \(dis_u\) 的含义就是从源点开始中间直径过 已扩展过的集合 的点到达 \(u\) 的最短距离。

我们将点分成两个集合:已经扩展过的点集和未被扩展过的点集。在已经得到最短路的节点中,取出没有被扩展过的距离源点最近(即 dis 最小的)的节点并扩展。因为没有负边权,所以取出节点的最短路长度单调不降。

如何判断一个节点已经取到了最短路呢?实际上不需要判断,每次取出的节点恰取到了其最短路。根据边权非负以及 dijkstra 的贪心算法流程,可以通过归纳法与反证法证明这一点。

归纳假设已经扩展过的节点 \(p_1,p_2,...,p_{k-1}\) 在扩展时均取到其最短路。\(p_k\) 为没有被扩展的 \(dis\) 最小的点。

\(p_k\) 的最短路一定由 \(p_i ~(1\le i< k)\) 的最短路扩展而来,不可能出现 \(dis(p_i)+w(p_i,p_{k+1})+w(p_{k+1},p_k)<dis(p_j)+w(p_j,p_k)~(1\le i,j<k)\) 的情况。否则由于边权非负,\(w(p_{k+1},p_k)\ge 0\),所以 \(dis(p_i)+w(p_i,p_{k+1})<dis(p_j)+w(p_j,p_k)\),即当前的 \(dis(p_{k+1})<dis(p_k)\)\(dis(p_k)\) 的最小性矛盾。

初始令源点的 \(dis\)\(0\) ,假设成立,因此算法成立。

取出 \(dis\) 最小的点可以用优先队列维护。每次扩展 \(u\) 时,若 \(dis_u+w_{u,v}<dis_v\) ,那么将 \((v,dis_u+w_{u,v})\) 即节点编号和它更新后的二元组 \(dis\) ,作为二元组丢进队列。尝试取出节点时,以 \(dis_u+w_{u,v}\) 为关键字取出最小的二元组,则它对应的节点编号就是我们要找的点。

注意,一个点可能有多个入边并多次进入优先队列。但当它第一次取出时,得到的一定是最短路。为此,需要记录 vis[i] 表示一个节点是否被扩展过。若当前取出的节点已经被扩展过了,则忽略。也可以判断是否有当前二元组的第二个元等于第一个元的 \(dis\),若不等说明该节点已经被扩展过了。否则复杂度会退化成 \(m^2logm\)

时间复杂度 \(O(mlogm)\)

最短路径的任意子路也是最短路径,从堆中取出点得最短路值一定不降。

img

image-20220824191304882

P4779 模板题

SPFA

本质上是优化的 bellman-ford。

显然,只有上一次被松弛的节点,所连接的边,才有可能引起下一次松弛操作。松弛操作必定只会发生在最短路径前导节点松弛成功过的节点上,用一个队列记录松弛过的节点,可以避免冗余计算

松弛 \(x\) 时找到接下来有可能松弛的点,即与 \(x\) 相邻且 最短路被更新的点 并压入队列。通过这样,队列中都保存了待扩展(松弛成功)的顶点,每入队一次相当于完成了一次 \(dis\) 的更新,使其满足三角形不等式。一个节点可能会入队、出队多次。最终图中所有顶点收敛到满足不等式的状态,相对于BF,避免了对不需要扩展的节点进行冗余扫描(即对一些边无用的松弛操作)。此外,需记录一个点是否在队列中,若是则不压入,可以减小常数。

正确性证明:对于最短路径 \(1 \rightarrow p_1 \rightarrow p_2 \rightarrow ... \rightarrow u\) ,将 \(1\) 号点的每个邻居进行松弛操作,由于对于任意 \(i\)\(1 \rightarrow p_i\) 也是最短路,所以 \(p_1\) 一定在队列里。然后对于队列里的每个点,我们把它能到达的点加入队列中,这时我们有保证了 \(p_2\) 在队列中。另外注意到,假如 \(p_i \rightarrow p_{i+1}\) 是目标最短路上的一段,那么在松弛这条边的时候一定会被更新,所以一条边如果未被更新,那么它的终点就不入队。

时间复杂度相比 bellman-ford 没有区别,仍为 \(O(nm)\) 。在一般图上效率很高,但可以被特殊数据卡成平方,所以尽量使用 dijkstra。

注意,使用 SPFA 求解点对点的最短路径 (费用流 EK),当队头为目标节点时不能结束算法。因为一个节点进入队列并不等同于它已经取到了最短路。

SPFA 判负环:若一个点 进入队列 超过 \(n-1\) 次(注意不是 被松弛,因为一个节点被松弛不意味着进队),或 最短路边数 大于 \(n-1\) ,则整张图存在负环。对于后者,记录 \(l_i\) 表示从源点到 \(i\) 的最短路长度,松弛时令 \(l_v \leftarrow l_u + 1\) 判断是否有 \(l_v<n\) 即可。

在稀疏图和随机图上效率比较高 \(O(km)\) ,其中 \(k\) 是较小的常数。在稠密图和网格图或精心构造的图上能被卡成 \(O(nm)\)

https://blog.csdn.net/muxidreamtohit/article/details/7894298

https://www.cnblogs.com/BlackDan/p/16098485.html

SLF优化

SLF(Small Label First),使用双端队列实现。

在原有的 SPFA 中,将松弛成功的点与队头元素比较,进行判断:若小于进队头,否则进队尾。小细节:队列为空时直接插入队尾。

LLL优化

LLL(Large Label Last)。

设队列 \(dis\) 的平均值为 \(ave\)

在出队的时候调整:设当前队首元素为 \(x\) ,若 \(x>ave\) ,那么就将该元素移至队尾,直到出现 \(x\le ave\) ,将 \(x\) 出队进行松弛操作。

SLF+LLL优化

SLF 是入队的优化,LLL 是出队的优化,这两部分互不影响,合并也可以达到优化效果。

dfs_spfa

SPFA 是在bellman-ford 的基础上,采用广度优先的思想,每松弛一个节点,都将它放入队列末尾,其缺点是中断了迭代的连续性。如果我们采用深度优先的思想,我们可以直接从这个节点不断向下松弛,不断递归求解。常用于判断正环/负环,可以达到 \(O(m)\)。如果我们 dfs 的时候走回了访问过的节点,那么存在一个环,用栈来记录就行了。

但实际这是一个假算法,可以被卡到指数级!!!

如何卡SPFA?

https://www.cnblogs.com/luckyblock/p/14317096.html

Johnson 全源最短路

Johnson 用于解决 带有负边权全源最短路 问题。

先来看一下全源最短路的几个基本想法:

  1. Floyd:\(O(n^3)\) ,能够解决负边权,不能解决负环,慢,常数小;
  2. Dijkstra:以每个源点为起点做一遍 Dijkstra ,\(O(nmlogm)\) ,时间可以,但只能解决非负边权;
  3. Bellman-Ford:以每个点为源点做一遍 Bellman-Ford ,\(O(n^2m)\) ,太慢了。

首先来解决 负环 问题,跑一遍 Bellman-Ford 就行了。

注意到 Dijkstra 的问题只有边权的正负问题,而 Johnson 算法 就是改造这张图的边权,使它等效成一张非负权图的方案。

一个简单但错误的想法是:我们给每条边加上 偏移量 \(d\) ,求一遍最短路并记录其长度 \(k\) ,最后再减去 \(kd\)

但这样是显然不行的,因为我们无法控制最短路的长度,导致答案不优。

我们记每个点的势能为 \(h_i\),最初全部为0,跑一遍bellman-ford。对于原图中的每条边 \((u,v,w)\) ,将边权 \(w\leftarrow w+h_u-h_v\) ,再对新图每个点跑一遍 Dijkstra 就行了。

正确性证明:先来引入 势能 的概念,诸如重力势能、电势,势能的变化量只和起点和终点的相对位置有关,而与起点到终点所走过的路径无关。换句话说,从一个点出发,到达另一个点,无论走什么路径,势能的总变化量是一定的。物理启发我们将边 \((u,v)\) 的新权值 \(w_{u,v}'\) 设置为 \(w_{u,v}+h_u-h_v\)

考虑一条路径 \(S \rightarrow p_1 \rightarrow p_2 \rightarrow ... \rightarrow T\) ,其原长为 \(L(S\rightarrow T) = w_{S,p_1}+w_{p_1,p_2}+...+w_{p_l,T}\) ,新长度 \(L'(S\rightarrow T) = (w_{S,p_1}+h_S-h_{p_1})+(w_{p_1,p_2}+h_{p_1}-h_{p_2})+...+(w_{p_l,T}+h_{p_l}-h_T)\)

不难看出 \(L'(S\rightarrow T) = L(S\rightarrow T) + (h_T-h_S)\) 。这说明,对于固定的 \(S,T\) ,原图的任意一条路径,对应到新图上长度增加了 \((h_T-h_S)\) 。这与路径经过了哪些点无关,只与 \(S,T\) 有关,所以新图上的最短路 仍为最短路

由于 \(h\) 对于 \((u,v)\) 满足 三角形不等式 \(h_u+w_{u,v}\ge h_v\) ,移项发现 \(w_{u,v}+h_u-h_v\ge 0\) !。在无负环的情况下,我们在原图上不断松弛最终得到了一个这样的 \(h\)。具体地,初始令所有 \(h\) 等于 \(0\),进行 BF。上述操作等价于建立超级源点,向每个点连边权为 \(0\) 的边,并求出最短路 \(h_i\),所以最多松弛 \(n-1\) 轮。不要忘了加上 \(h_v-h_u\)

P5905

Floyd

\(dis_{k,i,j}\) 表示 只经过编号不超过 \(k\) 的节点 ,从 \(i\)\(j\) 的最短路长度。该问题可以划分成两个子问题,经过编号不超过 \(k-1\) 的节点从 \(i\)\(j\),或者从 \(i\) 先到 \(k\) 再到 \(j\)\(dis_{k,i,j}=\min(dis_{k-1,i,j},\min(dis_{k-1,i,k}+dis_{k-1,k,j}))\)\(k\) 是阶段,必须置于外层循环中,\(i\)\(j\) 是附加状态, \(k\) 这一维可以省略,得到 \(dis_{i,j}=\min(dis_{i,k}+dis_{k,j})\)

正确性证明依然考虑归纳法:对于最短路径 \(u \rightarrow p_1 \rightarrow p_2 \rightarrow ... \rightarrow v\) 。令 \(p_i\)\(p\) 当中最后一个被枚举到的点,若 \(dis_{u,p_i}\)\(dis_{p_i,v}\) 已取得最短路,那么 \(dis_{u,v}\) 自然能取得最短路。

存图用邻接矩阵,初始化 \(dis_{i,j}=w_{i,j}\)

时间复杂度为 \(O(n^3)\) ,在稠密图上效率比 Johnson 高,因此 Floyd 也是数据规模较小时的不二之选。

此外,Floyd 还可以求 传递闭包

在交际网络中,给定若干个元素和若干对二元关系,且关系具有 传递性 。“通过传递性推导出尽量多元素之间的关系的问题被称为传递闭包。

传递性:设 \(\bigodot\) 是定义在集合 \(S\) 上的二元关系,若对于 \(\forall a,b,c\in S\) ,只要有 \(a \bigodot b\)\(b\bigodot c\) ,就必然有 \(a \bigodot c\),则称关系 \(\bigodot\) 具有传递性

有向图的传递闭包定义为 \(n\) 阶布尔矩阵 \(T\) ,满足 \(T_{i,j}\)\(i\) 可达 \(j\) 时等于 \(1\) ,否则为 \(0\) 。只需要将内层操作改成 \(T_{i,j}\leftarrow T_{i,j}\or (T_{i,k}\and T_{k,j})\) 即可。

bitset 可将传递闭包的复杂度优化至 \(O(\frac{n^3}{w})\) ,对于稀疏图,缩点后可以做到 \(O(\frac{nm}{w})\)

Acwing 340 Telephone Lines

简化题意:在无向图中求出一条从 1 到 N 的路径,使路径上第 K+1 大的边权尽量小。\(K<N\le 1000,M\le 10000\)

(1)分层图最短路

我们可以仿照 DP 的思想,用 \(f_{u,i}\) 代表从 1 号点到达节点 u ,途中指定了 \(i\) 条边免费,经过路径上的最大边权最小是多少(也就是选择一条 1 到 x 的路径,使路径上的第 i+1 大的边权尽量小)。若有一条有向边 \((u,v,w)\) ,那么 \(f_{v,i+1}\xleftarrow{min} f_{u,i}~,~f_{v,i}\xleftarrow{min} \max(f_{u,i},w)\)

DP 的 “无后效性” 告诉我们:DP 对状态空间的遍历构成一张 DAG,遍历顺序就是该 DAG 的一个拓扑序。DAG 的节点对应问题中的 状态 ,图中的边对应状态之间的 转移 ,转移的选取就是 DP 当中的 决策。显然,我们设计的 DP 状态是有后效性的。在有后效性时,一种解决方法就是依据迭代思想,借助 SPFA算法 进行动态规划,直至所有状态收敛(不能再更新)。

从最短路的角度去理解,我们将编号扩展到二维,用二元组 \((u,i)\) 表示一个节点,\((u,i)\)\((v,i)\) 有长度为 \(w\) 的边,\((u,i)\)\((v,i+1)\) 有长度为 \(0\) 的边,\(f_{u,i}\) 代表从源点到 \(u\) 的路径上最长边最短是多少。

(2)二分答案+01bfs

答案具有单调性,我们在这个最优性函数的定义域 \(I\) 上考虑,不妨设最优解的评分为 \(S\),对于 \(\forall x<S\) ,都不存在一个合法方案不超过 \(x\),否则就与 \(S\) 的最优性矛盾,对于 \(\forall x>S\) ,一定存在一组合法方案小于或等于 \(x\),因为最优解就满足这个条件,于是将问题弱化 \(P(x)=[是否存在一个合法方案使得第 K+1 大边最短小于等于x]\),我们将边权 \(> x\) 的看成长度为 1 的边, 边权 \(\le x\) 的看成长度为 0 的边,然后求一遍最短路,检查最短路是否小于等于 \(K\) 即可。

Acwing 341 最优贸易

无向边可以看成两条方向相反的有向边,建立原图的一个反图,从一号点开始 SPFA,求出一个数组 \(dis_u\) 表示 1 到 u 的所有路径中,能够经过的最小点的权值,与最短路计算过程类似,用 \(min(dis_u,w_v)\) 更新 \(dis_v\) 就行了,注意上面的更新不满足 Dijkstra 的贪心性质。反图同理。

最后枚举每个点,更新答案就行了

Acwing343 Sorting It All Out

这是一道 有向的传递闭包问题,对于每一个形如 \(i<j\) 的不等式,令 \(dis_{i,j}=1\) ,否则有 \(dis_{i,j}=0\)

使用 Floyd 对 dis 做传递闭包,若存在一对变量 \(i,j\) 使得 \(dis_{i,j}=dis_{j,i}=1\) ,说明不等式矛盾,若存在变量 \(i,j\) 满足 \(dis_{i,j}=dis_{j,i}=0\) 则这些不等式不能确定这一对变量的大小关系。我们可以二分 \(t\) 的值,判断仅用前 \(t\) 个不等式能否达到条件。

P5304 GXOI/GZOI2019 旅行者

有趣题,暂不研究。

P1462 通往奥格瑞玛的道路

答案满足可二分性,设 \(P(x)=[存在一条合法路径使得最大点权不超过x]\) ,即所有点权不能超过 \(x\) ,将大于 \(x\) 的删掉,跑一遍 dijkstra 就行了。\(O(mlogm logc)\)

P4568 JLOI2011 飞行路线

注意到 k 很小,分层图最短路,每一层记录免费了多少条边。\(O(mklog(mk))\)

P2761 软件补丁问题

问题全都是围绕软件和补丁之间集合的关系,不妨将错误用二进制表示,一个软件相当于一条权为时间的边,求一遍单源最短路就行了。

因为补丁数量很小,所以从初始态能到达的点不多,状压+SPFA/Dijkstra。

这里有一个小 trick:边的上界为 \(10^8\) ,邻接表存不下。由于实际是跑不满的,所以我们对于每个点直接枚举所有补丁看是否有出边即可。

差分约束

问题引入:有 \(n\) 个未知量 \(\{x_i\}\)\(m\) 个约束条件,每个限制形如 \(x_i-x_j≤c\)\(x_i-x_j\ge c\),求出一组 \(\{x_i\}\) 的合法解,使得满足 \(m\) 个约束条件。差分约束就是求解上述形式特殊的 \(n\) 元一次不等式

我们发现,只要 \(x_i,x_j\) 在不等式的某一侧符号不同,那么所有限制均可以写成 \(x_i+c\ge x_j\)

三角形不等式 再一次出现。我们从 \(i\rightarrow j\) 连一条长度为 \(c\) 的边,然后再从超级源点 \(0\) 向每个点连长度为 \(0\) 的边防止图不连通(或者一开始令所有点的 \(dis=0\) 并将所有点入队,跑最短路,每个点的最短路长度就是一组 有界的 可行解。

不难发现一个性质:如果 \(\{x_1,x_2…,x_n\}\) 是一组合法解,则 \(\{x_1+d,x_2+d,…,x_n+d\}\) 也是一组合法解。

这个 \(c\) 一般有负值(全是非负的话所有数组不就行了嘛),所以用 BF 或 SPFA 求解最短路。显然,若出现负环无解(其实并不怎么显然,证明挺繁琐的)。

时间复杂度 \(O(nm)\)。模板题 P5960 代码如下。

解的字典序极值

一般而言差分约束系统的解没有 “字典序” 这个概念,因为我们只对变量之间的差值进行约束,而变量本身的值可以随着某个变量取值的固定而固定,所以解的字典序可以趋于无穷大或无穷小。

字典序的极值建立于变量有界的基础上。不妨设希望求出当限制 \(x_i\le 0\) 时,整个差分约束系统的字典序的最大值。注意到 \(x_i\le 0\) 可以看成三角形不等式 \((x_0=0)+0\ge x_i\) ,所以我们建立虚点 \(0\) ,将其初始 \(dis\) 赋为 \(0\) ,并向其他所有变量连权值为 \(0\) 的边(这等价于一开始令所有点入队且 \(dis=0\))。

求解差分约束系统时我们使用了三角形不等式,将其转化为最短路问题。这给予它很好的性质:通过 SPFA 求得的一组解,恰为字典序最大解

首先明确一点,对于从 \(u \rightarrow v\) 的边权为 \(w(u,v)\) 的边,它的含义为限制 \(x_u+w(u,v)\ge x_v\)

考虑 \(0\) 到每个点的最短路树。对于树上每条边均满足 \(x_i+w(i,j)=x_j\) 。若 \(x_i+w(i,j)<x_j\) ,那么整张图还可以继续松弛;若 \(x_i+w(i,j)>x_j\) ,说明 \(j\) 的最短路不是由 \(i\) 继承而来,因此 \((i,j)\) 必然不会作为树边出现。

这说明树上的 \(x_i+w(i,j)\ge x_j\) 的限制已经取满,取到了等号( \(x_j\) 不能再大了,再大就破坏限制),每个变量都取到了其理论上界,自然就得到了字典序最大解。

对于字典序最小解,我们限制 \(x_i\ge 0\) 。因此所有节点向 \(0\) 连边 \(w(i,0)=0\) 。字典序最小即字典序最大时的相反数。因此,考虑将所有变量取反,将负号代入,那么限制也要全部取反,体现为将图中的所有边(包括所有新建的边 \(i\rightarrow 0\) )的 方向 (而不是边权)取反。然后取字典序最大解,再取相反数即可。

最小生成树

无向连通图。

https://blog.csdn.net/zengchen__acmer/article/details/17323245

(1)定义在一棵树里添加一条边,并在产生的环里删除一条边叫做一次操作。则树 A 和 B 是无向图的两个生成树,那么 A 可以通过若干次操作变成 B

证:将生成树看做边的集合,如果 B 中有一条 A 没有的边,则将这条边加到 A 上,A 产生的环至少有一条是 B 没有的边,把这条边删掉,A 仍然是生成树,A,B中相同的边多了一条,这个过程可以递归下去直到所有边都相同。

这告诉我们:任意两棵生成树都可以通过不断换边得到,重要的是换边的过程形态是树

(2)把一个无向连通图的生成树按边权递增排成的序列称作有序边权列表,则任意两棵最小生成树的有序边权列表是相同的。

(3)A,B 是同一个无向连通图的两棵不同的最小生成树,则 A 可以通过若干次 (1) 定义的换边操作,并且保证每次结果仍然是最小生成树,最终转换成 B

(4)一个无向连通图,不会有一棵最小生成树包含图中一个环中最大权值唯一的边。

证:依然反证法,设该条边为 \(e\) ,在生成树上删掉 \(e\) 后形成两个连通分量 \(A,B\) , 环 \(C\) 去掉该边是一条路径 \(C-e\),这条路径一些点在 \(A\),也有一些点在 \(B\),那么用这条路径上跨过两个分量的边替换即可。

(5)对于一个无向连通图,只考虑它的边权,形成的有序边权列表中,最小生成树的列表是字典序最小的。

(6)一棵生成树不是最小生成树,则一定存在一个(1)中描述的操作,使得操作之后,它的总权值减小,直到把它转换成最小生成树。

(7)如果一棵生成树,任何边都在某棵最小生成树上,则它不一定是最小生成树。

定理1 : 最小生成树一定包含无向图中权值最小的边。

推论 : 给定一张无向图,从 \(E\) 中选出 \(k < n - 1\) 条边构成 \(G\) 的一个生成森林。若再从剩余的 \(m - k\) 条边中选 \(n-1-k\) 条添加到生成森林中,使其成为 \(G\) 的生成树,且选出的边权和最小,则该生成树一定包含 \(m - k\) 条边中连接生成森林的两个不连通节点的权值最小的边。

定理 2: 一个无向图,如果它的边权都不相同,那么它的最小生成树唯一。

瓶颈生成树

无向图 \(G\) 的瓶颈生成树是这样的一棵生成树,它的最大边权值在 \(G\) 的所有生成树中是最小的。

最小生成树是瓶颈生成树的充分不必要条件。最小生成树一定是瓶颈生成树,瓶颈生成树不一定是最小生成树。

反证法:我们假设最小生成树的最大边权为 \(w\) ,有假设知瓶颈生成树的所有边权都小于 \(w\) ,我们删除生成树上最大边会得到两棵子树,我们用瓶颈生成树中连接这两棵子树的边替换掉 \(w\),所得的边权和一定比 MST 小,矛盾。

最小瓶颈路

无向图 \(G\)\(x\rightarrow y\) 的最小瓶颈路是这样的一类简单路径:满足这条路径上的最大的边权在所有 \(x\)\(y\) 的简单路径中是最小的。

任意两个点的最小瓶颈路一定在最小生成树上,因为只关心最大边,考虑 Kruskal 的过程(也是只关心最大边),当我们将边权从小到大排序后,当加入某一条边时,\(u\)\(v\) 恰好连通,那么这条路径就是最小瓶颈路,且出现在最小生成树上

https://www.cnblogs.com/konjak/p/6020958.html

单组询问我们可以用 广义最短路 做,DP 用 SPFA/Dijstra 不断松弛直到收敛即可。

具体地,我们设 \(dis_u\) 代表最大边权的最小值,用 \(\min(dis_u,w(u,v))\) 更新 \(dis_v\) ,这个满足 Dijkstra 的贪心性质,即先加入点集的 \(dis\) 值之后一定不会被未加入的点集更新

https://blog.csdn.net/m0_55982600/article/details/120795946

Kruskal

基于以上推论,Kruskal算法总是维护无向图的最小生成森林。在任意时刻,从剩余的边中选出一条权值最小的·,并且两个端点属于生成森林的两棵不同的树,把该边加入森林中。具体地,贪心地将所有边按照边权从小到大排序并枚举每一条边,如果当前边两端点不连通则加入该边。连通性用并查集维护。\(O(mlogm)\)

证明考虑反证法,若存在一条边 \((u,v)\) 使得 \((u,v) \notin T\) 且在加入 \((u,v)\)\(T\) 上形成的环当中 \(w(u,v)\) 不是最大权重,那么一定可以断掉最大边得到一棵更小的生成树 \(T'\)。但这与贪心策略矛盾,因为 \((u,v)\) 一定在被断掉的这条边之前被枚举到并加入 \(T\)

Prim

Prim 算法总是维护最小生成树的一部分,设已经确定的集合为 \(T\),剩余节点集合为 \(S\)。prim 找到 \(\min_{y\in T\and x\in S} (z)\) ,即两个端点分别属于集合 \(T,S\) 的权值最小的边,然后把 \(x\) 删除加入到 \(T\) 中。初始 \(T\) 可以包含任意一个节点。

具体地,我们可以维护数组 \(d_x\) ,若 \(x\in S\) ,则 \(d_x\) 表示节点 x 与集合 T 中节点之间权值最小的边,加入时扫描 \(x\) 的所有出边,更新别的点的 \(d\) 值即可。可以类比 Dijkstra 算法,标记一个点是否属于 \(T\)\(O(mlogm)\)\(O(n^2)\),prim算法只要用于稠密图或完全图上。

反证法:假设最小边为 \(e=(u,v),u\in T ,v\in S\),我们来证明这条边一定出现在最小生成树中:假设最小生成树中不含改该边,那么加入这条边会形成一个环,环上一定有一条边的两个端点分别在 \(T,S\) 中,直接替换,矛盾。

Boruvka 算法

其实是一种多路增广的 prim。

考虑若干个点,对于每个点而言,如果想要使得它有边相连,那么与它相邻当中的边权最小的边必然入选,这一点反证法可以证明。

因此我们将连通块看成点,对每个连通块找到连向别的连通块边权最小的边,加入。

  1. 定义 \(E'\) 为我们当前找到的最小生成森林的边集。在算法执行过程中,我们逐步向 \(E’\) 加边,定义 连通块 表示一个点集 \(V'\subseteq V\) ,且这个点集中的任意两个点 \(u,v\)\(E'\) 中的边构成的子图上是连通的(互相可达)。

  2. 定义一个连通块的 最小边从这个连通块出发、不在最小生成树上,到达其他连通块 的边中的最短边。

1.最优性:最小边一定包含在生成树中。
2.合法性:一定不会构成环。如果存在环说明一个点的最小连边有两个,显然矛盾。

初始时,\(E'=\empty\) ,每个点各自是一个连通块。遍历每条边,我们每次找出当前所有连通块的 最小边 ,将每个连通块的最小边加入 \(E'\) ,这时可能会出现两个连通块互连的情况,打个标记,说明该边已经出现在MST中。对于权重相同的边,我们按标号为第二关键字,这是为了防止两个连通块互相连最小边时会出现环,可以直接大力判环。

当原图连通时,每次连通块数量至少减半,最坏复杂度是 \(O(mlogn)\)

在解决某类最小生成树问题中非常有用。它形如给定一张 \(n\) 个点的完全图,两点之间的信息可以通过某种计算方式得出,\(n\) 一般在 \(10^5\) 级别。

总结一下,Kruskal是边拓展,Prim是点拓展,boruvka 就是两者的结合体。为什么这么说呢?因为 boruvka 将其中的每棵树看成了一个点,和 Kruskal 一样维护了最小生成森林,扩展的时候又和 prim 一样找最小边

1.为了避免边权相同的情况,以点标号为第二关键字,为了方便维护最小点编号,把每个联通块在并查集上的代表元素设为该联通块内的最小元素。
2.这个算法执行完后会有重边,可以利用一些奇怪的方法去重。
3.注意把给边去重和merge这两部分操作分开做(防止影响f数组)。

次小生成树

可以证明,一定存在一棵严格次小生成树,使得它与某棵最小生成树仅有一条边的差距。

反证法:设严格次小生成树和最小生成树有 \(k(k\ge 2)\) 的边的差距,考虑在原树上断开这些边,形成 \(k+1\) 个连通块,用 \(k-1\) 条MST边将 \(k\) 个连通块连起来, 此时有一条边的差距,再用次小生成树的边连起来就行了。

有了这个性质,我们就可以寻找那条不属于MST,但属于次小生成树的边。枚举每一条非树边,断掉环上边权最大的边(如果最大边权和该边权相同,就断开环上的次大边,注意可能不存在次大边),那么就得到了包含这条边的生成树的最小者,取min后就是答案。树上倍增预处理。

[P4180 BJWC2010] 严格次小生成树

如上

省队二轮集训Day4生成树

给定 \(n\) 个点, 求它的曼哈顿距离最大生成树. \(n ⩽ 2 × 10^6\)

曼哈顿距离难以处理,考虑曼哈顿距离转切比雪夫 \(x\leftarrow x+y,y\leftarrow x-y\)\(|x_1-x_2|+|y_1-y_2|\leftarrow \max(|x_1-x_2|,|y_1-y_2|)\)

先来引入一个问题:给定数轴上 \(n\) 个数,坐标为 \(\{x_i\}\) ,问 \(\sum_{1\le i,j\le n} |x_i-x_j|\) 最大是多少?

贪心,按 \(x\) 从小到大排个序,易发现 \(j\)\(x\) 中最小/最大的一定更优,至于是选哪个,比较一下就行了。还有一个很好的性质,不妨设 \(x\) 最小和最大的点分别为 \(a,b\) ,把 \((i,j)\) 看成一条边,则所有点一定只会向这两个点连边,即这是一棵最大生成树。

Solution1

回归这题,我们考虑 prim 算法,对于当前点集,我们需要找到连向不在点集内的点的最大距离,先在点集和不在点集中横/纵坐标最小/最大的4个端点(记为 \(S_1,S_2\),由于 \(\max(\max(|x_1-x_2|,|y_1-y_2|))=\max(\max(|x_1-x_2|),\max(|y_1-y_2|))\),于是我们将两部分分开考虑 ,然后将点全部映射到 \(x\) 维上,发现最大距离的两个顶点只能是 \((S_1,S_2)\)。调整法可证。

证:首先对于在点集中的点 \(i\) ,最优的 \(j\) 一定在 \(S_2\) 中,且 \(i\) 调整为 \(S_1\) 会更优。

于是只需要动态维护出 \(S_1,S_2\) 就行了,\(S_2\) 可以先排个序然后开个指针扫(也可以开个堆做,只是没有插入操作)。

纵坐标同理。做法显而易见了,维护出 \(S_1\) 矩形的边界,用 \(S_2\) 的点向边界的四个点中最优的连边。\(O(nlogn)\)

Solution2

从引入的问题获得启发,考虑求出所有点的 \(S\),那么所有点只会向 \(S\) 的点连边,对于每个点找到最优的点加入到边集中。注意这样对于 \(S\) 之间的连边可能会出现互连所导致的不连通情况,需要处理,将 \(S\) 之间的所有边加入到边集,Kruskal。这样的方案一定是可行解同时是最优解。

但是复杂度依然带个 log ,其实我们的思路已经很接近了,这就是 Boruvka 算法

我们对于每个点找到距离它最长的那条边,根据题目性质,这个点一定在 \(S\) 中,普通的 Boruvka 需要进行至多 \(logn\) 轮,但由于题目性质,一轮后连通块个数一定为 \(1 或 2\) ,所以这是 \(O(n)\) 的。

CF888G Xor-MST

Solution1

Kruskal 做,根据异或和最小值不难想到对所有 \(a_i\) 建出 01trie,我们找出所有度为 \(2\) 的点(一共 \(n-1\) 个),任意两点的异或值一定是它们 \(lca\) 以下部分的异或值,由此可得,若 lca 深度越深,便越优。

进一步的,考虑 01trie 上的最高位的左右儿子,此时跨过这个点连边时左右节点已经形成各自的连通块了(因为子树内部两两异或在这一位都是 0)。选出一条异或值最小的边将两个连通块合并,枚举左子树的每个点,在右子树查异或最小值即可,\(\min u⊕v\) 即为连边代价。

有一个小 trick:如果将元素从小到大排序加入 trie,那么一个集合内的元素就在同一区间了。

由于一个节点最多被枚举 \(logV\) 次,每次查询 \(logV\) ,总复杂度是严格 \(O(nlog^2 V)\) ,如果启发式合并,每次枚举左子树和右子树较小的那一个,花费 \(\min(size_l,size_r)\) 的代价,复杂度为 \(O(nlogVlogn)\) ,其实并没有太大区别。

Solution2

boruvka !

假设当前有若干个连通块(生成森林),需要找到每个连通块不在已加入的边、且连向别的连通块权最小的边。将所有 \(a_i\) 插入到 01trie,把该连通块的所有点删除,查找其中每一个点的异或最小值,这就是最小边权。每处理完一个连通块后别忘了再将所有点插回来。

连边时注意判环。

复杂度 \(O(nlogVlogn)\)

二分图的性质与判定

对于无向图 \(G=(V,E)\),若能划分成两个不相交点集 \(A,B\)\(A,B\) 的点导出子图不含边,则 \(G\) 称为二分图。\(A,B\) 称为左部点和右部点。

这即是说,\((u,v)\in E \rightarrow (u\in A,v\in B)\or (u\in B,v\in A)\)。简单来说,二分图就是可以将原图点集分成两部分,满足每个点集内部没有连边。

由此我们也可以用黑白染色来定义二分图:连通图 \(G\) 是二分图当且仅当可以给 \(G\) 的每个顶点染上黑色和白色,使每条边的两个端点颜色不同

这里存在一个性质:若二分图 \(G\) 包含 \(C\) 个连通分量,则黑白染色的方案为 \(2^C\) ,这是因为每个连通分量恰有两种染色方案。

我们发现,对于一张二分图,从一个点开始,每走一条边都会切换一次所在集合。这说明从任意一个点出发,必须经过偶数条边才能回到这个点,即图上不存在奇环。反过来说,若一张图不存在奇环,对其进行黑白染色就可以得到一组划分 \(X,Y\) 的方案。

定理:图 \(G\) 是二分图,当且仅当 \(G\) 中不存在奇环

证明:

\(G\) 中存在奇环,设环上的点为 \(v_1,v_2,...,v_{2n-1}\) ,使得 \(v_i\)\(v_{i+1}\) 为邻居 (\(v_{2n}=v_1\)) ,考虑环的黑白染色情况,易知 \(v_1,v_3,...,v_{2n-1}\) 同色,则 \(v_1\)\(v_{2n-1}\) 是一对相邻的同色点,不符合二染色的定义,由逆否命题知必要性得证。

\(G\) 中部存在奇环时,考虑每一个连通分量,以任意点为根求出一棵 生成树 (可以不是搜索树),由于不存在奇环,所以任意一条非树边连接的两个顶点深度奇偶性不同,考虑将深度为奇数的点染为黑色,深度为偶数的点染为白色,这就得到了一种二染色方案,故充分性得证。

推论:二分图 \(G\) 的任意子图为二分图。

推论:无向图 \(G\) 是二分图当且仅当其每个连通分量都是二分图。

根据上面的定理和证明过程,我们可以得到判定给定无向图是否是二分图的算法:

  • 设给定的图为 \(G\) ,由推论2 ,只需判定 \(G\) 的每个连通分量是否是二分图。
  • 对于 \(G\) 中的一个连通分量,对其进行 DFS ,求出一棵搜索树并记录每个点的深度,遇到反向边的时候,判定这条边两端点的深度是否同奇偶,如果同奇偶则 \(G\) 不是二分图。
  • 实现时,只需要记录每个点深度的奇偶性,这等价于模拟对 \(G\) 黑白染色的过程。
  • 复杂度 \(O(|E|)\)

事实上,结合上面的过程还可以得到一些比较显然的小 结论:例如连通图 \(G\) 不是二分图当且仅当对于任意生成树 \(T\) 都存在恰好包含一条非树边的奇环,这些结论比较贴近连通性相关知识了。

二分图有关 路径长度 的前线 性质: 二分图中任意两点间的路径经过的边数奇偶性确定;反之,一个 连通 非二分图中任意两点间路径经过的边数奇偶性都不确定。

(1)黑白染色

什么是黑白染色?我们希望给每个点染上白色或黑色,使得任意一条边两端的颜色不同。

若图有多个连通分量,我们只需要判断每个连通分量是否是二分图就行了。

对于每个连通分量,由于二分图的线段端点分别位于两个不同的点集中,因此连通图 \(G\) 是二分图当且仅当可以给 \(G\) 中的顶点染上黑色或白色,使每条边的两个端点颜色不同。

才能够某个点开始深搜,初始点颜色任意。遍历当前点 \(u\) 的所有邻居 \(v\) 。如果 \(v\) 未被访问,则将 \(v\) 的颜色设为与 \(u\) 相反的颜色并向 \(v\) 深搜。否则检查 \(u\) 的颜色是否与 \(v\) 的颜色不同——若是,则满足限制;否则说明图上存在奇环,黑白染色无解。

\(O(|V|+|E|)\)

黑白染色不仅给予了我们判定二分图的方法,还可以将二分图划分成两个点集。于是我们接下来讨论的二分图均指点集划分方案 \(X,Y\) 已经确定二分图。事实上,给定连通二分图,若 \(X,Y\) 之间无序,则将其划分成两部点的方案是唯一的。但对于非连通二分图,方案数不唯一,因为每个连通分量安排 \(X',Y'\) 的方案有两种。本质不同的方案有 \(2^{c-1}\) 种,其中 \(c\) 是连通分量个数。

(2)奇环判定法

一张图是二分图当且仅当这张图不存在奇环。

证明:

若一张图存在奇环,则奇环上的点一定不能被分成两个点集,使得每个点集内部没有连边。所以逆否命题就是一张二分图不存在奇环。

再来证不存在奇环的图一定是二分图:若不存在奇环,我们可以对每个连通块生成一棵搜索树,每条反向边连接的两个点深度的奇偶性必然不同,所以我们对深度为奇数的染黑,偶数染白,就得到了一种合法方案。

P1330 封锁阳光大学

给定无向简单图 \(G=(V,E)\) ,求最小的点集 \(A\subseteq V\) 使得 \(\forall (u,v)\in E\)\(u\in A\)\(v\in A\) 恰好成立其一,或报告无解。

容易发现,若存在这样的 \(A\) ,每条边恰好被 \(A\) 中的点覆盖一次,即 \(\forall (u,v)\in E\) ,恰好有一个端点在 \(A\) 中,则 \(G\) 是以 \(A\)\(V \backslash A\) 构成两部的二分图, 因此若 \(G\) 不是二分图则无解。

\(G\) 是二分图时,考虑每个连通分量的独立性,对每个连通分量考虑,将其黑白染色后取点数较小的一边加入 \(A\) 中,点数较多的一边加入 \(V \backslash A\) 中。

复杂度 \(O(|E|)\)

P1525 [NOIP2010 提高组] 关押罪犯

\(N\) 个罪犯和两个监狱,罪犯间有形如 \(M\) 对形如 \((u,v,w)\) 的关系表示 \(u,v\) 两名罪犯若在同一监狱则会发生影响力为 \(w\) 的冲突事件,求合理安排每个罪犯到某个监狱后,影响力最大的事件的最小值。

双最值问题,考虑定义域 \(I\) 和最优评分 \(S\),对于 \(\forall x<S\) ,一定不合法,对于 \(\forall x>S\) 一定合法,(\(P(x)=[是否存在一组方案使最大事件的影响力 \le x]\)) ,因为至少存在一个合法方案(当 \(x=S\)),满足 01 单调。

二分答案 \(W\) ,考虑所有关系 \((u,v,w)\) ,建立 \(V=\{1,2,...,N\},E=\{(u,v,w)|w>W\}\) 的无向图 \(G\) ,则 \(\forall (u,v,w)\in E\) ,都有 \(u\in A,v\in B\) 。即 \(E\) 中的每个关系的两个罪犯必须在不同监狱中,这是经典的二染色问题(等价于二分图),于是所有冲突事件的影响力 \(\le W\) 等价于 \(G\) 是二分图,判定之。

二分图的匹配

定义一个二分图 \(G=(V,E)\)匹配(matching) 是一个边集 \(M\),匹配又称 边独立集(edge independent set),满足 \(M\subseteq E\)\(M\) 中的任意两条边没有公共端点,\(M\) 中的边又称 \(G\)匹配边, 剩余的边称为 非匹配边 。在匹配边的端点叫做 匹配点(matched vertex),又称饱和点。反之则称为 非匹配点,又称非饱和点。

一个点 \(u\) 被匹配当且仅当 \(M\) 存在一条边以 \(u\) 为端点。

最大匹配(maxinum matching):具有最多边的匹配 \(M\)

匹配数(matching number): 最大匹配的大小 \(|M|\)

完美匹配(perfect matching):\(|X|=|Y|\) 且匹配了所有点的匹配

完备匹配(complete matching):匹配了二分图的较小集合的所有点的匹配

交错路(alternating path):图的一条简单路径,满足任意相邻的两条边,一条在匹配内,一条不在匹配内。

增广路(augmenting path): 是一个始点和终点都为非匹配点的交错路。

根据定义可以立马得出一些性质:对于匹配 \(M\),左部点和右部点的匹配点个数均为 \(|M|\)

用自然语言描述,增广路 就是从一个没有被匹配的点出发,依次走非匹配边、匹配边、非匹配边……最后通过一条非匹配边到达 另外一部点 当中某个 非匹配点 的路径。因此,不妨钦定增广路的方向为从左部端点到右部端点。

如图,红色边是匹配边 \(M=\{(p1,p2),(p3,p4)\}\) 。我们从非匹配左部点 \(p_0\) 开始,一次走蓝边、红边、蓝边……,最后到达非匹配点 \(p_5\) 。这些边连接而成的路径就是一条增广路。

img

网络流上增广路 的形态为从 \(S\) 开始,到某个未被匹配的左部点,然后再左右部点之间反复横跳,最后到达某个未被匹配的右部点,并走到 \(T\)。如果一个点被匹配,那么在残量网络上它和 \(S\)\(T\) 不连通,因此路径上和 \(S,T\) 相连的都是非匹配点,对应的增广路的开头和结尾都是非匹配点。忽略掉 \(S\)\(T\) ,路径的第一条和最后一条都是从左部点走向右部点,对应增广路的第一条最后一条边都是非匹配边。

由增广路的定义推出下述三个结论:

(1)对于增广路 \(P\),起点和终点都是非匹配点,路径上的点都是匹配点。

(2)增广路 \(P\) 的长度必定为奇数,第一条边和最后一条边都不属于 \(M\) ,因为两个端点分属两个集合,且未匹配。

(3)\(P\) 经过将非匹配边和匹配边交换操作可以得到一个更大的匹配 \(M'\)。这个互换操作我们称为 路径取反

(4)\(M\)\(G\) 的最大匹配当且仅当 \(G\) 不存在相对于 \(M\) 的增广路

最大匹配是互相的,如果我们给 \(X\) 找到了最多的 \(Y\) 中的对应点,同样,\(Y\) 中也不可能有更多的点得到匹配了。

由上述性质我们可以设计一个增广路算法,不断扫描二分图中的点集,直到找不到增广路为止,该算法就是 匈牙利算法

\(P\) 是一个匹配,其中的边称为匹配边,匹配边的端点称为匹配点,如果一条路径从一个 未匹配的左部点 出发到达一个 未匹配的右部点交替经过 不在 \(P\) 中的边和在 \(P\) 中的边,则称该路径为一条 增广路

匈牙利算法依赖一个重要 结论:\(P\) 是二分图的最大匹配,当且仅当图中不存在增广路。

  • 必要性容易证明:若存在增广路,则将增广路上全体非匹配边改为匹配边,匹配边改为非匹配边,就得到了一组更大的匹配。

  • 充分性证明:若 \(P\) 不是最大匹配,设最大匹配为 \(P'\) ,显然存在左部点 \(x\) 不在 \(P\) 中而在 \(P'\) 中,假设在 \(P'\) 中它匹配了 \(y\) ,若 \(y\)\(P\) 中是非匹配点,那么我们找到了一条增广路 \(x \to y\) ,否则对 \(y\)\(P\) 中匹配的点重复上述过程,则我们得到了一条增广路,或者得到了一个增广路删去一条边的结构,将其中所有边取反,可以将 \(P’\) 调整为一个和 \(P\) 更为 “接近” 的最大匹配,不断执行此过程。特殊地,若一直未找到增广路,则最后 \(P'=P\),说明 \(P\) 是最大匹配,矛盾。

上面充分性的证明已经部分蕴含了匈牙利算法的内容:

  • 枚举 \(i=1,...,n\) ,其中 $n $ 为左部点的数量,时刻维护左部点当前前缀 \(1,...,i-1\) 与全部右部点的一组最大匹配 \(P\),考虑将 \(i\) 加入:
  • \(i\) 为端点进行 DFS 寻找增广路,假设寻找以左部点 \(x\) 开始的增广路(递归的思想),首先枚举 \(x\) 的邻居 \(y\) ,若 \(y\) 是非匹配点或者从 \(y\) 匹配的点出发存在增广路,那么找到了一条以 \(x\) 开始的增广路(对于前者,\(x\to y\) ,对于后者,\(x\to y\to z\to ...\));
  • 如果找到了一条增广路,答案加 1
  • 为了控制算法复杂度,在对于每个 \(i\) 寻找增广路时,如果从一个 \(x\) 出发寻找失败,下次就可以直接返回,如果从一个 \(y\) 的匹配点出发时寻找失败,下次也不必检查 \(y\)

匈牙利算法基于 贪心 原则:一旦一个点进入匹配,就不会重新成为非匹配点。因此当找不到增广路时表示 \(i\) 在保持 \(1,...,i-1\) 的匹配情况不变时一定无法加入最大匹配中,且左部点 \(i\) 不会在枚举完 \(i\) 之后被枚举第二次, \(i\) 不会因此增加一条增广路了, 原因很简单,\(i\) 的所有邻居 \(j\) 都不能找到一条增广路。于是我们可断言,当前状态一定为某一个左部点为 \(1,...,i\) 时的最大匹配。

由此,我们知道:若匹配的左部点记为 \(1\),未匹配的左部点记为 \(0\) ,则按照枚举顺序拼接左部点的匹配情况,匈牙利算法求出的最大匹配的字典序是最大的。

正确性证明:匈牙利算法按照索引大小尽可能的找增广路,对于左部点 \(i\) ,若找到了一条增广路,就立刻进行路径取反操作,加入到匹配中。且状态 \(S=0/1\)(该点是否是匹配点)在枚举完该点之后不会发生改变。

具体实现方法如下:

从二分图的 \(X\) 部取出一个未匹配的顶点 \(u\) 开始,找到一个未被访问的点 \(v\)\(v\) 一定属于 \(Y\) 部的顶点)。对于 \(v\) ,分两种情况讨论:

(1)如果 \(v\) 是未匹配点,则已经找出了一条增广路

(2)如果 \(v\) 是匹配点,取出 \(v\) 的匹配顶点 \(w\)\(w\) 一定是 \(X\) 部的顶点),边 \((w,v)\) 此时是匹配边,根据 路径取反 ,如果我们能从 \(w\) 找到一条增广路 \(P'\),那么 \(u\rightarrow v\rightarrow P'\) 就是一条增广路,将 \((w,v)\) 改成未匹配,\((u,v)\) 改成匹配就行了。

(3)如果 \(v\) 找不到一条增广路,那么尝试找下一个点。

https://blog.csdn.net/u013384984/article/details/90718287

https://blog.csdn.net/lemonxiaoxiao/article/details/108672039

二分图匹配模型有两个要素:

  1. 节点能分成独立的两个集合,每个集合内部有 \(0\) 条边,简称为 \(0\) 要素
  2. 每个节点只能与一 \(1\) 条匹配边相连,简称为 \(1\) 要素

在把实际问题抽象成二分图匹配模型时,我们就要寻找这种 \(0,1\) 性质的对象。

Acwing372 棋盘覆盖

给定 \(n\times m\) 的棋盘,其中某些格子禁止放置,求最多可以放多少块 \(2\times 1\) 的骨牌,不能重叠。

\(n,m\le 100\)

一个相似的经典问题是:给定 \(N\times M\) 的棋盘,求用骨牌铺满的方案数。可以状压 DP 解决(轮廓线 DP)。

在本题中,骨牌不能重叠,也就是说每个点只能被至多一个骨牌覆盖,且一个骨牌会覆盖两个四连通的(相邻)格点。因此,黑白染色,保证任意两个相邻的节点颜色不同(0要素),在相邻格子对应节点间连边(把骨牌作为无向边)(1要素)。\(O(n^2m^2)\)

Acwing373 车的放置

给定 \(N\times M\) 棋盘,某些格子禁止放置,问棋盘上最多能放上多少个不能互相攻击的 “车”。

\(N,M\le 200\)

题目告诉我们,一行或一列至多放置一个车,且在坐标 \((x,y)\) 放置了一个车后 \(x\)\(y\) 都不能再放车了。

于是我们把行、列看成点,一共 \(N+M\) 个点,如果在 $(x,y) $ 没有禁止,那么就在 \(x\)\(y\) 之间连一条边。求最大匹配即可。

一个车显然不能放在两行,于是0要素满足。\(O((N+M)MN)\)

P1963 [NOI2009] 变换序列

给定序列 \(D_{1...n}\),求字典序最小的排列 \(T_{1,...,n}\) ,使得 \(\min(|T_i-i|,n-|T_i-i|)=D_{i,T_i}\)

每个 \(i\) 只会向 \(i+D_i,i-D_i,i+n-D_i,i-n+D_i\) 这四个点连边,由于 \(i+D_i,i+D_i-n\) 只会有一个在范围内,另外两个同理,所以一个 \(i\) 只会向两个点连边。

由于映射 \(i \to P_i\) 的距离 \(D\) 计算方式比较奇怪,我们不妨将 \(1-n\) 放在首尾相接的环上考虑,\(|T_i-i|\)\(T_i\)\(i\) 路径上的边数,于是 \(D_{x,y}\) 的含义就变成了 \(x\)\(y\) 环上距离的最小值,我们将将映射当成边 \(i\to i+D_i,i-D_i\) ,问题变成了求 \(T_i\) 字典序最小的完美匹配。

如果不能完美匹配,直接无解。因为要求字典序最小,我们考虑新匹配一个的影响。首先肯定要从小到大枚举它的右部点邻居(字典序最小),如果有没匹配的,直接匹配。否则必须去抢一个匹配点。这样只能保证这个点的字典序一定最优,不能保证抢的点以及增广路的其它点最优。而一个序列的字典序是从前往后比较的,需要保证前面的最优。倒着从 \(n\to 1\) 匹配。

这个做法是根据本题的特殊性质保证的:每个左部点只会连向恰好两个右部点。一般二分图字典序最小完美匹配是不可以这么做的,比如下图:

img

推广之,虽然倒着匹配可以保证当前 \(T\) 最小,但因为有后效性,后面增广路可能会交叉,导致顺序被打乱。

这是个棘手的问题,不过左部点 \(i\) 向右部点恰好连出了 \(2\) 条边。对于右部点度为 \(1\) 的点,匹配方案就已经确定了,将两个匹配点及其边删除,直到原图的各个连通分量所有点的度为 \(2\) ,此时是一个环,环上路径的点左部和右部交替出现。一个环的完美匹配数量恰好为 \(2\) ,选出字典序较小的一个。

在倒着匹配的过程中,对于第 \(i\) 个点,确定该点的匹配点之后,该点的两个邻居的另外两个左部点的匹配也确定了,一直循环下去,环中所有剩余的匹配也都确定了。尽管有后效性(该点的需选择会影响到 \(i\to n\) 的字典序),但后面的过程只有一种选择,就一定可以保证字典序最小了。

一般二分图完美匹配,求最小字典序的通解 是什么呢?

算法 1

枚举全排列,判定是否是完美匹配,需要判断每一条边是否是原图的边。\(O(n!mlog)\) ,对每个点开个 \(map\) 记录 \((i,j)\) 是否右边即可。

算法 2

注意到匈牙利算法的局限性在于增广路会修改以前的方案。从前往后枚举,对于每个点,从小到大枚举直接相连的右部点,如果删掉这两点一边,剩下的子图仍构成完美匹配,就说明该点可以形成匹配,直接删掉即可。复杂度 \(O(m^2n)\)

算法 3

在算法 2 中,每次点 \(x\)\(y\) 匹配都要对剩下的图跑一遍二分图最大匹配,实际上大多数点在这次匹配操作后是没有影响的。我们先对整张图跑一遍二分图最大匹配,发现将 \(x\)\(y\) 进行匹配只会影响到两个点:当前与 \(x\) 匹配的点和当前与 \(y\) 匹配的点。由于增广路是从非匹配点开始的,所以每次只需要对有影响的两个点中任意一个点跑一次增广路就行了。\(O(m(n+m))\)

https://byvoid.com/zhs/blog/noi-2009-transform/

二分图最小点覆盖

给定一张二分图,求出一个最小的点集 \(S\),使得图中的任意一边都至少有端点属于 \(S\)。这个问题被称为二分图的 最小点覆盖

König定理

二分图的最小点覆盖包含的点数等于二分图最大匹配包含的边数。

首先,最大匹配是边集的一个子集,且所有边都不相交,所以至少需要从每条匹配边中选出一个端点。因此最小点覆盖的点数不可能小于最大匹配的边数。下面给出构造最小点覆盖的方法:

  1. 求出二分图的一个最大匹配
  2. 对于每个匹配失败(未匹配)的左部点,从该点出发进行一次寻找增广路(交错树),标记访问过的点。
  3. 左部未被标记的点,右部被标记的点,这些点组成了二分图的最小点覆盖。

下面证明该构造的正确性:

  1. 左部非匹配点一定都被标记——因为它们是出发点
  2. 右部非匹配点一定都未被标记——因为交错树上的右部点都是匹配点,所以当非匹配点出现在交错树上意味着找到了一条增广路,与匹配的最大型矛盾。
  3. 一对匹配点要么都被标记,要么都没被标记——因为在寻找增广路的过程中,左部点只能通过右部点到达。

在构造中,每条匹配边恰好取了一个点,所以选出的点数等于最大匹配包含的边数。

再来讨论这种取法是否能覆盖所以边:

  1. 匹配边一定被覆盖——因为恰有一个端点被取走
  2. 不存在连接两个非匹配点的边——否则就有长度为 \(1\) 的增广路了。
  3. 连接左部非匹配点 \(i\) ,右部匹配点 \(j\)——因为 \(i\) 是出发点,所以 \(j\) 一定被访问。
  4. 连接左部匹配点 \(i\) ,右部非匹配点 \(j\)—— \(i\) 一定没有被访问,否则再走到 \(j\) 就形成增广路了。

Acwing376 Machine Schedule

有两台机器 \(A,B\)\(n\) 个任务,每个机器有 \(M\) 种不同的模式。

对于每个任务 \(i\) ,给定两个整数 \(a_i,b_i\) ,表示如果该任务在机器 \(A\) 上运行需要模式为 \(a_i\),在 \(B\) 上的模式为 \(b_i\)任务可以被任意顺序执行,求怎样分配任务并合理安排顺序,使得机器重启次数最小。

\(N,M\le 100\)

二分图最小点覆盖模型特点:2 要素:每条边有两个端点,二者至少选 1

机器重启次数等于该机器的不同模式数,由于每个任务只能在机器 \(A\) 中和机器 \(B\)二选一,不能都不选。在机器 \(A\) 中需要 \(A\) 提供模式 \(a_i\) ,在机器 \(B\) 中需要提供模式 \(b_i\) 。将每个机器的每个模式看成一个点,每个任务看成 $(a_i,b_i) $ 的无向边。对于每一个任务,至少有一个机器要覆盖这条边。

求出它的最小覆盖,就等价于用尽量少的模式执行所有任务。\(O(NM)\)

Hopcroft-Karp 算法

给定二分图 \(G\) ,求出边集 \(M\) 大小的最大值。

尝试用网络流求解问题。一个点最多与一条边相连,即顶点度数 \(\le1\) 。这启发我们从源点 \(S\)\(X\) 每个顶点连容量为 \(1\) 的边,从 \(Y\) 向汇点 \(T\) 连容量为 \(1\) 的边,再加上给 \(E\) 中所有边定向后由 \(X\) 指向 \(Y\) 的容量为 \(1\) 的边。 \(S\rightarrow T\) 的最大流即最大匹配。

容易证明正确性:每个点至多与一个点相邻,因为限制了它到源点或汇点的流量为 \(1\) 。因此,一组可行流与一组匹配一一对应,且流量等于匹配大小。

使用 dinic 求解的话,复杂度为 \(O(m\sqrt n)\)

最大多重匹配

多重匹配是指顶点 \(u\) 至多与 \(L_u\) 条边相连,从中选出尽量多的边。一般匹配即 \(L_u=1\)的特殊情况。

算法 1:

拆点,把左部和有部的第 \(i\) 个点拆成 \(L_i\) 个点,这样每个点是 等效的 。对于原图的边 \((i,j)\),在 \(i\) 拆成的所有点和 \(j\) 拆成的所有点之间连边,求最大匹配。

算法 2:

前提:对于所有右部点均有 \(L_i =1\),即只有一侧是 “多重的”

在匈牙利算法中,对每个左部点 \(i\) 执行 \(L_i\) 次 DFS

算法 3:

此时右部也是 “多重” 的。修改匈牙利算法,让右部点可以匹配 \(L\) 次,超过匹配次数后再依次尝试递归当前匹配的 \(L\) 个点,去寻找增广路。

算法 4:

求解最大多重匹配,只需将 \(S\rightarrow X\) 的每条边 \(S \rightarrow u\) 的容量设为 \(L_u\)\(Y\rightarrow T\) 同理。二分图内部每条边容量不变,仍为 \(1\)。对上述网络求最大流即最大多重匹配。

HK 算法的时间复杂度证明中并没有用到与 \(S,T\) 相连的边容量为 \(1\) 的性质,所以用 HK 求解复杂度仍为 \(O(m\sqrt n)\)

二分图带权最大匹配

二分图的每条边都有权值 \(w(i,j)\) ,求出二分图的最大匹配,使得匹配边的权值和最大。注意前提是匹配数最大,然后再最大化匹配边的权值总和

KM算法

求在满足 “带权最大匹配一定是完美匹配” 的图中正确求解(即 二分图的最大权完美匹配),稠密图效率较高,局限性很强。

交错树

如果对于一个左部点 \(x\) 出发寻找匹配失败,在 DFS 过程中,所有访问的点,以及为了访问这些点而经过的边,构成交错树。

易发现,根是左部点 \(x\),所有叶子也都是左部点(因为最终匹配失败了),树上 \(1,3,5,...\) 层的边都是非匹配边,\(2,4,6,...\) 层的边都是匹配边;除了起点和终点剩下所有在交错树的点都是匹配点。

顶标

全称 顶点标记值,给第 \(i\) 个左部点的权值为 \(A_i\) ,第 \(j\) 个右部点的权值为 \(B_j\) 。同时必须满足 \(\forall i,j,A_i+B_j\ge w(i,j)\),这些数值称为节点的顶标。

相等子图

二分图中所有节点和满足 \(A_i+B_j\ge w(i,j)\) 的边构成的子图

定理: 若相等子图中存在完美匹配,则这个完美匹配就是二分图的带权最大匹配。

证明:在相等子图中,完美匹配边权和为 \(\sum_iA_i+B_i\) ,因为满足那个不等式,所以该匹配的边权和一定不劣于任何一组匹配的边权和。

KM 的基本思想就是:先在满足不等式的前提下,给每个节点赋值一个顶标,然后采取适当的措施不断扩大相等子图的规模,直到存在完美匹配。

相等子图如果不存在完美匹配,记匹配失败的那次 DFS 的交错树为 \(T\)

根据匈牙利,有结论:

  1. 除了根,\(T\) 中左部点都是 从右部点沿匹配边访问到的,即调用 \(dfs(match[y])\)
  2. \(T\) 中所有右部点都是 从左部点沿着非匹配边访问到的

在寻找增广路前,每个右部点沿匹配边到的左部点是固定的,让匹配数增加,只能让左部点沿非匹配边访问更多右部点。

\(T\) 中左部点的 \(A_i \gets A_i -\Delta\) ,右部点的 \(B_i \gets B_i+\Delta\)

对于最小权最大匹配,将最大流算法换成最小费用最大流。

对于最大权最大匹配,将最大流算法换成最大费用最大流。图中无正环,只需权值求反求最小费用最大流。

对于最大权 完美 匹配,用专门的 KM 算法

二分图相关问题

最小点覆盖集

给定二分图 \(G=(V,E)\) ,若点集 \(C\subseteq V\) 满足对于任意 \((u,v)\in E\) 都有 \(u\in C\)\(v\in C\) ,则称 \(C\)\(G\)点覆盖集。即一个点可以覆盖以该点为端点的边,覆盖所有所有边的点集就是点覆盖集。点覆盖集的大小为 \(|C|\)

考虑一组点覆盖集,不存在边 \((u,v)\in E\) 使得 \(u,v\) 同时不属于 \(C\) 。因为一个点只有属于 \(C\) 和不属于 \(C\) 两种状态,这启发我们将其套入集合划分模型。

....... 等学完网络流

并查集

并查集包含两个基本操作 :get,merge

代表元法 : 给每个集合选取一个固定的元素,作为整个集合的代表元

其次,我们需要定义归属关系的表示方法

第一种思路是记 \(f_x\)\(x\) 所在集合的代表元,每次可以快速查询一个点的归属集合

第二种思路是用一个树形结构来存储每一个集合,树上的每个点都是一个元素,树根是集合的代表元

并查集是实质上是维护了一个森林,我们用 \(fa_x\) 记录 \(x\) 的父亲,合并两个集合的时候,只需连接两个集合的树根\((f_{rt1}=rt2)\),即可,对此有两种优化方法

路径压缩

注意到我们只关心每个集合的树根是什么,并不关心树形结构,于是一些树之间其实是等价的

每次进行get操作时,把访问元素的全部祖先指向树根,每次get操作均摊是 \(O(logn)\)

按秩合并

"秩" 有两种定义,一种是树的深度(未经过路径压缩),另一种是集合的大小,我们将秩记录在每个集合的代表元上,每次 merge 时都将秩较小的合并到秩较大的集合上

注意到,当"秩"记录的是集合大小的时候,按秩合并此时与启发式合并等价,这样的话每次合并增加的代价为小的集合的大小,get 操作均摊是 \(O(logn)\)

同时使用路劲压缩和按秩合并,每次 get 均摊下是 \(O(\alpha(n))\) 的,其中 \(\alpha(n) < 5\)

Acwing237

从该题能看到,并查集能在一张无向图上维护结点之间的连通性,这是他的基本用途之一,并查集擅长动态维护具有传递性关系的问题

Acwing145

显然这个是"带限期和罚款的单位时间调度问题",考虑用并查集优化

每次找到 \(d\) 的树根 \(r\) , 若 \(r>0\) , 则安排在 \(r\) 天卖出,将 \(r\)\(r-1\) 所在集合合并

这个并查集实质上是维护了一个数组中位置的占用情况,每一个位置所在集合的代表即为从左数第一个没被占用的位置,当一个位置被占用了,就指向前一个位置,利用路径压缩,就能快速找到往前数第一个空闲的位置

扩展域与带权并查集

并查集实质上维护了一个森林,我们可以在每条边上记录一个权值 \(d_u\) ,代表一个点与父亲之间的某种关系,每次路径压缩时,将访问过的点指向树根,同时修改这些点的 \(d\) 值,就可以通过路径压缩来统计每个结点到树根之间的路径上的一些信息

P1196 银河英雄传说

题目要求维护的是若干条链,用并查集维护,我们用 \(d_x\) 记录 \(x\)\(fa_x\) 之间的边权,路径压缩时除了将 \(x\) 指向树根,还将 \(d_x\) 更新为 \(x\) 到根路径上的边权和,合并时,令 \(fa_x=y,d_x=sz_y\)

查询时找到两个点的 \(d\) 值即可

Acwing239

"程序自动分析"中提到,并查集擅长动态维护变量间具有传递性的关系及连通性,当"传递关系"不止一种时,并且这些传递关系能够相互导出,可以使用扩展域和边带权并查集解决

在这个题目中,原题与动态维护变量间的奇偶关系等价

显然奇偶性具有传递性,记奇偶性相同为 \(0\) ,奇偶性不同为 \(1\)

\(0+0=0,1+1=0,1+0=1\)

方法1 : 边带权并查集

\(d_x = 0/1\) , 代表 \(x\)\(fa_x\) 的奇偶性相同/不同,路径压缩时记录边权异或和,可得到 \(x\) 与根的奇偶性关系(get操作)

merge操作:若 \(x\)\(y\) 在同一个集合中,get(x),get(y) 执行完毕后,以根为中转点可导出 $d_x \text{ xor } d_y $

否则,\(fa_p=q,d_p=d_x \text{ xor } d_y \text{ xor } ans\)

方法2 :扩展域并查集

将每个变量拆成两个点 \(x_{odd},x_{even}\) , 代表 \(x\) 是偶数还是奇数,若奇偶性相同,连 \((x_{odd},y_{odd})(x_{even},y_{even})\) , 否则连 \((x_{odd},y_{even}),(y_{odd},x_{even})\)

边的含义就是这些变量之间的关系能否相互推出,

P2024 食物链

不妨将每个点拆成三个点 \(x_a,x_b,x_c\) 代表他们是哪一类

如果 \(x\)\(y\) 同类,连 \((x_a,y_a),(x_b,y_b),(x_c,y_c)\)

如果 \(x\)\(y\) , 连 \((x_a,y_b),(x_b,y_c),(x_c,y_a)\)

此外,我们还需要判断一句话是否是假话,由于两个变量间的关系式可以传递得到的,判断时只需考虑 \(A\) 域和 \(B\) 域就行了

如果 \(x\)\(y\) 同类,判断 \((x_a,y_b),(x_b,y_a)\) 这两对点是否连通

如果 \(x\)\(y\),判断 \((x_a,y_a),(x_b,y_a)\) 是否连通

其实还可以边带权并查集做,记 \(d_x=\{0,1,2\}\)

不妨回想一下之前做过的边带权和扩展域并查集的实质,他们都是动态维护一些具有传递性的复杂信息,比如(x,y)奇偶性相同,(y,z)奇偶性不同,(x,z)奇偶性不同,A能吃B,B能吃C,C能吃A

这种神秘的传递性看起来很复杂且无从下手,但这两种方法可以很好的解决;边带权并查集是维护了 \(x\)\(fa_x\) 的关系,在路径压缩时将信息合并,得到 \(x\) 与根的关系,从而得到 \(x\) 与他所关联的点(这个集合)之间的信息;扩展域并查集而是将一个点拆成若干个不同的变量取值,在这些"确定"的变量间建立简单的连通关系,从而加以判断,本质与 2-SAT 类似

P1525 关押罪犯

Kruskal重构树

将边权从小到大加入,每次合并两个集合的时候,将这条边看为一个点,边权看为点权,找到这两个集合的代表元,作为左儿子和右儿子,连边形成的有根树即为 kruskal重构树

  • 二叉树,点数是2n-1
  • 原图中的结点对应重构树上的所有叶子
  • 祖先后代之间满足偏序关系,即堆性质(大于等于)
  • 最小生成树上两点之间的最大边权对应重构树上两点lca的点权
  • 重构树中,每个子树都对应着最小生成树的一个连通块,该连通块内所有边都小于等于根的点权
  • 反过来说,从某个点开始,只经过边权小于等于某值能到达的所有点,在个重构树上对应的是一颗子树的所有叶子结点

最后一个性质很好,有了这个性质,可以做很多事情:比如x在原图上经过不超过d的边权所能到达的结点,就是在边权从小到大排序后重构树上,不超过d的最浅的祖先子树内的所有叶子,这个祖先可以倍增找到,于是: kruskal重构树常与倍增相结合

P4768 [NOI2018] 归程

倍增找到对应重构树上的树根,问题变成了求子树内所有叶子到1号结点的最短路

从1号点反向dijkstra预处理最短路即可

点权重构树

kruskal重构树 还可以限制点权,具体的,如果限制经过点权的最大值,则将每条边 \((u,v)\) 的权值赋为 \(\max(w_u,w_v)\), 同理限制最小值为 \(\min(w_u.w_v)\)

上下界重构树

如果上下界都给出,即既限制经过的最小值又限制最大值,可以拆成两部分考虑,求出按边权从小到大/从大到小的两颗重构树 \(T_{max},T_{min}\) , 找到对应的两颗子树, 记两颗重构树的dfs序为 ${a},{b}, $ 则问题变成了求 \(i \in [l1,r1],j\in [l2,r2], a_i=b_j\) 的个数

稍加处理一下,记 \(c_i\)\(i\) 号点在 \(T_{min}\) 对应的 dfn 值,问题变成了求 \(i\in [l1,r1],c_i\in[l2,r2]\) 的个数,这是经典的二维偏序问题,用主席树或离线树状数组

多叉重构树

注意到重构树是个二叉树这个性质我们几乎没有用到,于是&%&&$&*&*%&

P4899 [IOI2018] werewolf 狼人

上下界重构树板题

欧拉图

欧拉路径 : 从一个点出发,不重不漏的经过图上的每一条边的路径(允许多次经过同一个点)

欧拉回路 : 起点和终点相同给的欧拉路径

欧拉图 : 具有欧拉回路的有向图或无向图

半欧拉图 : 具有欧拉路径但不具有欧拉回路的有向图或无向图

判别是否存在欧拉路径/回路

  • 无向图欧拉回路 : 连通,每个点的度数均为偶数
  • 无向图欧拉路径 : 连通,两个点的度数为奇数,剩下点的度数为偶数,这两个点为起点和终点
  • 有向图欧拉回路 : 强连通,每个点的入度和出度都相同
  • 有向图欧拉路径 : 弱连通,一个点出度比入度大,另一个点入度比出度大1,剩下点的入度和出度相等

证明

必要性显然,因为进入一个点后必须走出去(除起点和终点)

充分性可以考虑构造法,即欧拉路可以看做环套环,我们可以在原图 \(G\) 上找到一个任意回路,将他消去,除孤立点外的所有子图 \(G'\) 依然满足上述要求,这个过程可以递归做下去,且这个回路一定和所有子图都有交点,最后将所有回路合并成一条路径即可

Hierholzer 算法

也叫 "圈套圈算法" ,"逐步插入回路法"

用于求解有向图欧拉回路

思路 : 不断往当前回路的某个点中插入环,与欧拉回路的充分性证明完全等价

一个朴素的实现方法是,每次找到一个回路 P ,将 P 的点 p1,p2.... 加入序列中,加入 \(p_i\) 之前,递归找到 \(p_i\) 所在子图的欧拉回路,将这条回路插进去

双向链表可以做到 \(O(n+m)\) ????

易发现的是每次删去的边一定是一个点的边的一个前缀,因为我们不会因找不到环二而反悔走另一条边

从一个点不断无脑深搜,一定能找到一个回路

于是的话我们只需要链式前向星,与dinic的当前弧优化相似,记录第一个有用的边

当是这个朴素的做法还是太蠢了,我们可以找到回路回溯的过程中对每个点找环.换句话说,将找环的顺序倒过来,就没必要显示的找到环了,而是在回溯的过程中,一边对当前点找环,一边往回路中插入环

具体做法 : 从起点开始,每次遍历还未被删除的边,深搜,遍历完所有出边时将该点压入栈,最后栈顶到栈底的点序列即为一条欧拉路

复杂度 \(O(n+m)\)

无向图欧拉回路类似,只需要把每条边当成两条有向边,但注意这条无向边只能被经过一次,需要用到 "成对变换" 的技巧

对于有向图和无向图欧拉路径,只需要从起点开始 dfs ,其他地方类似

解得字典序极值

如果需要使得欧拉路径字典序最大/最小,需要将每个点的邻边排序,带个log

P7771 【模板】欧拉路径

字典序最小的有向图欧拉路径

P2731 [USACO3.3]骑马修栅栏

无向图欧拉路径

P1127 词链

注意到每个单词只有首字母和尾字母有意义,可以将单词当成边,字母当成点

有向图欧拉路径,判弱联通的一个方法:dfs完后看每条边是否都被遍历

posted @ 2022-08-16 17:45  hs_wfz_orz  阅读(217)  评论(0编辑  收藏  举报