Day 5 杨思祺(YOUSIKI)
今天的难度逐渐上升,我也没做什么笔记
开始口胡正解
今天的主要内容是最小生成树,树上倍增和树链剖分
最小生成树
Prim
将所有点分为两个集合,已经和点 1 连通的集合 S、未和点
1 连通的集合 T
计算集合 T 中每个点 u 和集合 S 的距离,d_u=min<u,v>∈E,v∈S{w_u,v }
选取集合 T 中距离 S 最近的点 u,选中对应的边,加入集合 S
重复上面的过程,直到所有点都被加入集合 S
朴素写法时间复杂度较劣,可以采用堆优化至 O((N + M) logN)
Prim 是一个基于贪心的算法,可以采用归纳法和反证法证明其正确性。
首先证明第一条选中的边 e1 一定包含在某最优解方案中
如果最优解中不包含边 e1,则加入 e1 一定会出现环,且环上存在比 e1 大的边,用 e1 替换之答案更优。
假设最优解包含前 k 个选中的边,e1, e2, . . . , ek,则类似地可证明 ek+1 存在于最优解中。
运用归纳法,Prim 算法得到的 n − 1 条边构成最优解
Kruskal
将所有边按照边权从小到大排序# 依次考虑每一条边 < ui, vi >,如果这条边和之前选中的边形成环,则不选中此边;反之,选中此边
当考虑遍所有边后,选中的边一定构成了一棵最小生成树需要并查集的支持,时间复杂度一般认为是 O(M logM)
证明依赖于拟阵的知识。
拟阵
遗传性:若 S 是一个独立集,那么 S 的子集 S′ 是独立集。
遗传性的推论:空集是独立集。
交换性:若 A 和 B 是 S 的两个独立集且 |A| < |B|,那么存在一个元素 x 满足 x < A 且 x ∈ B,使得 A ∪ {x} 是一个独立集
交换性的推论:一个集合的所有极大独立集大小都相同。例:线性无关组、无向图生成森林。
拟阵最优化
拟阵最优化问题:将集合中每个元素赋予一个权值,求权值和最小 (大) 的极大独立集。
拟阵最优化的贪心算法:
维护当前独立集 G,初始为空。将元素按照权值排序,从小到大枚举元素 x,若 G ∪ {x} 是一个独立集,那么就将 x 加入独立集并将 x 的权值累加入答案。最后的结果就是权值和最小的及大独立集。
证明:如果最优解不包含最小元素 x_1,记该集合为 A,创建新的集合B {x_1},利用交换性不断将 A 中元素加入 B 直到 |A| |B|,则有 B 集合为更优解,矛盾。故 x 一定属于最优解。利用数学归纳法,假设已经证明 x_1, x_2, . . . , x_k−1 属于最优解,如果存在最优解不包含 x_k,还是创建新的集合 B {x_1, x_2, . . . , x_k−1, x_k },利用交换性将元素加入 B 得到更优解,矛盾。
例题比较多,就不放了
树上倍增
序列倍增
回忆普通的序列倍增思想,以 ST 表为例。
Fi,j 记录区间 [i, i + 2 j − 1] 内信息(区间和或区间最值)
Fi,j =merge(Fi,,j−1, Fi+2 j−1,j−1 )
取出区间 [l, r] 答案时,使用若干个 Fi,j 即可
如果求区间最值,那么取 Fl,k 和 Fr−2 k ,k 即可,其中 k ⌊log2(r − l + 1⌋
如果求区间和,可以取 Fl,k1 , Fl+2 k1 ,k2 , Fl+2 k1+2 k2 ,k3 , . . . 即可
树上倍增的主要思想
树上从每个点出发到根结点的链也具有类似的形式
Fi,j 表示点 i 向上走 2 j 步的结点
Fi,0 就是点 i 的父亲节点
Fi,j =FFi,j−1,j−1
如何求解 u 向上移动 k 步是哪个点?
将 k 写作 2 的幂次之和,如 11=2 3 + 2 1 + 2 0。 用 Gi,j 表示 i 向上移动 j 步的结果.
Gu,11=FGu,10,0
Gu,10=FGu,8,1
Gu,8=Fu,3 在 O(logN) 步内完成。
LCA(最近公共祖先)
树上倍增最常见的用处是求解两个点的最近公共祖先。
求解 a 和 b 的最近公共祖先
将 a 和 b 调整到相同高度
判断 a 和 b 是否重合,若重合则该点即为答案
令 a 和 b 一起向上移动尽可能大的距离,保证移动后两点 不重合
此时两点的父亲结点即为答案 单次询问时间复杂度 O(logN)
最近公共祖先的常见求解方法有
1.树上倍增
2.树链剖分
3.DFS 序 +RMQ
向上路径
如何求解从 u 到 v 路径上边权最值?保证 v 是 u 的祖先。
Mi,j 表示点 i 出发向上移动 2 j 步经过边权最值
Mi,0=Wi, fatheri
Mi,j =merge(Mi,j−1, MFi,j−1,j−1) 在树上倍增向上走时取移动区间最值更新答案即可。
树上路径
记 g=LCA(u, v),则树上从 u 到 v 的路径可以拆分为两段:
从 u 到 g 的路径
从 g 到 v 的路径
如何求解从 u 到 v 路径上的边权和?
将路径拆分为两段向上路径,分别求解其答案再合并。
树链剖分
首先是一些基本的概念
树链:不拐弯的路径
剖分:每个点属于一条链
重儿子:子树大小最大的子结点
重链:从一点出发,一直选择重儿子向下走,走到叶子
轻边:不属于任何一条重链的边
分析:从任意一点 u 走到根结点,经过的重链、轻边个数的量级 是多少?
走一条重链、轻边,子树大小至少翻倍,故易知 O(logN)。
一些记号
dep_u 表示点 u 的深度
fat_u 表示点 u 的父亲节点
top_u 表示点 u 所在重链的顶端结点
dfn_u 表示重儿子优先 DFS 下点 u 的时间戳
end_u 表示重儿子优先 DFS 下点 u 子树最大时间戳
求点 a 和点 b 的最近公共祖先.
记 ta=top_a , tb=top_b
如果 ta=tb,则 a 和 b 在同一条重链,深度较小的为 LCA
如果 dep_ta > dep_tb,那么令 a=fat_ta
如果 dep_ta < dep_tb,那么令 b=fat_tb
将树序列化
树的结构较为复杂,相较而言我们更喜欢序列这样的一维结构, 因为有丰富的数据结构及其它算法可以处理序列上的种种问题。
那么能否将树转化为序列以便维护信息?
如果按照 dfn_u 将树转化为序列,有哪些有用的性质?
子树是序列中连续一段
树上路径由 O(logN) 个区间组成
下放板子题
luogu P4114 Qtree1
#include <iostream> #include <cstring> #include <cstdio> #include <algorithm> using namespace std; long long read() { long long a=0,b=1; char c=getchar(); while(!isdigit(c)) { if(c=='-') b=-1; c=getchar(); } while(isdigit(c)) { a=(a<<3)+(a<<1)+(c^48); c=getchar(); } return a*b; } void out(long long a) { if(a>9) out(a/10); putchar(a%10+'0'); } const int maxn=100005; int n,a[maxn];//a数组表示点与父亲连边的长度 int num,head[maxn]; struct node { int to,nxt,dis; }edge[maxn<<1]; void add(int u,int v,int d) { edge[num].dis=d; edge[num].to=v; edge[num].nxt=head[u]; head[u]=num; num++; } int f[maxn],sz[maxn],dep[maxn],son[maxn]; void dfs(int u,int fa) { int maxm=0; sz[u]=1; for(int i=head[u];~i;i=edge[i].nxt) { int v=edge[i].to; if(v==fa) continue; f[v]=u; dep[v]=dep[u]+1; a[v]=edge[i].dis; dfs(v,u); sz[u]+=sz[v]; if(sz[v]>maxm) maxm=sz[v],son[u]=v; } } int b[maxn],in[maxn],top[maxn];//b数组表示当前编号所指的点,in数组表示点的编号,top数组表示点所在链的顶端 void dfs(int u,int fa,int topf) { in[u]=++num; b[num]=u; top[u]=topf; if(!son[u]) return; dfs(son[u],u,topf); for(int i=head[u];~i;i=edge[i].nxt) { int v=edge[i].to; if(v==fa||v==son[u]) continue; dfs(v,u,v); } } #define lc p<<1 #define rc p<<1|1 struct Node { int val; }t[maxn<<2]; void pushup(int p) { t[p].val=max(t[lc].val,t[rc].val); } void build(int p,int l,int r) { if(l==r) { t[p].val=a[b[l]]; return; } int m=l+r>>1; build(lc,l,m); build(rc,m+1,r); pushup(p); } void update(int p,int l,int r,int L,int z){ if(l==r) { t[p].val=z; return; } int m=l+r>>1; if(m>=L) update(lc,l,m,L,z); else update(rc,m+1,r,L,z); pushup(p); } int query(int p,int l,int r,int L,int R) { if(l>R||r<L) return 0; if(L<=l&&r<=R) return t[p].val; int m=l+r>>1; return max(query(lc,l,m,L,R),query(rc,m+1,r,L,R)); } void solve_1()//从第0条边开始存,每条边存两次,所以输入的第i条边对应的是第2*i-2和第2*i-1条边,谁是儿子改谁 { int x=read()*2-2,d=read(),u=edge[x].to,v=edge[x+1].to; if(f[v]==u) update(1,1,n,in[v],d); else update(1,1,n,in[u],d); } void solve_2()//链上查询 { int x=read(),y=read(),ans=0; while(top[x]!=top[y]) { if(dep[top[x]]<dep[top[y]]) swap(x,y); ans=max(ans,query(1,1,n,in[top[x]],in[x]));//由于存的是与父亲的距离,而top的父亲直接取所以此处无需加一 x=f[top[x]]; } if(dep[x]>dep[y]) swap(x,y); ans=max(ans,query(1,1,n,in[x]+1,in[y]));//但是最后一次存储必须加1否则会多询问一条边 out(ans); puts(""); } int main() { memset(head,-1,sizeof(head)); n=read(); for(int i=1;i<n;i++) { int x=read(),y=read(),d=read(); add(x,y,d); add(y,x,d); } num=0; dfs(1,-1); dfs(1,-1,1); build(1,1,n); char z[10]; while(1) { scanf("%s",z); if(z[0]=='D') break; switch(z[0]) { case 'C':solve_1();break; case 'Q':solve_2();break; } } return 0; }
Day 6 杨思祺(YOUSIKI)
这一天主要讲了有关联通分量的内容,下午考试...(真~爆零)
强连通分量
在有向图中,如果两个点之间存在两个方向的路径,则称两个点 强连通;如果有向图的任何两个顶点都强连通,则称其为强连通图;有向图的极大强连通子图即为强连通分量。
缩点
强连通分量最常见的用途是将能互相到达的点集缩为一个新的点,建立的新图一定是有向无环图。
Tarjan 算法
Tarjan 算法可以在 O(N + M) 的时间复杂度内求解有向图的所 有强连通分量。 首先提取出有向图的一棵搜索树作为基础框架。 # d f nu 为 u 点的 DFS 时间戳 # lowu 为 u 点出发能追溯到的最小时间戳 low_u=min{dfn_u , low_v1 , dfn_v2 } 其中 < u, v1 > 为树枝边,< u, v2 > 为后向边
模板
void tarjan(int x) { dfn[x]=low[x]=++tim; vis[x]=1; s[++top]=x; for(int i=head[x];i;i=nxt[i]) { int y=to[i]; if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]); else if(vis[y]) low[x]=min(low[x],dfn[y]); } if(dfn[x]==low[x]) { int y;num++; do { y=s[top--]; vis[y]=0; color[y]=num; size[num]++; }while(x!=y); } }
双连通分量
在无向图中,如果无论删去哪条边都不能使得 u 和 v 不联通, 则称 u 和 v 边双连通;
在无向图中,如果无论删去哪个点(非 u 和 v)都不能使得 u 和 v 不联通,则称 u 和 v 点双连通。
割点:删去该点,图分裂为多个连通块。
割边:也叫“桥”,删去该边,图分裂为多个连通块。
点双连通分量
类似地,定义 dfn_u 和 low_u。 如果 v 是 u 的子结点,并且 low_v ≥ dfn_u 则点 u 是割点,删去点u后 v 子树和其它点不连通
每个割点属于多个点双连通分量,非割点只属于一个点双连通分量。
边双连通分量
类似地,定义 dfn_u 和 low_u。 如果 v 是 u 的子结点,并且 low_v > dfn_u 则边 < u, v > 是割边。
每个点属于一个边双连通分量,边双连通分量之间以割边连接。
Day 7 杨思祺(YOUSIKI)
最后一天,讲了有关二分图和差分约束的知识
二分图
二分图:点黑白染色,邻点不同色。
如何判断一个给定的无向图是不是二分图?
从任意一点开始 BFS 或 DFS,起点不妨染色为白
当前在 u 点时,尝试将所有邻点染为不同的颜色
如果邻点已经染色且颜色不符,则不是二分图
二分图的等价条件
无向图是二分图当且仅当其不包含奇环。
二分图匹配
匹配:选取一些边,使得任意两条边没有公共点(每个点至多属于一条边)
最大匹配:选取边数尽可能多
完美匹配:所有点都在匹配中(每个点恰好属于一条边)
匹配边:选中的边
非匹配边:没有选中的边
匹配点:和选中边相连的点
非匹配点:和选中边不相连的点
常常将二分图的点画成两列
二分图最大匹配是一个常见问题.
匈牙利算法
网络流
匈牙利算法
理论基础
交错路:从非匹配点出发,依次经过非匹配边、匹配边、非匹配边...
增广路:从非匹配点出发,结束于非匹配点的交错路
增广路定理:任意一个非最大匹配的匹配一定存在增广路
算法思想
初始没有选中的边
寻找增广路
找到增广路则将路径中匹配边和非匹配边对换
找不到增广路则当前为最大匹配
网络流
取额外的两个点作为源点和汇点
源点向左边一列每个点连流量为 1 的边
右边一列每个点向汇点连流量为 1 的边
二分图中每条边从左向右连流量为 1 的边
求最大流即可
最小顶点覆盖 Knoig 定理
二分图最小顶点覆盖数等于其最大匹配数。
最小路径覆盖
给定有向图 G < V, E >。
设 P 是 G 的一个简单路(顶点不相 交)的集合。
如果 V 中每个顶点恰好在 P 的一条路上,则称 P 是 G 的一个路径覆盖。
P 中路径可以从 V 的任何一个顶点开始, 长度也是任意的,特别地,可以为 0.
G 的最小路径覆盖是 G 的 所含路径条数最少的路径覆盖。
最小路径覆盖 = |V| - 二分图最大匹配
二分图:将原图每个点拆分为入点和出点,如果原图存在 u 到 v 的边,则在 u 的出点和 v 的入点间连无向边。
差分约束
差分约束可以确定特定的不等式组是否存在解.
xi1 − xj1 ≤ a1
xi2 − xj2 ≤ a2
......
为每个变量 xi 建立一个点 pi.
如果要求 xi − xj ≤ a,则建立一条从 pj 到 pi 长度为 a 的边
以任意一点为起点求单源最短路,则一定有 di − dj ≤ a 如果出现负环,则归结出形如 xi − xi < 0 的约束,不等式组无解。
如果没有负环,最短路算法得到的距离数组就是一组合法的解。