图论

图论这……感觉是知识点多,算法多,但用的频率不一定高,容易忘记,这里就整点板子防老年痴呆

欧拉路径

即一笔画问题,定义为:
图中经过所有边恰好一次的路径叫欧拉路径。如果此路径的起点和终点相同,则称其为一条欧拉回路

注意图必须连通,可用并查集或dfs判断

关于判断欧拉路径是否存在:

以下 \(S\) 为起点, \(T\) 为终点,并保证唯一

  • 有向图欧拉路径:\(S\) : 入度 + 1 = 出度; \(T\) : 入度 = 出度 + 1; 其余节点: 入度 = 出度
  • 有向图欧拉回路:所有节点: 入度 = 出度
  • 无向图欧拉路径:\(S, T\) : 度数为奇; 其余节点: 度数为偶
  • 无向图欧拉回路:所有节点: 度数为偶

在欧拉回路中,起点终点可以为任意点

关于求出欧拉回路

欧拉回路与欧拉路径(上)
欧拉回路与欧拉路径(下)
有向图:
\(S\) 开始跑dfs,每条边跑完后删去(打上标记),跑完所有儿子后再将该点入栈。
最后将整个栈反过来即为答案
关于正确性可画图感性理解

我是懒狗我不管

参考代码:

//N 为点的数量, M 为边的数量
int head[N], tot;
struct edge{
	int to, nxt;
	bool vis;
}e[M];
//有向边

int ans[M], cnt;
//每条边一次,共 M + 1 个点
//注意 M 开为数据范围 +5 即可

inline void Dfs(int now){
	for(int i = head[now]; i; i = e[i].nxt){
		if(!e[i].vis){
		//未访问过
			e[i].vis = 1;
			Dfs(e[i].to);
			//访问
		}
	}
	ans[++ cnt] = now;
	//入答案栈
}

一定注意最后答案是倒着入的栈,故需倒序输出

发现若用链式前向星存图,我们找未访问过的边时需将与该点相连的所有边都访问一次。
在这一操作上,其实使用领接链表更为便捷,以下为代码。

int num[N];//用法具体看Dfs函数
vector<int> to[N];
int ans[M], cnt;

inline void Dfs(int now){
	for(int i = num[now]; i < to[now].size(); i = num[now]){
	//以访问过的节点再也不会被访问啦
		num[now] ++;
		Dfs(to[now][i]);
	}
	ans[++ cnt] = now;
}

其实链式前向星应该也是支持这个操作的。只是

我是懒狗我不管

下面会提到如何实现

无向图:
类似于有向图的方法,只是删边时正边和反边一起删除罢了。

此时,若使用领接链表,会发现并不好处理删边,我们转回链式前向星。
实际上我们只是在找边时将查询的起始位置更新了,来避免多余的查询,将这思想放在链式前向星上,便是以下的代码。

int head[N], tot = 1;
//tot 从奇数开始方便查询反边
struct edge{
	int to, nxt;
	bool vis;
	//因为有未访问但被删除的反边存在,我们还是需要删除标记
}e[M << 1];
//无向图

int ans[M], cnt;

inline void Dfs(int now){
	for(int i = head[now]; i; i = head[now]){
		if(e[i].vis){
		//边已被删
			head[now] = e[i].nxt;
			continue;
		}
		e[i].vis = 1;
		e[i ^ 1].vis = 1;
		//删边
		head[now] = e[i].nxt;
		Dfs(e[i].to);
		
		ans[++ cnt] = now;
		//此句放循环外或内还并不清楚,有知道的麻烦留言提醒,谢谢了
	}
}
貌似欧拉回路题挺少的说

虚树

OI-Wiki
洛谷日报
对于树上需要用到的关键节点建立一颗新树,再在新树上解决问题以降低时间复杂度。
用虚树时一定要注意数据范围,虚树的时间复杂度建立在总询问节点数上。
其实虚树题难度还是来源于树形dp,虚树较为板。

while(m --){
	cin >> x;
	for(int i = 1; i <= x; ++ i){
		cin >> c[i];
	} c[++ x] = 1;
	//大根节点要算上
	
	sort(c + 1, c + x + 1, cmp);
	
	for(int i = 1; i < x; ++ i){
		s[++ cnt] = c[i];
		s[++ cnt] = Lca(c[i], c[i + 1]);
	} s[++ cnt] = c[x];
	
	sort(s + 1, s + cnt + 1, cmp);
	cnt = unique(s + 1, s + cnt + 1) - s - 1;
	//去重
	
	for(int i = 1; i < cnt; ++ i){
		int L = Lca(s[i], s[i + 1]);
		add(L, s[i + 1]);
	} //建立虚树
	
	Dfs(1, 0); //虚树上跑树形dp
	cout << ans << '\n';
	
	//清空!
	for(int i = 1; i < cnt; ++ i){
		int L = Lca(s[i], s[i + 1]);
		head[L] = 0;
	} tot = 0;
	cnt = 0;
}

