目录导航:
最小生成树:
对于一个有n个点的图,边一定是大于等于n − 1条的,最小生成树,就是在这些边中选择
n -1 条出来连接所有的n个点,且这n − 1条边的边权之和是所有方案中最小的
再说人话一点:
原图中有n个点,边大于n-1条,将多余的边删掉,使得剩下的n-1条边的边长最小
Prim算法构造最小生成树
大神写的图解很形象:
概括一下就是:贪心算法
每次都选择最小的那条边,并将那条最小的边放入我已经更新好的集合,并用这个集合去更新其他的点
算法思想:
inf=0x3f3f3f3f//很大的一个数
dist[i]<----inf//初始化距离为一个很大的数
for(i=0;i<n;i++)
{
t<----找到集合外距集合最近的点
用t更新其他点到集合的距离
st[t]=true//标记
}
例题:
给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。
给定一张边带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,
n=|V|,m=|E|。
由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树
题目意思也就是上面概括说的意思,如果两点之间不联通,那么也无法生成最小生成树
代码实现:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int n, m;
int g[N][N], dist[N];//稠密图
//邻接矩阵存储所有边
//dist存储其他点到S的距离;
bool st[N];//判断数组
int prim() {
//如果图不连通返回INF, 否则返回res
memset(dist, INF, sizeof dist);//初始化距离为一个很大的数
int res = 0;//结果
for (int i = 0; i < n; i++)
{
int t = -1;//任意一个无实际意义的数即可
for (int j = 1; j <= n; j++)
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
//寻找离集合S最近的点
if (i && dist[t] == INF) return INF;//第一次经过i=0 不执行该if
//判断是否连通,有无最小生成树
if (i) res += dist[t];//如果i>0 且t到集合S的距离为dist[t]
//cout << i << ' ' << res << endl;
st[t] = true;//标记
//更新最新S的权值和
for (int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);//与dijstra算法不同的地方:
}
return res;
}
int main() {
cin >> n >> m;//点数,边数
int u, v, w;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (i == j) g[i][j] = 0;//重边:到自己的距离初始化为0
else g[i][j] = INF;//不为重编:i->j的距离初始化为INF
while (m--)//m条边
{
cin >> u >> v >> w;//u->v的边长为w
g[u][v] = g[v][u] = min(g[u][v], w);//将原先的INF的边长进行覆盖
}
int t = prim();//接收返回值
//临时存储防止执行两次函数导致最后仅返回0
if (t == INF) puts("impossible");//说明无法联通
else cout << t << endl;//输出边长之和
}
与dijstra算法的区别与联系:
可以看到整体与dijstra算法的思路一样,代码也差不多
差别就在于:Dijkstra算法是更新到起始点的距离,Prim是更新到集合S的距离
在代码上体现为:for (int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);
而dijstra算法为:
for (int j = 1; j <= n; j++)//遍历1号点到n号点
dist[j] = min(dist[j], dist[t] + g[t][j]);//1~j点的距离变为了:1~t的距离+t~j的距离
Kruskal算法
关于算法的图解仍是推荐:
概括:还是贪心!
将每条边按照边长进行排序,边长小的在上!那么就得到了某条边到某条边的最短距离,按照次序进行连接,也就是说先连接在上面的,在加边构造的过程中,可能会出现回路:1->2->3->1,这是不被允许的,所以跳过此边不选,最后形成了没有回路的最小生成树!
实现功能:
(1)排序,按照边长排序即可
(2)重点:判断加了这条边是否会形成回路
可以联想到数据结构那一节的连通块,只要证明这两条边的祖先是同一个,那么就来联通了
(3)将该边加入res,最后返回res即可
代码简述:
算法思路:①将所有边按权重从小到大排序
②枚举每条边 a,b,权重是c
if a,b不连通
将这条边加入集合
例题:
给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。
给定一张边带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。
由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树。
代码实现:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 2e5 + 5;
int n, m;
struct Edge //定义结构体,方便进行对应,并排序
{
int u, v, w;
bool operator<(const Edge& a) const //排序的预处理
{
return w < a.w;
}
}edge[N];
int p[N];
int find(int x)//并查集中的寻找相同的祖先
{
return p[x] == x ? x : p[x] = find(p[x]);
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i++)
{
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
edge[i] = { u,v,w };
}
sort(edge, edge + m);//对m条边进行排序
for (int i = 1; i <= n; i++) p[i] = i;//存储节点
int cnt = 0, sum = 0;//cnt:次数 sum:边长的总和
for (int i = 0; i < m; i++)//遍历每条边
{
int a = edge[i].u, b = edge[i].v, w = edge[i].w;
a = find(a);//寻找a的祖宗节点
b = find(b);//寻找b的祖宗节点
if (a != b)//如果祖宗节点不同:证明a,b未连通
{
cnt++;//将a,b连通,次数++
sum += w;//将边长加入sum总和
p[a] = b;//将a节点连接到b节点
}
}
if (cnt < n - 1) puts("impossible");
else printf("%d", sum);
}
核心:
明白未连接前的祖宗节点是不同的,连接后是相同的,如果是1->2->3 3->1 :我们不能连接,为什么?因为它们的祖宗节点是一样的,所以跳过这种情况,这就完美实现了克鲁斯卡尔算法
二分图
重要结论:一个图是二分图,当且仅当图中不含有奇数环
(1)染色法判断二分图
问题:
给定一个n个点m条边的无向图,图中可能存在重边和自环。
请你判断这个图是否是二分图
图解:
概述:将起点染为1,与起点相连的点染为2,依次类推,相邻的点染的颜色不能相同,如果没有冲突,那么该图就是二分图,如果有冲突,那么该图就不是二分图
深度优先搜索做法:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10;
int n, m;
int h[N], e[M], ne[M], idx; //邻接表的存储
int color[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool dfs(int u, int c)
{
color[u] = c; //当前这个点u的颜色是 c
for (int i = h[u]; i != -1; i = ne[i])//链表的遍历
{
int j = e[i];//记录节点
if (!color[j]) //u 的邻接点 j 未被染色
{
dfs(j, 3 - c); // u的颜色如果是1,j就是3-1=2;u的颜色如果是2,j就是3-2=1
}
else if (color[j] == c) return false; //两邻接点染相同颜色
}
return true;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof(h));
while (m--)
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a); // 无向图建两条相反方向的边
}
bool flag = true;
for (int i = 1; i <= n; i++) //遍历图的所有点
if (!color[i])//未染色
{
if (!dfs(i, 1))//将1号节点染为颜色1,并向下进行染色 :如果dfs返回的结果是false,那么就标记flag=false
{
flag = false;
break;
}
}
if (flag) cout << "Yes";
else cout << "No";
}
广度优先搜索做法:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int, int> PII; //first存点编号,second存颜色
const int N = 1e5 + 10, M = 2e5 + 10;
int n, m;
int h[N], e[M], ne[M], idx; //邻接表的存储
int color[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool bfs(int u)
{
queue<PII> q;
q.push({ u,1 });//入队
color[u] = 1; //当前这个点u的颜色是 c
while (q.size()) //队列不空
{
PII t = q.front();
q.pop();//取出队头
int ver = t.first, c = t.second;
for (int i = h[ver]; i != -1; i = ne[i])//遍历链表
{
int j = e[i];
if (!color[j]) //未被染色
{
color[j] = 3 - c;
q.push({ j,3 - c });//入队
}
else if (color[j] == c) return false; //两邻接点染相同颜色
}
}
return true;
}
int main()//与dfs一样的框架
{
cin >> n >> m;
memset(h, -1, sizeof(h));
while (m--)
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a); // 无向图建两条相反方向的边
}
bool flag = true;
for (int i = 1; i <= n; i++) //遍历图的所有点
if (!color[i])
{
if (!bfs(i))
{
flag = false;
break;
}
}
if (flag) cout << "Yes";
else cout << "No";
}
易错:
单链表的存储不一定要按着顺序连在一起的比如:
树形结构 :
在单链表存储的结构:
与dfs的差别就在于单链表的连接顺序,重点是在脑海里构建模拟dfs,bfs
匈牙利算法
算法思路:
具体例子:兄弟我喜欢那个妹子,要不你换一个另外一个喜欢你的妹子,这个妹子让我来
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 510, M = 1e5 + 10;
int n1, n2, m;
int h[N], e[M], ne[M], idx; //邻接表
int match[N];
bool st[N];
void add(int a, int b)//a->b
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool find(int x)
{
for (int i = h[x]; i != -1; i = ne[i]) // 遍历 x 男孩喜欢所有的女孩
{
int j = e[i];
if (!st[j]) // 如果st[j] = true 就不考虑这个女孩
{
st[j] = true; // 标记 j 这个女孩,作用是如果这个女孩有男朋友,我们去找她男朋友有没有可能和别的女孩在一起时,就不需要考虑他现对象 j 了
if (match[j] == 0 || find(match[j]))// 女孩还没有对象或女孩的男朋友可以和其他喜欢的女生在一起
{
match[j] = x; //匹配成功
return true;
}
}
}
return false;
}
int main()
{
cin >> n1 >> n2 >> m;
memset(h, -1, sizeof(h));
while (m--)
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b); //只存一条男生到女生的边就够用啦
}
int res = 0; // 当前匹配的数量
for (int i = 1; i <= n1; i++) // 遍历每个男生
{
memset(st, false, sizeof(st)); //代表女孩还没有考虑过,每个女孩只考虑一次
if (find(i)) res++; // 男生匹配到了
}
cout << res;
}