Algorithm Review 5.1 图论

图论

差分约束

  • 对于 n n n 个变量 x 1 , x 2 , … , x n x_1,x_2,\dots,x_n x1,x2,,xn m m m 个约束条件 x i ≤ x j + c k x_i \le x_j + c_k xixj+ck,我们需要求出一组解或判断出无解。
  • 若求的是 x i x_i xi 的最大值,不难发现 x i ≤ x j + c k x_i \le x_j + c_k xixj+ck 与单源最短路中的三角形不等式 d i s [ y ] ≤ d i s [ x ] + z dis[y] \le dis[x] + z dis[y]dis[x]+z 极其相似,可从点 j j j 向点 i i i 连一条权为 c k c_k ck 的边,无解即存在负环。
  • 若求的是 x i x_i xi 的最小值,可将约束条件变形为 x j ≥ x i − c k x_j \ge x_i - c_k xjxick,与单源最长路中的不等式 d i s [ y ] ≥ d i s [ x ] + z dis[y] \ge dis[x] + z dis[y]dis[x]+z 极其相似,可从点 i i i 向点 j j j 连一条权为 − c k -c_k ck 的边,无解即存在正环。
  • 求判断正环或者负环可通过 SPFA 算法实现,具体若某点被更新的次数超过 n n n 则无解。

强连通分量

  • d f n [ x ] dfn[x] dfn[x] 为结点 x x x 搜索的时间次序。
  • l o w [ x ] low[x] low[x] u u u u u u 的子树(经过最多一条后向边或栈中横叉边)能够回溯到的最早的栈中结点的编号。
  • 由定义可以得出:

l o w [ x ] = min ⁡ low[x] = \min low[x]=min
{ \{ {
d f n [ x ] dfn[x] dfn[x]
l o w [ y ] , ( x , y ) 为树枝边, x 为 y 的父结点 low[y], (x, y) 为树枝边,x 为 y 的父结点 low[y],(x,y)为树枝边,xy的父结点
d f n [ y ] , ( x , y ) 为后向边或指向栈中结点的横叉边 dfn[y], (x, y) 为后向边或指向栈中结点的横叉边 dfn[y],(x,y)为后向边或指向栈中结点的横叉边
} \} }

inline void Tarjan(int x)
{
	dfn[x] = low[x] = ++tis;
	stk[++top] = x;
	ins[x] = true; 
	int y;
	for (arc *e = adj[x]; e; e = e->nxt)
		if (!dfn[y = e->to])
		{
			Tarjan(y);
			CkMin(low[x], low[y]);
		}
		else if (ins[y])
			CkMin(low[x], dfn[y]);
	if (dfn[x] == low[x])
	{
		++C;
		do
		{
			y = stk[top--];
			ins[y] = false;
			col[y] = C;
		}while (y != x);
	}
}

  • 无向图中 l o w [ x ] low[x] low[x] x x x x x x 的子树经过最多一条后向边能够追溯到的树中结点的次序号。
  • 根据定义,有:

l o w [ x ] = min ⁡ low[x] = \min low[x]=min
{ \{ {
d f n [ x ] dfn[x] dfn[x]
d f n [ y ] , ( x , y ) 为后向边 dfn[y], (x,y) 为后向边 dfn[y],(x,y)为后向边
l o w [ y ] , ( x , y ) 为树枝边 low[y], (x,y) 为树枝边 low[y],(x,y)为树枝边
} \} }

  • ( x , y ) (x, y) (x,y) 的判断条件: ( x , y ) (x,y) (x,y) 为树枝边且 d f n [ x ] < l o w [ y ] dfn[x] < low[y] dfn[x]<low[y]
  • 将桥标记后用并查集确定边双连通分量。
  • 结论 对于一个有桥的连通图,求加最少数量的边,使其变为边双连通图。用 Tarjan \text{Tarjan} Tarjan 求出边双,将边双缩为一点,则原图变为一颗树。记这棵无根树中叶子结点的个数为 l e a f leaf leaf,则所加最少边数为 ⌊ l e a f + 1 2 ⌋ \lfloor\frac{leaf + 1}{2}\rfloor 2leaf+1

证明 可归纳证明,每次优先选择路径上有至少两个支链的叶子结点合并。

inline void Tarjan(int x)
{
	dfn[x] = low[x] = ++tis;
	int y;
	for (int e = adj[x]; e; e = nxt[e])
	{
		if (e == (up[x] ^ 1)) //树枝边的反向边要注意判断
			continue;
		if (!dfn[y = to[e]])
		{
			up[y] = e;
			Tarjan(y);
			CkMin(low[x], low[y]);
			if (dfn[x] < low[y])
				bridge[e] = bridge[e ^ 1] = true;
		}
		else 
			CkMin(low[x], dfn[y]);
	}
}