求Lca时可用 \(O(logn)\) 的树剖、倍增,但我喜欢在虚树中使用 \(O(1)\) 的st表求Lca。
关于st表求Lca,在此处有提及

支配树

CSDN
OI-Wiki

定义

在 DAG 上定义一个终点 \(t\) ,对于一个节点 \(x\) 满足对于图上任意一条从 \(s\)\(t\) 的路径都需要经过节点 \(x\) ,则称 \(x\)\(s\) 的支配点。
在一新图中,若 \(x\)\(s\) 的支配点,则连接一条 \((s,x)\) 的边。发现若存在边 \((s,x)\)\(x,y\) ,则一定存在边 \((s, y)\) 。我们可以简化图删去边 \((s, y)\)
最后会得到一张 \(n\) 个点 \(n-1\) 条边的图,即为树,称为支配树。

求解支配树

我们发现 DAG 有一个很好的性质:根据拓扑序求解,先求得的解不会对后续的解产生影响。我们可以利用这个特点快速求得 DAG 的支配树。
对于当前一点 \(a_i\) ,因为为拓扑序,我们不需考虑删边的影响。设图中以 \(a_i\) 为起点的边有 \((a_i, b_1)\) , \((a_i, b_2)\) , \((a_i, b_3)\) ... 则 \(a_i\) 在支配树中的父亲节点为 \(Lca(b_1, b_2, b_3 ...)\)
依次我们可以用倍增发在 \(O(mlogn)\) 的时间中建出支配树。

这个方法只适用于 DAG(有向无环图) !
int in[N];
//入度

vector<int> w[N];
//w[i] 存从 i 出发到达的点
int tp[N], cnt = -1;
//tp 存 topu 序
//cnt 赋值为 -1 是防止 0 节点入 topu 序
//若没有这种情况 cnt 直接为 0 也可

int q[N], l, r;
//手动队列

inline void topu(int x){
	l = 1; r = 0;
	q[++ r] = x;
	
	while(l <= r){
		int now = q[l ++];
		tp[++ cnt] = now;
		
		for(int i = head[now]; i; i = e[i].nxt){
			int v = e[i].to;
			
			if(in[v]){
				in[v] --;
				
				if(now){
				//不管 0
					w[v].push_back(now);
				}
				
				if(!in[v]){
					q[++ r] = v;
				}
			}
		}
	}
}

int dep[N];
int fa[N][22];

inline int Lca(int x, int y){
	if(dep[x] < dep[y]){
		swap(x, y);
	}
	
	for(int i = 20; i >= 0; -- i){
		if(dep[fa[x][i]] >= dep[y]){
			x = fa[x][i];
		}
	}
	
	if(x == y){
		return x;
	}
	
	for(int i = 20; i >= 0; -- i){
		if(fa[x][i] != fa[y][i]){
			x = fa[x][i];
			y = fa[y][i];
		}
	}
	
	return fa[x][0];
}

int main(){
	topu(0);
	
	for(int i = 0; i <= n; ++ i){
		head[i] = 0;
	} tot = 0;
	//清空原图,建新图(支配树)
	
	for(int i = 1; i <= n; ++ i){
		x = tp[i];
		//依 topu 序处理
		
		for(int j = 0; j < w[x].size(); ++ j){
			if(!j) fa[x][0] = w[x][j];
			else fa[x][0] = Lca(fa[x][0], w[x][j]);
			//求当前节点在树上父亲节点
		}
		
		add(fa[x][0], x);
		dep[x] = dep[fa[x][0]] + 1;
		//实时处理深度
		
		for(int j = 1; j <= 20; ++ j){
			fa[x][j] = fa[fa[x][j - 1]][j - 1];
			//实时维护倍增
		}
	}
}

例题

luogu P2597 灾难
支配树起源题,题解中讲得很清楚(比我好多了)

LCT

参考资料:

[OI-wiki](Link Cut Tree - OI Wiki)

[【算法】LCT - Cloote](【算法】LCT - Cloote - 博客园 (cnblogs.com))

动态树,可断边且连边,但保证形态仍是一棵树。这时树上的信息可用 LCT 维护。

我累了,直接上代码

