当前位置: 首页 > news >正文

算法乱谈

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 算法是一种离线算法,需要使用并查集记录某个结点的祖先结点。

做法如下:

  1. 首先接受输入边(邻接链表)、查询边(存储在另一个邻接链表内)。查询边其实是虚拟加上去的边,为了方便,每次输入查询边的时候,将这个边及其反向边都加入到 queryEdge 数组里。

  2. 然后对其进行一次 DFS 遍历,同时使用 visited 数组进行记录某个结点是否被访问过、parent 记录当前结点的父亲结点。

  3. 其中涉及到了回溯思想,我们每次遍历到某个结点的时候,认为这个结点的根结点就是它本身。让以这个结点为根节点的 DFS 全部遍历完毕了以后,再将这个结点的根节点设置为这个结点的父一级结点。

  4. 回溯的时候,如果以该节点为起点,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}\)

证明:基本思想——分类讨论

  1. 如果 \(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}\)

  1. 如果 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)]);}
}
http://www.hskmm.com/?act=detail&tid=24382

相关文章:

  • 2025 年 9 月习题集
  • C# 代码规范
  • 实用指南:babelfish for postgresql 分析--todo
  • NFC 贴卡自动拨打微信视频电话
  • 10.4
  • 实用指南:d-分离:图模型中的条件独立性判定准则
  • [MCP] 监听资源更新
  • [RAG] 基础知识
  • CF1408F Two Different
  • 数据结构 - 字典树 Trie
  • 激活函数实现
  • 漏洞赏金入门指南:从零开始的实战方法论
  • PMON failed to acquire latch 的报错及sqlplus / as sysdba 无法连接 - 详解
  • 2025CSP-S模拟赛58 比赛总结
  • 精读C++设计模式20 —— 结构型设计模式:桥接模式 - 详解
  • 用纯.NET开发并制作一个智能桌面机器人(六):使用.NET开发一个跨平台功能完善的小智AI客户端
  • 2025/10/4 总结
  • Qt处理Windows平板上摄像头
  • 你必须知道的TCP和UDP核心区别,快速搞懂这两大协议!
  • [swift 外部干涉法 extension]
  • 2025国庆Day3
  • 量子迁移计划启动:应对未来密码学挑战
  • HPE SPP 2025.09.00.00 - HPE 服务器固件、驱动程序和系统软件包
  • 详细介绍:Linux字符设备驱动开发全攻略
  • sql注入和xss漏洞
  • 数学 trick
  • 完整教程:精读C++20设计模式——行为型设计模式:解释器模式
  • js疑惑
  • 关于我
  • 20251004国庆模拟4