图论
图的存储
1. 直接存边
利用结构体,存每一条边的起点和终点以及它的权。
该方法遍历时效率较低,常用于Kurskal算法中。
例如:
struct node{
int u;
int v;
int w;
}
2. 邻接矩阵
利用二维数组存边,以adj[i][j]
表示从 i 到 j 的边的情况,可以使用 1 表示存在,也可以存储边权。
该方法查询效率高,但是空间开销大且不能存重边,常适用于稠密图。
3. 邻接表存图
适用情况更多,对于一个 i 存储其所链接的节点 j。
//声明
vector<int> adj[NUM];
//存边
adj[i].push_back(j);
4. 链式前向星
本质上是利用链表实现的邻接表
// 定义边的结构体
struct Edge {
int to; // 边的终点
int weight; // 边的权重
int next; // 下一条边的索引
};
Edge edges[MAXM]; // 存储所有边的数组
int head[MAXN]; // head[u] 表示节点 u 的第一条边的索引
int cnt = 0; // 边的计数器
// 添加一条从 u 到 v 的边,权重为 w
void add_edge(int u, int v, int w) {
edges[cnt].to = v; // 记录边的终点
edges[cnt].weight = w; // 记录边的权重
edges[cnt].next = head[u]; // 将新边插入到节点 u 的链表头部
head[u] = cnt++; // 更新节点 u 的第一条边的索引
}
该存图方式的关键点在于通过edges[cnt].next = head[u]
和head[u] = cnt++
实现链表的延伸。遍历也是利用head
和next
实现的。
void traverse(int u) {
for (int i = head[u]; i != -1; i = edges[i].next) {
int v = edges[i].to;
int w = edges[i].weight;
}
}
并查集
并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。
顾名思义,并查集支持两种操作:
合并(Union):合并两个元素所属集合(合并对应的树)
查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合
——oi wiki
以洛谷P3367为例:
int arr[200010];
int find(int x){ //利用find函数寻找父节点并进行状态压缩
if(x==arr[x]){
return arr[x];
}else return arr[x] = find(arr[x]); //不断递归,直到找到父节点为自身的节点,即根节点
}
void join(int x, int y){
int tx = find(x); //找到需加入的两节点的根节点
int ty = find(y);
if(tx != ty){ //如果根节点相同表示在树上,否则更新某一结点的根节点
arr[ty] = tx;
}
}
int main(){
int n, m; cin >> n >> m;
for(int i=1 ; i<=n ; ++i) arr[i]=i;
for(int i=0 ; i<m ; ++i){
int z, x, y;
cin >> z >> x >> y;
if(z==1){
join(x,y);
}else{
if(find(x)==find(y)) cout << 'Y' << '\n';
else cout << 'N' << '\n';
}
}
return 0;
}
最小生成树
解决最小生成树常利用 Prim 或者 Kurskal。
Kurskal
该算法的本质是贪心,更适用于稀疏图。
基本思路:
- 把边按照权值进行排序
- 用贪心的思想优先选取权值较小的边,并依次连接
- 若出现环则跳过此边(用并查集来判断是否存在环)继续搜,直到已经使用的边的数量比总点数少一。
以洛谷P3367为例:
int n, m; // n 个节点,m 条边
int f[5010];
struct line{
int x, y, w;
}l[200010];//直接存边以便排序
bool cmp(line a, line b){ return a.w < b.w; } //用于sort函数的比较函数
int find(int x){
if(x==f[x]) return f[x]; //基于状态压缩的并查集,用于检查是否成环
else return f[x] = find(f[x]);
}
int kruskal(){ //核心代码
int ans=0, cnt=0;
for(int i=0 ; i<m ; ++i){
int rtx = find(l[i].x);
int rty = find(l[i].y);
if(rtx == rty) continue;
else{
++cnt;
ans += l[i].w;
f[rtx] = rty;
}
if(cnt==n-1) return ans;
}
return 0;
}
单源最短路
Dijkstra
- 初始化
-
距离数组:设置起点的距离为0,其他节点的距离为无穷大(∞)。
-
优先队列:将起点及其距离(0)加入优先队列。
-
主循环
-
取出最小距离节点:从优先队列中取出距离最小的节点(当前节点)。
-
遍历邻居:遍历当前节点的所有邻居节点。
-
更新距离:对于每个邻居节点,计算从当前节点到该邻居的新距离。如果新距离小于已知距离,则更新距离,并将该邻居节点及其新距离加入优先队列。
-
-
终止条件
- 当优先队列为空时,算法结束。此时,距离数组中存储的即为从起点到各节点的最短距离。
typedef long long ll;
struct edge{
ll w;
int v;
};
class Compare{
public:
bool operator() (edge a, edge b) const {
return a.w > b.w;
}
};
int n, m, s; // 顶点数n,边数m,源点s
ll dist[100005]; //存储源点到每个点的距离
vector<edge> graph; //存储边权和顶点编号
void dijkstra(){
for(int i=1 ; i<=n ; ++i) dist[i]=LLONG_MAX; //将每个点的值变为无穷大
dist[s] = 0; //初始化源点
priority_queue<edge, vector<edge>, Compare> pq; //建一个最小堆
edge temp = {0,s};
pq.push(temp); //存入源点的信息
while(!pq.empty()){ //形似BFS
ll dis = pq.top().w;
int u = pq.top().v; //对于每一个节点,访问其邻居节点并更新距离
pq.pop();
if(dis > dist[u]) continue;
for(int i=0 ; i<(int)graph[u].size() ; ++i){
auto edge = graph[u][i];
int v = edge.v;
ll w = edge.w;
if(dist[u] + w < dist[v]){
dist[v] = dist[u] + w;
temp = {dist[v],v};
pq.push(temp);
}
}
}
}