Prim
最小生成树问题
n个点,以及n个点之间存在若干条权值不同的边,权值表示建这条边的代价。
现在求应该选哪些边,可以使得n个点连通,并且总花费最小。
定理: n个点,n-1条边的无向连通图,一定是树。
在一个图上生成出的树叫做这个图的一个生成树,边的权值之和最小的生成树叫做这个图的最小生成树。
Prim算法
首先把所有点分为两类:蓝点和白点
已经选 入最小生成树中的点: 蓝点
还没有选入最小生成树中的点:白点
初始化随机一点到最小生成树的距离为0,其它点到最小生成树的距离为inf
每次从白点的集合中挑选出距离已有的最小生成树距离最近的白点,加入最小生成树中
直到所有点都被加入最小生成树
Prim算法模板
int MST_prim(){
int ans=0;
memset(dis,0x3f,sizeof(dis));
memset(found,false,sizeof(found));
dis[1]=0;
while(1){
int u=-1;
for(int i=0;i<=n;i++){
if(!found[i]&&(u==-1||dis[i]<dis[u])){
u=i;
}
}
if(u==-1) break;
found[u]=true;
ans+=dis[u];
for(int i=0;i<=n;i++){
if(!found[i]&&g[u][i]<dis[i]){
dis[i]=g[u][i];
}
}
}
return ans;
}
拓扑排序
拓扑排序算法思想
从DAG图中选择一个没有前驱的顶点并输出
从图中删除该顶点和以该顶点为起点的有向边。
重复1、2,直到当前的DAG为空,或者当前图中不存在有前驱的顶点为止。如果最终图不为空,则必然存在环。
参考代码
int n,m;
vector<int> g[MAXN];
queue<int> q;
int f[MAXN],in[MAXN];
void Topsort(){
for(int i=1;i<=n;i++){
if(in[i]==0){
q.push(i);
}
}
while(!q.empty()){
int t=q.front();
cout<<t<<" ";
q.pop();
for(int i=0;i<g[t].size();i++){
int idx=g[t][i];
if(--in[idx]==0){
q.push(idx);
}
}
}
}
欧拉路径
- 若图
G
G
G中存在这样一条路径,使得它恰通过
G
G
G中每条边一次,则称该路径为
欧拉路径
。若该路径是一个圈,则称为欧拉回路
- 我们定义奇点为度为奇数的点,对于存在欧拉路径的图,有以下两个定理:
- 存在欧拉路径的充要条件:图是连通的,有且只有 2 2 2个奇点
- 存在欧拉回路的充要条件:图是连通的,有 0 0 0个奇点
- 找欧拉路径(回路)的算法是深搜,时间复杂度为深搜的时间复杂度(邻接矩阵: O ( n 2 ) O(n^2) O(n2);邻接表: O ( n + m ) O(n+m) O(n+m)
参考代码
通过上面两个图,发现深搜时先序记录是不对的,只能后序遍历
代码如下:
#include <iostream>
#include <cstdio>
#include <vector>
#define MAXN 5010
using namespace std;
int n,m,start;//start是起点
bool g[MAXN][MAXN];//此图用邻接矩阵存储
int degree[MAXN];//每一个点的度
int path[MAXN],cnt;//记录路径
void find(int x){
for(int i=1;i<=n;i++){
if(g[x][i]){
g[x][i]=g[i][x]=false;//该图是无向图
find(i);
}
}
path[++cnt]=x;//记录当前路径,后序记录路径
//注意:只能后序记录,不能先序记录!先序记录是不对的!
//这是因为,如果遇到分叉口,先序遍历的话,一旦走错,就凉了!
}
int main(){
scanf("%d %d",&n,&m);//n为点的个数,m为边的数量
for(int i=1;i<=m;i++){//输入的时候处理度的问题
int x,y;
scanf("%d %d",&x,&y);
g[x][y]=g[y][x]=true;
degree[x]++;
degree[y]++;
}
for(int i=1;i<=n;i++){
if(degree[i]%2==1){
start=i;//存在奇点,以这个点为起点
break;
}
}
if(start==0)//没有奇点
find(1);//从任何一个点开始均可
else//有两个奇点
find(start);//从一个奇点开始,到另一个奇点结束
for(int i=cnt;i>=1;i--){//如果题目要求你按字典序输出,需要逆序输出
printf("%d ",path[i]);
}
return 0;
}
哈密尔顿回路
- 定义:不重复地走过所有的点,最后回到起点的路径
- 哈密尔顿回路的求法就是搜索(填数问题),没有更好的方法(邻接矩阵: O ( n 2 ) O(n^2) O(n2);邻接表: O ( n + m ) O(n+m) O(n+m))
参考代码
#include <iostream>
using namespace std;
int n, m, g[105][105], ans[105];
bool vis[105];
bool dfs(int x, int t) {
if(t>n) {
if(g[x][1]) return true;
return false;
}
for(int i=1; i<=n; i++) {
if(g[x][i] && !vis[i]) {
ans[t] = i;
vis[i] = true;
if(dfs(i, t+1)) return true;
vis[i] = false;
}
}
return false;
}
int main() {
cin >> n >> m;
int u, v;
for(int i=1; i<=m; i++) {
cin >> u >> v;
g[u][v] = g[v][u] = 1;
}
ans[1] = 1;
vis[1] = true;
if(dfs(1, 2)) {
for(int i=1; i<=n; i++)
cout << ans[i] << ' ';
cout << ans[1];
}
else cout << "NO";
return 0;
}
Kruscal算法
- Kruscal算法将一个连通块当做一个集合
- Kruscal首先将所有边按从小到大顺序排序(一般使用快排),并认为每一个点都是孤立的,分属于 n n n个独立的集合
- 然后按顺序从小到大枚举每一条边。如果这条边连接着两个不同的集合,那么久把这条边加入最小生成树,这两个不同的集合就合并成了一个集合;如果这条边连接的两个点属于同一个集合,就跳过
- 直到选取了 n − 1 n-1 n−1条边结束
参考代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int n,m;
struct edge{
int u,v,cost;
bool operator<(const edge &tmp)const{
if(cost!=tmp.cost)
return cost<tmp.cost;
if(u!=tmp.u)
return u<tmp.u;
return v<tmp.v;
}
}e[20010];
int fa[5010];
void init(){
for(int i=1;i<=n;i++)
fa[i]=i;
}
int find(int x){
if(x==fa[x])
return x;
return fa[x]=find(fa[x]);
}
bool same(int x,int y){
return find(x)==find(y);
}
void unite(int x,int y){
x=find(x);
y=find(y);
if(x==y)
return;
fa[y]=x;
}
int Kruscal(){
int MST=0;
init();
sort(e+1,e+1+m);//e为边集
for(int i=1;i<=m;i++){
if(same(e[i].u,e[i].v)){
unite(e[i].u,e[i].v);
MST+=e[i].cost;
}
}
return MST;
}
int main(){
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d %d %d",&e[i].u,&e[i].v,&e[i].cost);
printf("%d",Kruscal());
return 0;
}
Bellman-Ford算法
-
B
e
l
l
m
a
n
−
F
o
r
d
Bellman-Ford
Bellman−Ford算法解决的是一般情况下的单源最短路径问题,
在这里,边的权值可以为负数
- B e l l m a n − F o r d Bellman-Ford Bellman−Ford算法返回一个布尔值,以表明是否存在一个从原点可以到达的权值为负数的环。
- 如果存在这样的环,算法将告诉我们不存在解决方案。如果没有这种环存在,算法将给出最短路径和它们的权值
- 算法执行 n − 1 n-1 n−1次,每次遍历边集,至少会找到一个点,使得新的最短路径比原来的更小
伪代码
for i from 1 to n-1: //算法需要重复执行n-1次
for j from 1 to m: //遍历每一条边
if dis[e[j].u]+e[j].w<dis[e[j].v]: //这个if操作叫做松弛
dis[e[j].v]=dis[e[j].u]+e[j].w
for j from 1 to m: //再次执行一次算法
if dis[e[j].u]+e[j].w<dis[e[j].v]: //此时所有的点的最短路径都算出来了
return false //如果有边还能松弛,则有负环
return true //没有负环
- 通过伪代码可以看出,
B
e
l
l
m
a
n
−
F
o
r
d
Bellman-Ford
Bellman−Ford的时间复杂度为
O
(
n
m
)
O(nm)
O(nm),完全劣于
D
i
j
k
s
t
r
a
Dijkstra
Dijkstra算法的
O
(
n
2
)
O(n^2)
O(n2)时间复杂度(也劣于优化的
O
(
m
log
n
)
O(m \log n)
O(mlogn)时间复杂度)
模版
#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
int n,m,st,dis[1010];
struct edge{
int to,cost;
};
vector<edge> g[1010];
bool Bellman_Ford(int s){
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
for(int i=1;i<=n-1;i++){
for(int j=1;j<=n;j++){
for(int k=0;k<g[j].size();k++){
//两层循环遍历整个邻接表
edge e=g[j][k];
if(dis[j]+e.cost<dis[e.to])
dis[e.to]=dis[j]+e.cost;
}
}
}
for(int j=1;j<=n;j++){
for(int k=0;k<g[j].size();k++){
//两层循环遍历整个邻接表
edge e=g[j][k];
if(dis[j]+e.cost<dis[e.to])
return false;
}
}
return true;
//时间复杂度为O(nm),劣于Dijkstra
}
int main(){
cin>>n>>m>>st;
for(int i=1;i<=n;i++){
int u,v,w;
cin>>u>>v>>w;
g[u].push_back((edge){v,w});
}
if(Bellman_Ford(st)){
for(int i=1;i<=n;i++)
cout<<dis[i]<<" ";
}
else{
cout<<"No";
}
return 0;
}
SPFA
- S P F A SPFA SPFA是 B e l l m a n − F o r d Bellman-Ford Bellman−Ford算法的一种队列实现,减少了不必要的冗余计算
- 主要思想初始时将起点加入队列。每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若某个相邻的点修改成功,则将其入队。直到队列为空时算法结束
- 这个算法,利用了每个店不会更新次数太多的特点发明的
- S P F A SPFA SPFA在形式上和广度优先搜索非常类似,不同的是广度优先搜索中一个点除了队列就不可能重新进入队列,但是 S P F A SPFA SPFA中一个点可能在出队列之后再次被放入队列,也就是说一个点修稿过其它的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其它的点,这样反复进行下去
- 时间复杂度: O ( k ⋅ m ) O(k \cdot m) O(k⋅m), m m m是边的数量, k k k是一个常数(不好分析),平均值为 2 2 2
核心代码
struct edge {
int nxt, w;
};
queue<int> q;
int dis[1005];
bool exist[1005];//判断一个点是否已经加入了队列中
void SPFA(int s) {
memset(dis, 0x3f, sizeof(dis));
memset(exist, false, sizeof(exist));
dis[s] = 0;
q.push(s);
exist[s] = true;
while(!q.empty()) {
int h = q.front();
q.pop();
exist[h] = false;
for(int i=0; i<g[h].size(); i++) {
edge e = g[h][i];
if(dis[e.nxt]>dis[h]+e.w) {
dis[e.nxt] = dis[h]+e.w;
if(!exist[e.nxt]) {
q.push(e.nxt);
exist[e.nxt] = true;
}
}
}
}
}