割点

  • l o w [ x ] low[x] low[x] 定义同上。
  • x x x 为割点的判断条件:
  1. x x x 为树根,且 x x x 有多于一个的子树。
  2. x x x 不为树根, ( x , y ) (x,y) (x,y) 为树枝边且 d f n [ x ] ≤ l o w [ y ] dfn[x] \le low[y] dfn[x]low[y]
  • 在求割点的过程中就能顺便求出点双连通分量。
  • 在搜索图时,每找到一条树枝边或后向边(注意实现时后向边的反向边不应加入栈中),就把这条边加入栈中。若某点 x x x 满足 ( x , y ) (x,y) (x,y) 为树枝边且 d f n [ x ] ≤ l o w [ y ] dfn[x] \le low[y] dfn[x]low[y] ,把边从栈顶一个个取出,直到遇到了边 ( x , y ) (x, y) (x,y),取出的这些边与其相连的点,组成一个点双连通分量。
  • 与求割点不同,求点双时并不需要判断树根,方便将所有点双取出。

典例 POJ2942

题目大意

  • 给定 n n n 个骑士和 m m m 对厌恶关系,一个骑士能够不被驱逐当且仅当存在包含他的奇数个骑士,使得他们围坐一桌时任意的相邻两个骑士均不存在厌恶关系。
  • 求必须被驱逐的骑士数量, n ≤ 1 0 3 , m ≤ 1 0 6 n \le 10^3, m\le 10^6 n103,m106

解法

  • 以下的奇环和偶环均指简单环。
  • 建出原图的补图,则一名骑士能够参加会议当且仅当他在图中的一个奇环上。
  • 易知奇环一定只在某个点双内部。
  • 结论 若某个点双内存在奇环,则对于该点双内所有点,都存在某个奇环,使得该点在该奇环上。

证明 若某点在偶环上,则一定存在一个偶环与已知的奇环有公共边,则可将偶环和已知的奇环合并得到一个新的奇环。

  • 用二分图染色判断每个点双中是否存在奇环即可。
  • 完整代码:
#include 
#include 

template <class T>
inline void read(T &res)
{
	char ch;
	while (ch = getchar(), !isdigit(ch));
	res = ch ^ 48;
	while (ch = getchar(), isdigit(ch))
		res = res * 10 + ch - 48;
}

template <class T>
inline void CkMin(T &x, T y) {x > y ? x = y : 0;}

const int N = 1e3 + 5;
const int M = 2e6 + 5;

int fa[N], col[N], dfn[N], low[N], stkx[M], stky[M];
int tis, n, m, top;
bool edge[N][N], inc[N], ans[N];

struct arc
{	
	int to;
	arc *nxt;
}p[M], *adj[N], *T = p;

inline void linkArc(int x, int y)
{
	(++T)->nxt = adj[x]; adj[x] = T; T->to = y;
	(++T)->nxt = adj[y]; adj[y] = T; T->to = x;
}

inline bool dfsColoring(int x)
{
	for (arc *e = adj[x]; e; e = e->nxt)
	{
		int y = e->to;
		if (col[y] == -1)
		{
			col[y] = col[x] ^ 1;
			if (!dfsColoring(y))
				return false;
		}
		else if (col[y] == col[x])
			return false;
	}
	return true;
}

inline void solvePBC(int x, int y)
{
	T = p;
	for (int i = 1; i <= n; ++i)
		adj[i] = NULL;
	int u, v;
	do
	{
		u = stkx[top], v = stky[top];
		linkArc(u, v);
		inc[u] = inc[v] = true;
		--top;
	}while (x != u || y != v);

	for (int i = 1; i <= n; ++i)
		col[i] = -1;
	col[x] = 0;
	
	if (!dfsColoring(x))
	{
		for (int i = 1; i <= n; ++i)
			if (inc[i])
				ans[i] = true;
	}
	
	for (int i = 1; i <= n; ++i)
		inc[i] = false;
}

inline void Tarjan(int x)
{
	dfn[x] = low[x] = ++tis;
	for (int y = 1; y <= n; ++y)
	{
		if (y == fa[x] || !edge[x][y])
			continue;
		if (dfn[y] < dfn[x]) // 包含条件 !dfn[y]
		{
			++top;
			stkx[top] = x;
			stky[top] = y;
		}
		if (!dfn[y])
		{
			fa[y] = x;		
			Tarjan(y);
			CkMin(low[x], low[y]);
			if (dfn[x] <= low[y])
				solvePBC(x, y);
		}
		else
			CkMin(low[x], dfn[y]);
	}
}

