图论
- 有向图
- 无向图
- 自环
- 重边
- 简单图
- 度 degree(有多少条边连接了这个节点)
- 入度
- 出度
- 链式前向星:为图的每一个顶点建立一个存储它的邻接顶点的链表
- 路径 path:一个边的序列,且相邻两条边首尾相连
- 简单路径:同一条边只经过一次的路径(简单路径上可能有相同的节点)
- 环 cycle:一个起点和终点相同的路径
- 简单环:是简单路径,还是环
- 连通 connected:如果无向图中两个节点是连通的,则存在从a->b的路径
- 可达:有向图中
- 一个无向图是连通的,当无向图中任何两个节点都连通。
- 有向图没有连通的概念。
- 弱连通:把有向图所有边改成无向的,则该有向图弱连通。
- 强连通:有向图中任意两个节点可达
- 子图 subgraph:在一张图中选择一个边的子集和节点的子集,形成的一张新图,一个图本身是自己的子图。
- 生成子图:节点和原图相同,只删边的子图
- 导出子图:选出节点的子集,并将与该图中两端都有节点的边加入
- 边导出子图:选出边集,再选出相连的节点集合
- 连通子图:对于无向图来说的 连通的子图
- 连通分量 connected component:对于无向图来说,是一个无向图中的极大的连通子图。即再加入一个点,该图 不连通。
- 稀疏图:\(m=θ(n)\);
- 稠密图:\(m=θ(n^2)\);
- 完全图:任何两个节点之间都有边(对于无向图),m=n*(n-1)/2的简单图;
- 路径的长度:路径上边的长度和
- 最短路径:当不存在负环时存在。
- 树:有n个节点,n-1条边的连通无向图。
- 树:一个无向,无环的连通图。
- 树:任意两个节点之间有且只有一条简单路径的简单无向图。
- 森林:一个可能不连通的无向图
- 生成树:对于无向连通图G的一个子图,如果它是包含G中所有顶点的树,那么这个子图称为 G 的生成树。是无向连通图的包含所有顶点的极小联通子图。
- 有根树:在一棵树上选定一个节点作为根,则为有根树;
- 无根树:没根的树。
- 点的深度:当前节点到根节点的距离。根节点深度=0
- 树的深度:根节点儿子的深度的最大值
- 叶子节点:度为0或1的点
- 父亲:在有根树上,某结点到根的路径上的第二个节点即为父亲节点
- 祖先:到根路径上除了本身以外的所有节点
- 儿子/子节点/孩子/child/children:如果u是v的父亲,则v是u的儿子。
- 兄弟:同一个父亲的多个子节点之间为兄弟关系。
- 后代/子孙:儿子和儿子的后代们。
- 子树:删去该节点和该节点父亲之间的点之后该节点所在的连通分量。
- 一种树:一条链。
- 第二种树:菊花。
- 二叉树binary tree:每个节点最多有两个子节点的有根树。
- 左儿子:左边的儿子。
- 右儿子:右边的儿子。
- 真二叉树proper:每个点要么有2个儿子,要么有0个儿子。
- 满二叉树:对于每一层都有2^x个节点。
- 完全二叉树:除了最后一层以外都是满的,最后一排左对齐
- 树的存储方法:vector < int> childs[n];
- 最小生成树:各边权重总和最小的生成树称为最小权重生成树,简称最小生成树
Kruskal算法(贪心)
- 得到一张图之后,首先按照权重从小到大给边排一个序。
- 把图中所有边删掉,再按顺序一条一条地试着向图中加边。
- 加边的同时维护点之间的连通性。如果一条边的两个端点已经连通,则这条边不包含于最小生成树中。
- 重复上述操作,加边!加边!直到向图中加迚去\(|n|-1\)条边为止!
- 维护连通性?只需要并查集就可以啦!
- 有两条边的边权一样?随便加哪一条都可以,毕竟最小生成树不一定是唯一的。
- 时间复杂度 O(nlogn)简洁实用的算法,适用于稀疏图。
- 分为n部分分别找,再将这n部分之间找到最短路合并。
图例 |
描述 |
|
首先第一步,我们有一张图Graph,有若干点和边 |
|
将所有的边的长度排序,用排序的结果作为我们选择边的依据。这里再次体现了贪心算法的思想。资源排序,对局部最优的资源进行选择,排序完成后,我们率先选择了边AD。 |
|
在剩下的变中寻找。我们找到了CE。这里边的权重也是5 |
|
依次类推我们找到了6,7,7,即DF,AB,BE。 |
|
下面继续选择, BC或者EF尽管现在长度为8的边是最小的未选择的边。但是现在他们已经连通了(对于BC可以通过CE,EB来连接,类似的EF可以通过EB,BA,AD,DF来接连)。所以不需要选择他们。类似的BD也已经连通了(这里上图的连通线用红色表示了)。最后就剩下EG和FG了。当然我们选择了EG。最后成功。 |
看看洛谷上的最小生成树模板,用Kruskal写就是:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cctype>
using namespace std;
int cnt, head[2000005];
int n, m;
struct node
{
int to, next, val;
}edge[4000005];
void add(int x, int y, int val_)
{
edge[++cnt].next=x;
edge[cnt].to=y;
edge[cnt].val=val_;
}
int find(int x)
{
return head[x]==x?head[x]:head[x]=find(head[x]);
}
int ans, num;
void k()
{
for(int i=1; i<=cnt; i++)
{
int x=find(edge[i].next);
int y=find(edge[i].to);
if(x==y) continue;
head[x]=y;
ans+=edge[i].val;
if(++num==n-1) break;
}
}
inline int read()
{
int x=0, ch=getchar(), f=1;
while(!isdigit(ch))
{
if(ch == '-') f=-1;
ch=getchar();
}
while(isdigit(ch)) x=x*10+ch-'0', ch=getchar();
return x*f;
}
bool cmp(node x, node y)
{
return x.val < y.val;
}
int main()
{
n=read();
m=read();
for(int i=1; i<=n; i++) head[i]=i;
while(m--)
{
int x, y, z;
x=read();
y=read();
z=read();
add(x, y, z);
}
sort(edge+1, edge+1+cnt, cmp);
k();
printf("%d", ans);
return 0;
}
- 当然,这里(洛谷)不用读入优化也可以
- 链式前向星+Kruskal
Prim算法(贪心+1)
- 将图的顶点分为两部分:处理完了的顶点和还未处理的顶点。
- 任选一个起点,将它加入到处理完了的集合里面。
- 找到连接两个集合的最短边,将它加入到最短路中,并把它的未处理过的顶点标记为端点。
- 实际上,可以给每个顶点记录一个权值,表示和它关联的从处理过的顶点射出的最短边权(初始值设为无穷)。
- 之后每一次只需要遍历一遍未处理的顶点,把其中权值最小点的标记为已处理,接着更新和它邻接的顶点就好啦。
- 重复,直到所有顶点都处理完。
- 时间复杂度\(O(n^2)\)适用于稠密图。
- 和Kruskal相比的话,它并没有用到并查集。也就是选择一个开始找,而不是分为n部分分别找,再选择最短路合并。
图例 |
说明 |
不可选 |
可选 |
已选(Vnew) |
|
此为原始的加权连通图。每条边一侧的数字代表其权值。 |
- |
- |
- |
|
顶点D被任意选为起始点。顶点A、B、E和F通过单条边与D相连。A是距离D最近的顶点,因此将A及对应边AD以高亮表示。 |
C, G |
A, B, E, F |
D |
|
下一个顶点为距离D或A最近的顶点。B距D为9,距A为7,E为15,F为6。因此,F距D或A最近,因此将顶点F与相应边DF以高亮表示。 |
C, G |
B, E, F |
A, D |
|
算法继续重复上面的步骤。距离A为7的顶点B被高亮表示。 |
C |
B, E, G |
A, D, F |
|
在当前情况下,可以在C、E与G间进行选择。C距B为8,E距B为7,G距F为11。E最近,因此将顶点E与相应边BE高亮表示。 |
无 |
C, E, G |
A, D, F, B |
|
这里,可供选择的顶点只有C和G。C距E为5,G距E为9,故选取C,并与边EC一同高亮表示。 |
无 |
C, G |
A, D, F, B, E |
|
顶点G是唯一剩下的顶点,它距F为11,距E为9,E最近,故高亮表示G及相应边EG。 |
无 |
G |
A, D, F, B, E, C |
|
现在,所有顶点均已被选取,图中绿色部分即为连通图的最小生成树。在此例中,最小生成树的权值之和为39。 |
无 |
无 |
A, D, F, B, E, C, G |
Floyd(???我不会,略)
Dijkstra(目光长远的贪心,但不常用)
- 这个算法可以解决单源最短路径问题。类似于 Prim 算法,把顶点分为两类。
- 首先,当然是源点距离源点的距离啦!
- 然后枚举出边,更新它的邻接顶点的最短路径估计值。
- 之后从未确定最短路径的顶点中选出最短路径估计值最小的,将它标记为已确定,并继续更新它的周围顶点。
- 重复上述操作,直到源点到所有顶点的 最短路径都已确定。
- 时间复杂度\(O(n^2)\)。
堆优化的Dijkstra(这个才常用)
- 回顾 Dijkstra 算法,我们常常需要求最短路径估计值的最小值及对应结点很简单,拿一个堆来维护一下,时间复杂度瞬间降为O(nlogn)。类似地,Prim算法也可以使用堆优化。
- 用什么堆呢?平时用STL里的优先队列就好。如果想要卡常数,可以尝试一下配对堆/二项堆/斐波那契堆——在 pb_ds 库中都写好了哦!
单源最短路径【标准版】
#include <bits/stdc++.h>
#define re register
using namespace std;
inline int read()
{
int X=0,w=1; char c=getchar();
while (c<'0'||c>'9') { if (c=='-') w=-1; c=getchar(); }
while (c>='0'&&c<='9') X=(X<<3)+(X<<1)+c-'0',c=getchar();
return X*w;
}
struct Edge { int v,w,nxt; };
Edge e[500010];
int head[100010],cnt=0;
inline void addEdge(int u,int v,int w)
{
e[++cnt].v=v;
e[cnt].w=w;
e[cnt].nxt=head[u];
head[u]=cnt;
}
int n,m,s;
int dis[100010];
struct node
{ //堆节点
int u,d;
bool operator <(const node& rhs) const
{
return d>rhs.d;
}
};
inline void Dijkstra()
{
for (re int i=1;i<=n;i++) dis[i]=2147483647;
dis[s]=0;
priority_queue<node> Q; //堆
Q.push((node){s,0});
while (!Q.empty())
{
node fr=Q.top(); Q.pop();
int u=fr.u,d=fr.d;
if (d!=dis[u]) continue;
for (re int i=head[u];i;i=e[i].nxt)
{
int v=e[i].v,w=e[i].w;
if (dis[u]+w<dis[v])
{
dis[v]=dis[u]+w;
Q.push((node){v,dis[v]});
}
}
}
}
int main()
{
n=read(),m=read(),s=read();
for (re int i=1;i<=m;i++)
{
int X=read(),Y=read(),Z=read();
addEdge(X,Y,Z);
}
Dijkstra();
for (re int i=1;i<=n;i++) printf("%d ",dis[i]);
return 0;
}
- 这不是我写的,不要问我他写了些什么。
- 我才52分。。。
- 但是算法的话是一样的,Dijkstra+堆优化。
- 与Prim相比,Dijkstra更高级,毕竟它是在每一个节点都扫一遍,然后将这些路径都取最优,合并出最短路;而Prim则是一条路走到黑。
单源最短路径【弱化版】
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int head[20000];
int cnt=0;
int n, m, s;
struct node
{
int to, val, next;
}edge[1000000];
void add(int x, int y, int val_)
{
edge[++cnt].next=head[x];
edge[cnt].to=y;
edge[cnt].val=val_;
head[x]=cnt;
}
bool v[20000];
long long d[20000];
int x, y, z;
int main()
{
scanf("%d%d%d", &n, &m, &s);
for(int i=1; i<=n; i++) d[i]=2147483647;
for(int i=0; i<m; i++)
{
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
int c=s;
d[s]=0;
long long minn;
while(!v[c])
{
v[c]=true;
for(int i=head[c]; i!=0; i=edge[i].next)
{
if(!v[edge[i].to] && d[edge[i].to]>d[c]+edge[i].val)
d[edge[i].to]=d[c]+edge[i].val;
}
minn=2147483647;
for(int i=1; i<=n; i++)
{
if(!v[i] && minn>d[i])
{
minn=d[i];
c=i;
}
}
}
for(int i=1; i<=n; i++) printf("%lld ", d[i]);
return 0;
}
Pair
make_pair 函数
令人茫然的集训图论