图论
图论这……感觉是知识点多,算法多,但用的频率不一定高,容易忘记,这里就整点板子防老年痴呆
欧拉路径
即一笔画问题,定义为:
图中经过所有边恰好一次的路径叫欧拉路径。如果此路径的起点和终点相同,则称其为一条欧拉回路。
注意图必须连通,可用并查集或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,在此处有提及
支配树
定义
在 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'\) 的构造方式为:
- 将平面图中的每个面看作是对偶图中的一个点
- 对于每条边,连接以它为边界的两个面(对偶图上的点);割边会连出自环
性质
- \(G'\) 也为平面图
- \(G\) 中的割边对应 \(G'\) 中的自环,\(G\) 中的自环对应 \(G'\) 中的割边
- \(G'\) 中每个点的度数等于其在 \(G\) 中对应的面的次数
- 平面图最大流=最小割=对偶图最短路