int main()
{
	while (1)
	{
		read(n); read(m);
		if (!n && !m)
			break ;
		for (int i = 1; i <= n; ++i)
			ans[i] = false;
		for (int i = 1; i <= n; ++i)
			for (int j = 1; j <= n; ++j)
				edge[i][j] = i == j ? false : true;
		for (int i = 1, x, y; i <= m; ++i)
		{
			read(x); read(y);
			edge[x][y] = edge[y][x] = false;
		}
		tis = top = 0;
		for (int i = 1; i <= n; ++i)
			dfn[i] = low[i] = fa[i] = 0;
		for (int i = 1; i <= n; ++i)
			if (!dfn[i])
				Tarjan(i);
		int fans = 0;
		for (int i = 1; i <= n; ++i)
			fans += !ans[i];
		printf("%d\n", fans);
	}	
	return 0;
}

圆方树

  • 对每个点双连通分量建一个新点,每个点向其所属的点双连边,形成树结构。
  • 具体应用待补充。

2-SAT

  • 2-SAT \text{2-SAT} 2-SAT 问题指的是解下列形式的布尔方程:
    ( a ∨ b ) ∧ ( c ∨ d ) ∧ ( e ∨ f ) ∧ … (a\vee b)\wedge(c\vee d)\wedge(e\vee f)\wedge\dots (ab)(cd)(ef)
  • 其中 a , b , c , … a,b,c,\dots a,b,c, 称为文字,是一个布尔变量或其否定。
  • 利用 ⇒ \Rightarrow (蕴含) 将每个子句 ( a ∨ b ) (a\vee b) (ab) 写成等价形式 ( ¬ a ⇒ b ∧ ¬ b ⇒ a ) (\neg a \Rightarrow b \wedge \neg b \Rightarrow a) (¬ab¬ba),对每个布尔变量 x x x 构造两个顶点 x x x ¬ x \neg x ¬x,以 ⇒ \Rightarrow 关系为边建立有向图。
  • 若存在 x x x ¬ x \neg x ¬x 在同一强连通分量内,则无解。
  • 对强连通分量缩点后的图求拓扑序,则若 x x x 所在的强连通分量的拓扑序在 ¬ x \neg x ¬x 所在的强连通分量的拓扑序之后,则 x x x 为真,否则 ¬ x \neg x ¬x 为真。
  • 强连通分量的编号即为逆拓扑序。
  • 常见的等价形式转换:
    • ( x = 1 ) ⇔ ( ¬ x ⇒ x ) , ( x = 0 ) ⇔ ( x ⇒ ¬ x ) (x = 1) \Leftrightarrow (\neg x \Rightarrow x), (x = 0) \Leftrightarrow (x \Rightarrow \neg x) (x=1)(¬xx),(x=0)(x¬x)
    • ¬ ( a ∧ b ) ⇔ ( a ⇒ ¬ b ∧ b ⇒ ¬ a ) \neg(a\wedge b) \Leftrightarrow (a \Rightarrow \neg b \wedge b \Rightarrow \neg a) ¬(ab)(a¬bb¬a)
    • k k k 个点中至多选一个,令这 k k k 个点分别为 a 1 , a 2 , … a k a_1, a_2, \dots a_k a1,a2,ak,新建 2 k 2k 2k 个点, p r e i pre_i prei 表示 [ 1 , i ] [1, i] [1,i] 均不选, s u f i suf_i sufi 表示 [ i , k ] [i, k] [i,k] 均不选,作如下连边:
      • a i ⇒ p r e i − 1 , a i ⇒ s u f i + 1 a_i \Rightarrow pre_{i-1}, a_i \Rightarrow suf_{i + 1} aiprei1,aisufi+1
      • p r e i ⇒ p r e i − 1 , p r e i ⇒ ¬ a i pre_i \Rightarrow pre_{i - 1}, pre_i \Rightarrow \neg a_{i} preiprei1,prei¬ai
      • s u f i ⇒ s u f i + 1 , s u f i ⇒ ¬ a i suf_i \Rightarrow suf_{i + 1}, suf_i \Rightarrow \neg a_{i} sufisufi+1,sufi¬ai
  • 上述算法只适用于判断可行性并给出一种可行方案。