算了,累得不想上,你们看 [Cloote](【算法】LCT - Cloote - 博客园 (cnblogs.com)) 的算了

我的代码:

#include<bits/stdc++.h>
#define lx (son[x][0])
#define rx (son[x][1])
using namespace std;
const int N = 1e5 + 5;

int n, m;
int a[N];

int son[N][2], fa[N];
int tree[N], tag[N];

inline void pushup(int x){
	tree[x] = tree[lx] ^ a[x] ^ tree[rx];
}
inline bool get(int x){
	return son[fa[x]][1] == x;
}
inline bool isrt(int x){
	return son[fa[x]][0] != x && son[fa[x]][1] != x;
}
inline void wap(int x){
	swap(lx, rx);
	tag[x] ^= 1;
}
inline void pushdown(int x){
	if(tag[x]){
		if(lx) wap(lx);
		if(rx) wap(rx);
		tag[x] = 0;
	}
}
inline void update(int x){
	if(!isrt(x)) update(fa[x]);
	pushdown(x);
}

inline void rotate(int x){
	int fath = fa[x];
	int d = get(x);
	int D = get(fath);
	
	if(!isrt(fath)) son[fa[fath]][D] = x;
	fa[x] = fa[fath];
	
	son[fath][d] = son[x][d ^ 1];
	if(son[x][d ^ 1]) fa[son[x][d ^ 1]] = fath;
	
	son[x][d ^ 1] = fath;
	fa[fath] = x;
	
	pushup(fath);
	pushup(x);
}
inline void splay(int x){
	update(x);
	//千万别漏
	//这里和 Cloote 的有点不同,但本人觉得这样更方便
	int fath = fa[x];
	while(!isrt(x)){
		if(!isrt(fath)) rotate(get(x) == get(fath) ? fath : x);
		rotate(x);
		fath = fa[x];
	}
}

inline void access(int x){
	int p = 0;
	while(x){
		splay(x);
		son[x][1] = p;
		pushup(x);
		//千万别漏
		p = x;
		x = fa[x];
	}
}
inline void makert(int x){
	access(x);
	splay(x);
	wap(x);
}
inline void split(int x, int y){
	makert(x);
	access(y);
	splay(y);
}
inline int find(int x){
	access(x);
	splay(x);
	while(son[x][0]){
		pushdown(x);
		//千万别漏
		x = son[x][0];
	}
	splay(x);
	return x;
}
inline void link(int x, int y){
	makert(x);
	if(find(y) == x) return ;
	fa[x] = y;
}
inline void cut(int x, int y){
	makert(x);
	if(find(y) != x || fa[y] != x || son[x][1] != y || son[y][0]) return ;
	fa[y] = son[x][1] = 0;
//	pushup(x);
	//这里不知道要不要写,保险尽量写上吧
}

int op, x, y;

int main(){
	ios::sync_with_stdio(false);
	
	cin >> n >> m;
	
	for(int i = 1; i <= n; ++ i){
		cin >> a[i];
	}
	
	while(m --){
		cin >> op >> x >> y;
		
		if(op == 0){
			split(x, y);
			cout << tree[y] << '\n';
		}
		
		if(op == 1){
			link(x, y);
		}
		
		if(op == 2){
			cut(x, y);
		}
		
		if(op == 3){
			makert(x);
			a[x] = y;
		}
	}
	
	return 0;
}

平面图和对偶图

参考资料
oi-wiki

平面图

指除顶点处边与边都不相交的图

设有平面图 \(G\) ,它将平面分为 \(k\) 个部分(包括最外部分) ,每个部分被称为该图的一个面。
包围一个面的所有边称为这个面的边界,面的次数为边界的边数(注意此时割边会被计算两次)。

性质

设图的点数为 \(n\) ,边数为 \(m\) ,面数为 \(k\) ,次数为 \(c_i\)

  • \(n-m+k=2\)
  • \(\sum_{i=1}^{k}c_i = 2*m\)

对偶图

对于一个平面图 \(G\) ,它的对偶图 \(G'\) 的构造方式为:

  1. 将平面图中的每个面看作是对偶图中的一个点
  2. 对于每条边,连接以它为边界的两个面(对偶图上的点);割边会连出自环

性质

  • \(G'\) 也为平面图
  • \(G\) 中的割边对应 \(G'\) 中的自环,\(G\) 中的自环对应 \(G'\) 中的割边
  • \(G'\) 中每个点的度数等于其在 \(G\) 中对应的面的次数
  • 平面图最大流=最小割=对偶图最短路
posted @ 2023-10-08 15:12  Biuld  阅读(21)  评论(0编辑  收藏  举报