1.图与树
- 最短路
所谓最短路,在图上确定序列长度为 \(n\) 的序列 \(A\) 为 \({P_1,P_2,...P_n}\),其中总有 \(P_i \rightarrow P_{i+1} \in E\),并且最小化 \(\sum_{i=1}^n W_{(P_i,P_{i+1})}\) 。
算法 1.dijkstra
其应用于非负权边。
考虑当前钦定点 \(i\) 拥有最短路,且为全局最小,那么我们更新任何 $ (i,j) \in E$ 之后的 \(i\) 的最短路便被锁定不可修改。
考虑其正确性:
假设 \(i\) 之后仍然有最短路将其更新,那么一定为祖先更新而来和儿子更新过来,我们定义祖先为算法流程中比 \(i\) 先更新的节点,儿子同理。容易发现之前的祖先已经被加算过,而此时儿子的 \(dis_j>dis_i+w_{(i,j)}\),其中 \(w>0\),那么再次更新一定不优,证毕。
朴素来讲时间 \(O(n^2)\),考虑求最小的步骤使用堆优化为 \(O(n \log m)\)。
点击查看代码
#include"bits/stdc++.h"
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;int n,m,s;const int N=1e5+10,M=2e5+10;int h[N],e[M],ne[M],w[M],idx;void add(int a,int b,int c){e[idx]=b,ne[idx]=h[a],w[idx]=c,h[a]=idx++;
}ll dist[N];
bool st[N];void dij(){memset(dist,0x3f,sizeof dist);dist[s]=0;priority_queue<pii,vector<pii>,greater<pii> > q;q.push({0,s});while(q.size()){auto t = q.top();q.pop();int dis=t.first,ver=t.second;if(st[ver]) continue;st[ver]=1;for(int i=h[ver];~i;i=ne[i]){int j=e[i];if(dist[j]>dist[ver]+w[i]){dist[j]=dist[ver]+w[i];q.push({dist[j],j});}}}}int main(){memset(h,-1,sizeof h);cin>>n>>m>>s;while(m--){int a,b,c;cin>>a>>b>>c;add(a,b,c);}dij();for(int i=1;i<=n;i++){cout<<dist[i]<<" ";}return 0;
}
算法2.SPFA
虽然死力。
考虑拥有负权边的图上,其在更新结点之后不一定为最短,考虑更新 \(i\) 之后重置标记,并且如果当前点在队列中,我们考虑其不可被再次入队,因为一定不优。考虑入队次数,如果多次入队可判断负环。时间来说 \(O(n)\) 因为进队一次出对一次,但是严格卡的话 \(O(nm)\),别问怎么卡。
点击查看代码
int dist[N];
bool st[N];
void spfa(int s){queue<int> q;q.push(s);memset(dist,-0x3f,sizeof dist);memset(st,0,sizeof st);dist[s]=0;st[s]=1;while(!q.empty()){int x=q.front();q.pop();st[x]=0;for(int i=h[x];~i;i=ne[i]){int j=e[i];if(dist[j]<dist[x]+w[i]){dist[j]=dist[x]+w[i];if(!st[j]){st[j]=1;q.push(j);}}}}
}
3.Floyd
注意到前面的算法用于,考虑 \(f_{i,j}\) 表示 \(i\) 到 \(j\) 的最短路,循环钦定 \(i \rightarrow k\) 和 \(k \rightarrow j\) 即可。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define N 110
#define INF INT_MAX-2
int a[N][N];
int n,m;
void floyd(int x){for(int k=1;k<=n;k++){for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){if(a[i][k]<INF&&a[k][j]<INF){a[i][j]=min(a[i][j],a[i][k]+a[k][j]);}}}} for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){cout<<a[i][j]<<" ";}cout<<"\n";}
}
signed main(){ios::sync_with_stdio(false);cout.tie(nullptr);cin.tie(nullptr);int u,v,w;cin>>n>>m;for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(i==j)a[i][j]=0;else a[i][j]=INF;for(int i=1;i<=m;i++){cin>>u>>v>>w;a[u][v]=min(a[u][v],w);a[v][u]=min(a[v][u],w);}floyd(1);return 0;
}
2.最小生成树
详见最小生成树定义。
讨论最小生成树,我们容易想到一个朴素的贪心思想,使得最小生成树的权值总和最小。
在确定了选边顺序之后,考虑到如下情况:
- 第 \(i\) 条边是一个 \((u_i,v_i,w_i)\)的三元组描述一条边,表示 \(u_i\) 指向 \(v_i\),边权为 \(w_i\)。
- 第 \(i+1\) 条边是一个 \((u_{i+1},v_{i+1},w_{i+1})\)的三元组描述一条边,表示如上。
正确性显然。
所以在我们选择 \(i+1\) 条边时,我们查找这个边的两个端点所在集合是否在同一个集合,如果是就不能加入,否则形成环。
思想如上。
例题
Agri-Net最短网络。
模板题,不赘述。
灌水
建立虚拟点,将单独的修建改为统一的操作。
丛林道路
模板题。
坏电工
模板题,最大生成树。
极地网络
考虑将所有的点相连建成完全图,注意建成的距离是平面欧氏距离计算,接着进行
Kruscal
算法。
【Usaco Dec07 Silver】修建道路
如极地网络,略有不同。
【Usaco Nov08 Gold】安慰奶牛
包含了点权,考虑每一条边的所有总时间为端点安慰时间加上边权,接着进行Krucal
算法。
南渝泼水节
非常简单,考虑合并两个集合,我们进行表示:
- 集合一:\(F_1\) 描述祖先,\(S_1\) 描述大小。
- 集合二同理。
则在描述的一条边 \((u,v,w)\) 连接两个上述集合,则我们要扩展为完全图,一共有要连 \(S_1 \times S_2 - 1\) 条边,考虑到保持最小生成树,则连得边的边长通为 \(w+1\)。
点击查看代码
for(int i=1;i<n;i++){int fa=find(a[i].u),fb=find(a[i].v);if(fa!=fb){ans+=(size[fa]*size[fb]-1)*(a[i].w+1);f[fa]=fb;size[fb]+=size[fa]; }
}
cout<<ans<<endl;
Tree I
智力题,就是二分答案确定我们对白边的权值所改变的值,以此确定我们的白边数量每次趋近 \(need\) 条边。
3.最近公共祖先
引入
我们常常会遇到在一个家族中,两个人的祖辈是否相同,如果相同,最近的在那一辈。
对于这种情况,我们首先考虑到这个家族的关系图一定是一棵树,对于两个人我们就看做两个节点,求“两个人的祖辈是否相同,如果相同,最近的在那一辈”的问题,就是我们常说的 LCA,即 Lowest Common Ancestor 最近公共祖先。
倍增求 \(\text {LCA}\)
这种 LCA 求法是一种用 \(2\) 的幂次进行优化的暴力,下文简述,用 LCA 代替 倍增法求LCA。其是一种介于一维数组的稀疏表示与二维数组的稠密表示之间,其时间复杂度带有 \(\log n\),可优化成为 \(O(1)\),后文讨论。
LCA 和 RMQ问题 极度相识,其转态设计类似于 \(f_{u,i}\),表示 \(u\) 的 \(2^i\) 级祖先。而在 RMQ 中的 \(f_{i,j}\) 表示 \(i\) 的后 \(2^j\) 个数的最大值或则和。
形式化的问题如下:
给定一颗静态树,树中两点 \(u,v\),求 \(\text{LCA}_{u,v}\)。
我们考虑到同时查询,则先讲两个点提至同一深度,接着一起向上跳。
我们会发现,有的时候我们在调到一定深度时两个节点相同,便返回任意节点为其两个节点的 \(\text{LCA}\)。
此处的调整高度主要采取倍增调整,但是要注意到倍增 \(\text{LCA}\) 有一种原则:先跨大步再跨小步。因为不一定在跨到下一步的时候,令节点的位置为 \(i\),则不一定保证 \(dep_i>dep_{\text {LCA}}\)。
注意到代码中的 \(dep_u>dep_v\),其中 \(dep_i\) 为 \(i\) 的深度。关于 \(f_{u,i}\) 的定义,详见前文。
伪代码:
点击查看代码
for(int i=log(dep[u]);i>=0;i--){if(dep[fa[u][i]]>=dep[v]) u=fa[u][i];
}
但是当深度相同时,我们不一定相等,则考虑到倍增向上跳,同样遵循“先大步再小步”,但注意我们求得是“最近公共祖先”,则在跳越查找的时候,有跳到 \(\text{LCA}_{u,v}\) 的更高级祖先的可能,于是为了处理,则我们在跳的时候,考虑下一步是否相等,如果相等则遇见刚才的情况,不跳,反之则跳。
伪代码:
点击查看代码
for(int i=log(dep[u]);i>=0;i--){if(fa[u][i]!=fa[v][i]){u=fa[u][i],v=fa[v][i];}
}
return f[u][0];//返回父亲节点,意思是我们最后完成//“相等则跳,不相等则不跳”的原则//则跳到的一定是 LCA 的儿子节点//此处易证
Tarjan 离线算法
以下内容源自 oi-wiki。
Tarjan 算法是一种离线算法,需要使用并查集记录某个结点的祖先结点。
做法如下:
-
首先接受输入边(邻接链表)、查询边(存储在另一个邻接链表内)。查询边其实是虚拟加上去的边,为了方便,每次输入查询边的时候,将这个边及其反向边都加入到 queryEdge 数组里。
-
然后对其进行一次 DFS 遍历,同时使用 visited 数组进行记录某个结点是否被访问过、parent 记录当前结点的父亲结点。
-
其中涉及到了回溯思想,我们每次遍历到某个结点的时候,认为这个结点的根结点就是它本身。让以这个结点为根节点的 DFS 全部遍历完毕了以后,再将这个结点的根节点设置为这个结点的父一级结点。
-
回溯的时候,如果以该节点为起点,queryEdge 查询边的另一个结点也恰好访问过了,则直接更新查询边的 LCA 结果。
最后输出结果。
性质:Tarjan算法需要初始化并查集,所以预处理的时间复杂度为 \(O(n)\)。
欧拉序 求 \(\text{LCA}\)
时间复杂度 \(O(n\log n+m)\),其中 \(n\) 为节点个数,\(m\) 为查询次数。
欧拉序就是指访问到那个节点就加入欧拉序,一般都是 DFS 访问。
其实我们发现 LCA 就是在两点之间的树上最短路径上 \(dep\) 最浅的节点,答案显而易见,求 \(\text_{Euler}[pos[u],pos[v]]\) 中的深度最小值是谁,即 RMQ问题。
点击查看代码
const int N=1e5+10,M=2*N;int h[N],e[M],ne[M],w[M],idx;void add(int a,int b,int c){e[idx]=b,ne[idx]=h[a],w[idx]=c,h[a]=idx++;
}int A[N<<1],sz;
int dep[N];
int pos[N];void dfs(int u,int fa){A[++sz]=u;pos[u]=sz;dep[u]=dep[fa]+1;for(int i=h[u];i;i=ne[i]){int v=e[i];if(v==fa) continue;dfs(v,u),A[++sz]=u;}
}int fa[N<<1][20],lg[N<<1];void init(){for(int i=1;i<=(n<<1);i++) fa[i][0]=A[i];for(int i=2;i<=(n<<1);i++) lg[i]=lg[i>>1]+1;for(int i=1;i<=lg[n<<1];i++){for(int j=1;j+(1<<i)-1<=(n<<1);j++){if(dep[fa[i][j-1]]<dep[fa[i+(1<<(j-1))][j-1]]){fa[i][j]=fa[i][j-1];} else fa[i][j]=fa[i+(1<<(j-1))][j-1];}}
}int get(int u,int v){if(pos[u]>pos[v]) swap(u,v);int Log=lg[pos[v]-pos[u]+1];if(dep[fa[pos[u]][Log]]<dep[fa[pos[v]-(1<<Log)+1][Log]]) return fa[pos[u]][Log];else return fa[pos[v]-(1<<Log)+1][Log]
}
DFS序求 \(\text {LCA}\)
定义:
这个是欧拉序的衍生版本(个人理解,长得比较像)。
优点:时间常数少一半,空间常数也少一半,吊打欧拉序。
先给一些定义:
-
DFS序:表示对一棵树进行深度优先搜索得到的结点序列
-
时间戳DFN:表示每个结点在DFS序中的位置。
不妨设 \(dfn_u<dfn_v\)。
结论:在DFS序的 \([dfn_u+1,dfn_v]\) 中的深度最浅的节点即为 \(\text {LCA}_{u,v}\)。
证明:基本思想——分类讨论
- 如果 \(u\) 和 \(v\) 不重链。
令 \(d=\text {LCA}_{u,v}\),则其访问顺序一定是 \(d-u-d-v\),那么 \(d\) 及的祖先一定不在 \([dfn_u,dfn_v]\),此处易证。
那么令 \(d\) 的儿子是 \(p\),则 \(p\) 一定存在与 \([dfn_u,dfn_v]\) 之间。
那我们只需要求 $$[dfn_u,dfn_v]$$ 之间深度最浅的节点,其父亲为 \(\text {LCA}_{u,v}\)。
- 如果 u 和 v 重链。
将查询区间变为 \([dfn_u,dfn_v+1]\) 之间即可。
容易发现:\([dfn_u,dfn_v+1]\) 也适用于情况1。
查询:
点击查看代码
void add(int u,int v){g[u].push_back(v);}int get(int x,int y){return dfn[x]<dfn[y]?x:y;}void dfs(int u,int f){fa[dfn[u]=++dn][0]=f;for(auto it:g[u]) if(it!=f) dfs(it,u);
}int LCA(int u,int v){if(u==v) return u;u=dfn[u],v=dfn[v];if(u>v) swap(u,v);u++;int t=__lg(v-u);return get(fa[u][t],fa[v-(1<<t)+1][t]);
}
预处理:
点击查看代码
for(int i=1;i<=__lg(n);i++){for(int j=1;j+(1<<i)-1<=n;j++){fa[i][j]=get(fa[i-1][j],fa[i-1][j+(1<<i-1)]);}
}