欧拉回路

  • 设图 G = ( V , E ) G = (V,E) G=(V,E)
  • 欧拉回路/路径 G G G 中经过每条边一次并且仅一次的回路/路径。
  • 欧拉图 存在欧拉回路的图。
  • 半欧拉图 存在欧拉路径但不存在欧拉回路的图。
  • 基图 忽略有向图所有边的方向,得到的无向图。
  • 定理1 无向图 G G G 为欧拉图,当且仅当 G G G 为连通图且所有顶点的度为偶数。
  • 定理2 无向图 G G G 为半欧拉图,当且仅当 G G G 为连通图且除了两个顶点的度为奇数之外,
    其它所有顶点的度为偶数。
  • 定理3 有向图 G G G 为欧拉图,当且仅当 G G G 的基图连通,且所有顶点的入度等于出度。
  • 定理4 有向图 G G G 为半欧拉图,当且仅当 G G G 的基图连通,且存在顶点 x x x 的入度比出度
    大 1、 y y y 的入度比出度小 1,其它所有顶点的入度等于出度。
  • 求欧拉图 G G G 的欧拉回路:
inline void findCircuit(int x)
{
	for (int &e = adj[x]; e; e = nxt[e])
		if (!vis[e])
		{
			int c = e;
			vis[c] = true; 
			if (type & 1) // type = 1 为无向图,type = 0 为有向图
				vis[c ^ 1] = true;
			findCircuit(to[c]);
			stk[++top] = c;
		}
}

  • 若题目要求用简单环覆盖图中所有边,则先求出欧拉回路,将欧拉回路上的边依次入栈,一旦入栈过程中发现有重点,则不断弹栈至重点处,则弹栈取出的所有边组成一个简单环,最终即可得到一个简单环的覆盖方案。

哈密尔顿回路

  • 在无向图 G = < V , E > G = G=<V,E> 中,经过每个结点恰好一次的路径/称为 哈密尔顿路径/回路,存在哈密尔顿回路的图称为哈密尔顿图
  • 现在我们尝试给出哈密尔顿图的充分条件/必要条件。
  • 必要条件 ω ( G ) \omega(G) ω(G) 表示 G G G 的连通块个数,则若无向图 G = < V , E > G = G=<V,E> 是哈密尔顿图, 则 ∀ S ⊂ V ∧ S ≠ ∅ \forall S \sub V \land S\not = \varnothing SVS= ω ( G − S ) ≤ ∣ S ∣ \omega(G - S) \le |S| ω(GS)S

证明 对于回路 C C C,容易验证 ω ( C − S ) ≤ ∣ S ∣ \omega(C - S)\le |S| ω(CS)S,故
ω ( G − S ) ≤ ω ( C − S ) ≤ ∣ S ∣ \omega(G-S)\le \omega(C-S)\le|S| ω(GS)ω(CS)S

  • 充分条件 若简单无向图 G G G 每一对顶点的度数之和均大于等于顶点数 n n n,则 G G G 是哈密尔顿图。
    • 推论 G G G 每个顶点的度数均大于等于 n 2 \dfrac{n}{2} 2n,则 G G G 是哈密尔顿图。

证明 G G G 符合该条件且不是哈密尔顿图,通过给 G G G 加边可以得到一个边数最大且不是哈密尔顿图的图 G ′ G' G。假设给 G ′ G' G 中不相邻的两点 v 1 , v n v_1,v_n v1,vn 加边后会得到一个哈密尔顿图,则 G ′ G' G v 1 v_1 v1 v n v_n vn 的路径 v 1 , v 2 , … , v n v_1,v_2,\dots,v_n v1,v2,,vn 中必存在两个点 v i , v i + 1 v_i,v_{i+1} vi,vi+1,使得 v 1 v_1 v1 v i + 1 v_{i + 1} vi+1 相邻, v i v_i vi v n v_n vn 相邻,否则若 v 1 v_1 v1 v i 1 , v i 2 , … , v i k v_{i_1},v_{i_2},\dots,v_{i_k} vi1,vi2,,vik 相邻, v n v_n vn v i 1 − 1 , v i 2 − 1 , … , v i k − 1 v_{i_1-1},v_{i_2-1},\dots,v_{i_k-1} vi11,vi21,,vik1 均不相邻,则 d e g ( v n ) + d e g ( v 1 ) ≤ n − 1 deg(v_n)+deg(v_1)\le n - 1 deg(vn)+deg(v1)n1,与题设条件矛盾。则可以找到一条哈密顿回路 v 1 , v i + 1 , v i + 2 , … , v n , v i , v i − 1 , … , v 1 v_1,v_{i+1},v_{i+2},\dots,v_n,v_i,v_{i - 1},\dots,v_1 v1,vi+1,vi+2,,vn,vi,vi1,,v1,与 G ′ G' G 的前提条件矛盾,故 G G G 不存在。

  • 设简单无向图 G = < V , E > G = G=<V,E> 的顶点数为 n n n,将图中所有点的度数从小到大排序可得到度数序列 [ d 1 , d 2 , … , d n ] [d_1,d_2,\dots,d_n] [d1,d2,,dn]。若所有产生度数序列 [ d 1 , d 2 , … , d n ] [d_1,d_2,\dots,d_n] [d1,d2,,dn] 的图 G G G 都是哈密尔顿图,则称该序列为哈密尔顿序列。

  • 若能判定一个度数序列是哈密尔顿序列,则可以判定所有相关的图都是哈密尔顿图。

  • Chvátal 定理 ∀ i < n 2 , d i ≤ i ⇒ d n − i ≥ n − i \forall i < \dfrac{n}{2}, d_i \le i \Rightarrow d_{n-i} \ge n - i i<2n,diidnini,度数序列 [ d 1 , d 2 , … , d n ] [d_1,d_2,\dots,d_n] [d1,d2,,dn] 是哈密尔顿序列。

证明 G G G 符合该条件且不是哈密尔顿图,通过给 G G G 加边可以得到一个边数最大且不是哈密尔顿图的图 G ′ G' G,使得 G ′ G' G 存在两个不相邻的点 v 1 , v n v_1,v_n v1,vn,满足 d ( v 1 ) + d ( v n ) d(v_1)+d(v_n) d(v1)+d(vn) 尽可能大且 d ( v 1 ) ≤ d ( v n ) d(v_1)\le d(v_n) d(v1)d(vn),且增加边 ( v 1 , v n ) (v_1,v_n) (v1,vn) 后会得到一个哈密尔顿图,设 v 1 v_1 v1 v n v_n vn 的路径为 v 1 , v 2 , … , v n v_1, v_2, \dots, v_n v1,v2,,vn

A = { i ∣ ( v 1 , v i + 1 ) ∈ G ′ } , B = { i ∣ ( v i , v n ) ∈ G ’ } A = \{i|(v_1,v_{i+1})\in G'\},B=\{i|(v_i,v_n)\in G’\} A={i(v1,vi+1)G},B={i(vi,vn)G},若 A , B A,B A,B 有交,则存在 v 1 v_1 v1 v i + 1 v_{i+ 1} vi+1 相邻, v i v_i vi v n v_n vn 相邻,由 充分条件 的证明可构造出哈密尔顿回路,故 A , B A,B A,B 无交集且 d e g ( v 1 ) + d e g ( v n ) < n deg(v_1)+deg(v_n) < n deg(v1)+deg(vn)<n,进一步有 d e g ( v 1 ) = ∣ A ∣ < n 2 deg(v_1)=|A|<\dfrac{n}{2} deg(v1)=A<2n

注意到 v 1 v_1 v1 是不与 v n v_n vn 相邻的度数最大的点,且由 A , B A,B A,B 无交集, v n v_n vn 至少与所有 v i ( i ∈ A ) v_i(i\in A) vi(iA) 不相邻,故至少有 ∣ A ∣ |A| A 个度数至少为 ∣ A ∣ |A| A 的点,由题设条件可知至少有 ∣ A ∣ + 1 |A|+1 A+1 个度数至少为 n − ∣ A ∣ n - |A| nA 的点,由鸽巢原理其中至少有一个点与 v 1 v_1 v1 不相邻,但两者的度数之和为 n n n,与 d ( v 1 ) + d ( v n ) d(v_1)+d(v_n) d(v1)+d(vn) 是能取到的最大值矛盾,故图 G G G 不存在。

Prüfer 序列

  • 对树建立 Prüfer 序列
    • 每次选择一个编号最小的叶子结点删除,在序列中记录它连接的那个结点。
    • 重复 n − 2 n - 2 n2 次直至剩下两个结点,算法结束。
    • 线性实现上述过程只需用指针 p p p 记录当前编号最小的叶子结点,若删点后产生的新的叶子结点比 p p p 小则继续删除这个叶子结点不产生新的叶子结点或产生的叶子结点比 p p p 大。
  • Prüfer 序列的性质
    • 构造完 Prüfer 序列原树剩下的两个结点之一一定是编号最大的结点 n n n
    • 每个结点在序列中出现的次数是其度数减一,没有出现的就是叶子结点。
  • 用 Prüfer 序列重建树
    • 由 Prüfer 序列的性质还原出每个点的度数。
    • 依次枚举 Prüfer 序列上的点,选择一个度数为 1 且编号最小的结点与之连接,同时将两者的度数减一。
    • 重复 n − 2 n - 2 n2 次后只剩下两个度数为 1 的点,将它们建立连接,算法结束。
    • 线性实现上述过程同样是用指针 p p p 记录度数为 1 且编号最小的结点,具体做法类似。
inline void TreeToPrufer()
{
	for (int i = 1, x; i < n; ++i)
	{
		read(fa[i]);
		++deg[fa[i]];
	}
    // 这里的 fa[i] 指以 n 为根时结点 i 的父结点
	int p = 1, x = 0;
	for (int i = 1; i <= n - 2; ++i)
		if (x && x < p)
		{
			ans[i] = fa[x]; 
			x = !--deg[fa[x]] ? fa[x] : 0;
		}
		else
		{
			while (deg[p])
				++p;
			ans[i] = fa[p];
			x = !--deg[fa[p]] ? fa[p] : 0;
			++p;
		}
}

inline void PruferToTree()
{
	for (int i = 1; i <= n - 2; ++i)
	{
		read(ans[i]);
		++deg[ans[i]];
	}
	int p = 1, x = 0;
	for (int i = 1; i <= n - 2; ++i)
		if (x && x < p)
		{
			fa[x] = ans[i];
			x = !--deg[ans[i]] ? ans[i] : 0;
		}
		else
		{
			while (deg[p])
				++p;
			fa[p] = ans[i];
			x = !--deg[ans[i]] ? ans[i] : 0;
			++p;
		}
	for (int i = 1; i < n; ++i)
		if (!fa[i])
		{
			fa[i] = n;
			break ;
		}
}
  • Cayley 公式 完全图 K n K_n Kn n n − 2 n^{n-2} nn2 棵生成树。

证明 由构造和还原过程可知,任意一个长度为 n − 2 n - 2 n2、值域为 [ 1 , n ] [1,n] [1,n] 的整数序列都可以通过 Prüfer 序列双射对应一个生成树。

  • 结论1 n n n 个结点有标号有根树的数量为 n n − 1 n^{n - 1} nn1
  • 结论2 n n n 个结点的度数依次为 d 1 , d 2 , … , d n d_1,d_2,\dots,d_n d1,d2,,dn 的无根树的数量为 ( n − 2 ) ! ∏ i = 1 n ( d i − 1 ) ! \frac{(n - 2)!}{\prod\limits_{i = 1}^{n}(d_i - 1)!} i=1n(di1)!(n2)!
  • 结论3 n n n 个点划分为 k k k 个连通块,已知第 i i i 个连通块的内部连边情况和大小 a i a_i ai,包含所有连通块的生成树数量为 n k − 2 ∏ i = 1 k a i n^{k - 2}\prod\limits_{i = 1}^{k}a_i nk2i=1kai

证明 设将第 i i i 个连通块作为一个整体时的度数为 d i ( d i ≥ 0 ) d_i(d_i \ge 0) di(di0),先不考虑由具体哪个内部结点连边,总方案数为:
∑ ∑ i = 1 k d i = 2 k − 2 ( k − 2 d 1 − 1   d 2 − 1   …   d k − 1 ) ∏ i = 1 k a i d i \sum \limits_{\sum \limits_{i = 1}^{k}d_i = 2k - 2} \binom{k - 2}{d_1 - 1\ d_2 - 1\ \dots\ d_{k} - 1}\prod\limits_{i = 1}^ka_i^{d_i} i=1kdi=2k2(d11 d21  dk1k2)i=1kaidi
e i = d i − 1 e_i = d_i - 1 ei=di1,通过多元二项式定理进行代换,得到:
∑ ∑ i = 1 k e i = k − 2 ( k − 2 e 1   e 2   …   e k ) ∏ i = 1 k a i e i + 1 = ( ∑ i = 1 k a i ) k − 2 ∑ i = 1 k a i = n k − 2 ∑ i = 1 k a i \sum \limits_{\sum \limits_{i = 1}^{k}e_i = k - 2} \binom{k - 2}{e_1\ e_2\ \dots\ e_{k}}\prod\limits_{i = 1}^ka_i^{e_i+1} =\left(\sum \limits_{i = 1}^{k}a_i\right)^{k - 2}\sum \limits_{i = 1}^{k}a_i =n^{k - 2}\sum \limits_{i = 1}^{k}a_i i=1kei=k2(e1 e2  ekk2)i=1kaiei+1=(i=1kai)k2i=1kai=nk2i=1kai

Boruvka 算法

  • Boruvka \text{Boruvka} Boruvka 算法是一种古老的求解最小生成树的算法。
  • 初始时视 n n n 个点为 n n n 个连通块,每次遍历所有点和边,连接一个连通块中和其它连通块相连的最小的一条边,直到合并成一个连通块。
  • 每次连通块个数至少减少一半,可用并查集实现,时间复杂度 O ( ( n + m ) log ⁡ n ) \mathcal O((n + m)\log n) O((n+m)logn)

树哈希

  • 通过将树结构映射到一个便于存储的哈希值来判断一些树是否同构。
  • 常用的哈希方法有以下两种,设 f x f_x fx 为以 x x x 为根的子树的哈希值,则
    f x = ( s i z e x ∑ f s o n ( x , i ) B i − 1 ) m o d    P (1) f_x = \left(size_x \sum f_{son(x,i)} B^{i - 1}\right)\mod P \tag{1}\\ fx=(sizexfson(x,i)Bi1)modP(1) f x = ( 1 + ∑ f y p r i m e s i z e y ) m o d    P (2) f_x = \left(1 + \sum \limits f_y prime_{size_y}\right) \mod P \tag{2}\\ fx=(1+fyprimesizey)modP(2)
  • 其中 B , P B,P B,P 为选定的质数, s o n ( x , i ) son(x, i) son(x,i) 表示按照 f f f 排序后 x x x 的第 i i i 个子结点, p r i m e i prime_i primei 表示第 i i i 个素数。
  • 第二种方法的冲突概率更低,但需要预处理素数。
  • 通过换根 DP \text{DP} DP 即可得到以任意结点为根整棵树的哈希值,以下为第二种方法的代码。
inline void dfs1(int x)
{
	f[x] = sze[x] = 1;
	for (int y : e[x])
	{
		dfs1(y);
		f[x] = (1ll * f[y] * pri[sze[y]] + f[x]) % mod;
		sze[x] += sze[y];
	}	
}

inline void dfs2(int x)
{
	for (int y : e[x])
	{
		int tmp = f[x];
		add(tmp, g[x]);
		dec(tmp, 1ll * f[y] * pri[sze[y]] % mod);
		g[y] = 1ll * tmp * pri[_n - sze[y]] % mod;
		dfs2(y); 
	}
	add(f[x], g[x]);
}
  • 另一种更简单的方法是直接以树的重心为根 DP \text{DP} DP,因为树的重心不会超过两个,两棵树同构当且仅当重心数目相同且对应的哈希值相同。
  • 上述所有方法在判断两棵树同构之前都应确保两棵树的结点数相同。

无向图三/四元环计数

  • 先给所有的边定向,若两端点度数不同,则由度数较小的点向度数较大的连边,否则由编号较小的向编号较大的连边,具体统计过程见代码。
  • 考虑图中的一条边 u → v u\to v uv,设 v v v 在新图中的出度为 o u t v out_v outv,总复杂度即 ∑ o u t v \sum out_v outv
    • v v v 在原图中的度数小于等于 m \sqrt m m ,则显然有 o u t v ≤ m out_v \le \sqrt m outvm
    • v v v 在原图中的度数大于 m \sqrt m m ,在新图中它只能向度数大于 m \sqrt m m 的点连边,原图中这样的点不会超过 2 m 2\sqrt m 2m 个,所以有 o u t v ≤ 2 m out_v \le 2\sqrt m outv2m
  • 综上,该算法的时间复杂度为 O ( m m ) \mathcal O(m\sqrt m) O(mm ),同时也意味着答案的规模也为 O ( m m ) \mathcal O(m \sqrt m) O(mm )
const int N = 1e5 + 5;
const int M = 2e5 + 5;
vector<int> e[N], o[N];
int px[M], py[M], deg[N], vis[N];

inline bool cmp(const int &x, const int &y)
{
	return deg[x] < deg[y] || deg[x] == deg[y] && x < y;	
}

inline int countCycle3()
{
	int res = 0;
	for (int i = 1; i <= m; ++i)
		++deg[px[i]], ++deg[py[i]];
	for (int i = 1; i <= m; ++i)
	{
		if (!cmp(px[i], py[i]))
			std::swap(px[i], py[i]); 
		e[px[i]].emplace_back(py[i]);
	}
	for (int x = 1, y; x <= n; ++x)
	{
		for (int y : e[x])
			vis[y] = x;
		for (int y : e[x])
			for (int z : e[y])
				res += vis[z] == x;
	}
	return res;
}
  • 四元环计数与三元环计数建新图的过程相同,为表示方便,设新图中 u → v u\to v uv 连边的条件为 u < v u < v u<v,则以下代码中枚举四元环各点间的关系为 x < z , y 1 < z , y 2 < z x < z, y_1 < z, y_2 < z x<z,y1<z,y2<z,而 ( x , y 1 ) (x,y_1) (x,y1) ( x , y 2 ) (x,y_2) (x,y2) 是在原图中存在的边,显然这样不会重复计数,时间复杂度分析与三元环计数相同。
const int N = 1e5 + 5;
const int M = 2e5 + 5;
vector<int> e[N], o[N];
int px[M], py[M], deg[N], vis[N];

inline bool cmp(const int &x, const int &y)
{
	return deg[x] < deg[y] || deg[x] == deg[y] && x < y;	
}

inline int countCycle4()
{
	ll res = 0;
	for (int i = 1; i <= m; ++i)
	{
		++deg[px[i]], ++deg[py[i]];
		o[px[i]].emplace_back(py[i]);
		o[py[i]].emplace_back(px[i]); 
	}
	for (int i = 1; i <= m; ++i)
	{
		if (!cmp(px[i], py[i]))
			std::swap(px[i], py[i]); 
		e[px[i]].emplace_back(py[i]);
	}
	for (int x = 1, y; x <= n; ++x)
	{
		for (int y : o[x])
			for (int z : e[y])
				if (cmp(x, z))
					res += vis[z]++;
		for (int y : o[x])
			for (int z : e[y])
				if (cmp(x, z))
					vis[z] = 0;
	}
	return res;
}

LCA

DFS 序 + ST 表

  • 预处理时间复杂度 O ( n log ⁡ n ) \mathcal O(n \log n) O(nlogn),空间复杂度 O ( n log ⁡ n ) \mathcal O(n \log n) O(nlogn),单次询问时间复杂度 O ( 1 ) \mathcal O(1) O(1)
  • 设结点 x x x DFS \text{DFS} DFS 序编号为 d f n [ x ] dfn[x] dfn[x],则 x , y ( d f n [ x ] < d f n [ y ] ) x,y(dfn[x] < dfn[y]) x,y(dfn[x]<dfn[y]) LCA \text{LCA} LCA [ d f n [ x ] + 1 , d f n [ y ] ] [dfn[x] + 1, dfn[y]] [dfn[x]+1,dfn[y]] 上深度最小的结点的父亲。
inline void dfs(int x)
{
	dfn[x] = ++tis;
	dep[x] = dep[fa[x]] + 1;
	f[0][dfn[x]] = fa[x];
	for (arc *e = adj[x]; e; e = e->nxt)
	{
		int y = e->to;
		if (y == fa[x])
			continue ;
		fa[y] = x;
		dfs(y);
	}
}

inline int queryLCA(int x, int y)
{
	if (x == y)
		return x;
	x = dfn[x], y = dfn[y];
	if (x > y) std::swap(x, y);
	++x;
	int k = Log[y - x + 1];
	return depMin(f[k][x], f[k][y - (1 << k) + 1]);
}

inline void init()
{
	Log[0] = -1;
	for (int i = 1; i <= n; ++i)
		Log[i] = Log[i >> 1] + 1;
	dfs(rt);
	for (int j = 1; j <= Log[n]; ++j)
		for (int i = 1; i + (1 << j) - 1 <= n; ++i)
			f[j][i] = depMin(f[j - 1][i], f[j - 1][i + (1 << j - 1)]);
}

Tarjan

  • 离线,时间复杂度和空间复杂度均为线性。
  • 这里因为使用了 vectorpair 实测常数较大。
inline int ufs_find(int x)
{
	if (fa[x] != x)	
		return fa[x] = ufs_find(fa[x]);
	return x;
}

inline void Tarjan(int x)
{	
	fa[x] = x;
	vis[x] = true;	
	for (arc *e = adj[x]; e; e = e->nxt)
	{
		int y = e->to;
		if (vis[y])
			continue ;
		Tarjan(y);
		fa[y] = x;
	}
	for (pir e : query[x])
		if (vis[e.first])
			ans[e.second] = ufs_find(e.first); 	
}

树上路径并

  • 给出两条路径 ( u 1 , v 1 ) , ( u 2 , v 2 ) (u_1, v_1), (u_2, v_2) (u1,v1),(u2,v2),它们路径并的两个端点为 ( u 1 , u 2 ) , ( u 1 , v 2 ) , ( v 1 , u 2 ) , ( v 1 , v 2 ) (u_1, u_2),(u_1, v_2),(v_1, u_2),(v_1, v_2) (u1,u2),(u1,v2),(v1,u2),(v1,v2) 四对点的 LCA \text{LCA} LCA 中深度最大的那两个点,是否存在路径并只要判断求出的其中一个点是否同时在两条路径上。

证明 分类讨论即可。

你可能感兴趣的:(学习笔记,图论)