12.22最小生成树算法prim(dij松弛思想,堆优化),kruskal(并查集实现)

最小生成树

p算法是往树里加点,k算法是往树里加边

prim

#include
#include
using namespace std;
#define re register
#define il inline
il int read() {
	re int x = 0, f = 1; char c = getchar();
	while (c < '0' || c>'9') { if (c == '-')f = -1; c = getchar(); }
	while (c >= '0' && c <= '9')x = (x << 3) + (x << 1) + (c ^ 48), c = getchar();
	return x * f;
}
#define inf 123456789
#define maxn 5005
#define maxm 200005
struct edge {
	int v, w, next;
}e[maxm << 1];//因为是无向图,所以一个边被当成两个边用
int head[maxn], dis[maxn], cnt, n, m, tot, now = 1, ans;
bool vis[maxn];//cnt表示边的编号,u是边的起点,v是边的终点
//e为边的数组,head是记录每个点的第一个边的编号
//编号就是每个边的索引,然后edge就是每条边,e数组记录每条边
il void add(int u, int v, int w) {
	e[++cnt].v = v;
	e[cnt].w = w;
	e[cnt].next = head[u];
	head[u] = cnt;
}
il void init() {
	n = read(), m = read();
	for (re int i = 1, u, v, w; i <= m; i++) {
		u = read(), v = read(), w = read();
		add(u, v, w), add(v, u, w);
	}
}
il int prim() {
	for (re int i = 2; i <= n; i++) {
		dis[i] = inf;
	}
	for (re int i = head[1]; i; i = e[i].next) {//i是边的编号,e[i]才是实际的边
		dis[e[i].v] = min(dis[e[i].v], e[i].w);
	}//初始化第一个点到其他可行点的距离
	while (++tot < n) {
		re int minn = inf;
		vis[now] = 1;//如果vis为1,就说明当下这个点已经加入进最小生成树当中
		for (re int i = 1; i <= n; i++) {
			if (!vis[i] && minn > dis[i]) {//在所有还没加入到最小生成树的节点中选一个距离最短的
				minn = dis[i];
				now = i;
			}
		}
		ans += minn;
		for (re int i = head[now]; i; i = e[i].next) {
			re int v = e[i].v;//按当前新加入到最小生成树的点,进行一次松弛操作,更新dis数组
			if(dis[v]>e[i].w&&!vis[v]){
				dis[v] = e[i].w;
			}
		}
	}
	return ans;
}
int main() {
	init();
	cout << prim();
	return 0;
}

思路就是不断扩大最后的最小生成树

dis记录的是各点到最小生成树的距离,每次都在还没有加入到最小生成树里的点里找距离最小的

然后把它加入到最小生成树里,然后再通过新加入的点,利用它所有可以到的点,进行一次松弛操作

每轮迭代都往生成树里加入一个点,那么需要N-1轮迭代;每次迭代中,在dis数组里找还没有访问过的最小节点,找到后进行松弛 

堆优化的PRIM

#include
#include
#include
using namespace std;
struct edge {
	int target, w, next;
}e[400005];
int n, m;
int k = 0, cnt = 0, ans = 0;//表示当前的边的数量,从1开始,编号同样
int head[5005];//下标为节点的编号,数组元素记录为以该点为起点的第一条边
void add(int u, int v, int w) {
	e[++k].target = v;
	e[k].w = w;
	e[k].next = head[u];
	head[u] = k;//记录编号,第一条边的编号为1,如果返回为0,就表示没边了
}
typedef pairpii;
priority_queue, greater>q;
int dis[5005], vis[5005];
void prim() {
	dis[1] = 0;
	q.push(make_pair(0, 1));//前一项表示到最小生成树的距离,后一项表示节点编号
	while (!q.empty() && cnt < n) {//如果是通过前者,则大概率是不连通图,后者就是已经找到
		int d = q.top().first, u = q.top().second;
		q.pop();
		if (vis[u])continue;
		cnt++;//cnt记录当前加入到最小生成树里的节点数量,如果不连通,那么prim只会生成一个联通分图里的最小生成树
		ans += d;//在堆优化的情况下,不会添加到另一个分图里边,cnt也只是一个分图的点的数量
		vis[u] = 1;//访问过u,将u加入到最小生成树里
		for (int i = head[u]; i; i = e[i].next) {//通过此时加入到树里的点,去松弛这个点所连通的其他点
			if (e[i].w < dis[e[i].target]) {
				dis[e[i].target] = e[i].w;
				q.push(make_pair(dis[e[i].target], e[i].target));//只有有效的松弛后,才会被加入到优先队列里
				//类似于SPFA
			}
		}
	}
}
#define inf 1234567
int main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {//只表示循环次数
		int u, v, w;
		cin >> v >> u >> w;
		add(u, v, w);
		add(v, u, w);
	}
	for (int i = 1; i <= n; i++) {
		dis[i] = inf;
	}
	prim();
	if (cnt == n) {
		cout << ans;
	}
	else {
		cout << "orz";
	}
	return 0;
}

链式前向星

实际上就是用结构体记录边,边里保留目标点,边的权重,下一条边的编号,然后用一个数组记录实际的所有边。边的编号区分每条边,并可以进行访问边

然后用head数组记录点,下标是点的编号,数组元素是该点的第一条边的编号;

#include
#include
using namespace std;
const int maxm = 200005;
const int maxn = 5005;
struct edge {
	int target;
	int w;
	int ne;
}e[maxm << 1];
int head[maxn], m, n;
int cnt = 0;//表示边的编号
void add(int u, int v, int w) {
	e[++cnt].target = v;
	e[cnt].w = w;
	e[cnt].ne = head[u];
	head[u] = cnt;
}
int main() {
	cin >> n >> m;
	for (int i = 0; i < n; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		add(u, v, w), add(v, u, w);
	}

	return 0;
}
#include
#include
using namespace std;
const int maxm = 200005;
const int maxn = 5005;
struct edge {
	int target;
	int w;
	int ne;
}e[maxm << 1];
int head[maxn], m, n;
int cnt = 0;//表示边的编号
void add(int u, int v, int w) {
	e[++cnt].target = v;
	e[cnt].w = w;
	e[cnt].ne = head[u];
	head[u] = cnt;
}
#define inf 99999999
int dis[maxn];
bool vis[maxn];
int main() {
	cin >> n >> m;
	for (int i = 0; i < m; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		add(u, v, w), add(v, u, w);
	}
	for (int i = 1; i <= n; i++) {
		dis[i] = inf;
	}//初始化dis数组
	int cur = 1, ans = 0;//cur表示当前新加入到最小生成树的节点编号
	dis[cur] = 0;
	vis[cur] = 1;
	for (int i = head[1]; i; i = e[i].ne) {//i表示边的编号,e[i]才代表实际的边
		int t = e[i].target;
		dis[t] = min(dis[t], e[i].w);
	}//将一号节点放入最小生成树中,并更新dis数组
	for (int i = 1; i <= n - 1; i++) {
		int mind = inf;
		for (int j = 1; j <= n; j++) {
			if (!vis[j] && dis[j] < mind) {
				cur = j;
				mind = dis[j];
			}
		}//找到没访问过的最小dis点
		if (mind == inf) {//优化一下,就是如果此时要加入到最小生成树的点到其的距离为初始化距离,就说明此最小生成树已经加不了点了,就说明是不连通图
			cout << "orz";
			return 0;
		}
		ans += mind;
		vis[cur] = 1;
		for (int j = head[cur]; j; j = e[j].ne) {
			int t = e[j].target;
			dis[t] = min(dis[t], e[j].w);//依据新加入树的点,更新dis数组,即其他点到最小生成树的距离
			//如果要做改进,就是在还没有访问到的点里更新,不然就是树里面自己更新,代表的含义就不是各个点加入到最小生成树里时的距离
		}
	}
	cout << ans;
	//if (ans >= inf) { cout << "orz"; }//由于迭代n-1轮,且每次都会确定好一个点,所以无论如何也都会在dis上确定所有的数据
	如果有数据依然保持inf的状态,就说明没有边的target是它,即这个点和其他所有点,到最小生成树的距离都是inf
	//else {
	//	cout << ans;
	//}
	return 0;
}

Prim算法的正确性可以通过以下证明来说明:

假设存在一个最小生成树T,其中对于任意一个顶点v,连接到该顶点的所有边中的一条最短边(v, vj)不属于T。我们将这条边添加到T中,得到一个新的图T'。

由于T是一个最小生成树,根据贪心选择的性质,T中包含了连接T中节点和不在T中节点的最小边。那么(v, vj)作为连接到顶点v的最短边,必然是连接T中节点和不在T中节点的最小边之一。因此,将(v, vj)加入T后,T'仍然是连通的。

接下来,我们来考虑T'的权值。根据假设,(v, vj)不属于T,那么T的权值必然小于T'的权值。但根据Prim算法的性质,Prim算法选择的是连接到T中节点的最小边,即权值最小的边。这就意味着在生成T时,Prim算法也会选择(v, vj)作为连接到顶点v的最短边,与假设矛盾。

因此,假设不成立,对于任意一个顶点v,连接到该顶点的所有边中的一条最短边必然属于最小生成树。这证明了Prim算法的正确性。

证明:Prim算法之所以是正确的,主要基于一个判断:对于任意一个顶点v,连接到该顶点的所有边中的一条最短边(v, vj)必然属于最小生成树(即任意一个属于最小生成树的连通子图,从外部连接到该连通子图的所有边中的一条最短边必然属于最小生成树

证明:刚刚有提到:如果某个连通图属于最小生成树,那么所有从外部连接到该连通图的边中的一条最短的边必然属于最小生成树。所以不难发现,当最小生成树被拆分成彼此独立的若干个连通分量的时候,所有能够连接任意两个连通分量的边中的一条最短边必然属于最小生成树

K算法

K算法的思路就是排序,然后从小的开始遍历,不断去在生成树里添加能够增大生成树的最小边,遍历完所有的边,如果满足条件的边的数量达到了n-1,就提前退出;

需要通过并查集的数据结构来实现

kruskal主要思路:
  1. 输入边,用结构体储存
  2. 用结构体快排以边比较从小到大快排
  3. 建一个并查集,并初始化并查集(并查集代表两个点有没有在同一个树里面)
#include
#include
using namespace std;
struct edg {
	int u, v, w;
}edge[200005];
int fa[5005], n, m, ans, eu, ev, cnt;
bool cmp(edg a, edg b) {
	return a.w < b.w;
}
int find(int x) {
	while (x != fa[x]) {
		x = fa[x] = fa[fa[x]];
	}
	return x;
}
void k() {
	sort(edge, edge + m, cmp);
	for (int i = 0; i < m; i++) {
		eu = find(edge[i].u), ev = find(edge[i].v);//得到每条边所连通的起点与终点
		//即判断这条边是否能扩大最小连通图
		//如果能的话,那么说明此时这条边所连通的两点在两个连通分量里,即两个点在并查集里的根节点不同
		//并查集即保证从一开始各点是相互独立的,然后随着边的不断加入,点与点之间不断连通
		//如果不能,就说明两个点在并查集里的根节点相同,即在同一个连通分量里
		//而由于遍历是从小到大,现在能连通,说明这两点之间就已经有了更小的通路来实现
		if (eu == ev) {//eu==ev说明这两点已经连通,即这两点都已经加入到了最小生成树里
			continue;
		}
		ans += edge[i].w;
		fa[ev] = eu;
		if (++cnt == n - 1) {
			break;
		}
	}
}
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		fa[i] = i;
	}
	for (int i = 0; i < m; i++) {
		cin >> edge[i].u >> edge[i].v >> edge[i].w;
	}
	k();
	cout << ans;
	return 0;
}
#include
#include
#include
using namespace std;
struct edge {
	int target, start, w;
}e[200005];
bool cmp(edge a, edge b) {
	return a.w < b.w;
}
int n, m, cnt = 0, ans = 0;//cnt意味着整个图里此时沟通的边的数量,不局限于单个的联通子图
int fa[5005];//构建并查集
int find(int x) {
	while (x != fa[x]) {
		x = fa[x] = fa[fa[x]];//在查询时进行压缩,缩短路径
	}
	return x;
}
void k() {
	sort(e + 1, e + m + 1, cmp);
	for (int i = 1; i <= m; i++) {
		int ft = find(e[i].target), fs = find(e[i].start);
		if (ft == fs)continue;//说明这两个点已经连通,不需要再加这条边,不然会构成回路
		ans += e[i].w;
		fa[ft] = fs;//合并并查集,意味着两个联通子图的合并与联通
		if (++cnt == n - 1)break;//再加一条就会产生回路
	}
}
int main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		cin >> e[i].start >> e[i].target >> e[i].w;
	}
	for (int i = 1; i <= n; i++) {
		fa[i] = i;//初始化,此时并查集里的每个点都只是自己,即都还没连通,是一个个的孤点
	}
	k();
	if (cnt == n - 1) {
		cout << ans;
	}
	else {
		//cout << cnt << endl;
		cout << "orz";
	}
	return 0;
}

树其实就是不包含回路的连通无向图。

树的特性:
1)一棵树中的任意两个结点有且仅有唯一的一条路径连通;
2)一棵树如果有nn个结点,则它一定有n−1n−1条边;
3)在一棵树中加一条边将会构成一个回路

并查集

并查集是来查找祖先的,初始化时每个点的祖先指向自己

查找就是不断往上走,直到找到祖先为止

int find(int x) {
	while (x != fa[x]) {//由于初始化时每个点的父节点为自己,所以真正的父节点的父亲节点还是自己,即x==fa[x]
		//而只要x!=fa[x],就说明x还不是父节点
		x = fa[x];//通过该语句继续向上
	}
}

合并操作 

fa[find(a)] = find(b);
//find(b)找到b的父节点编号
//find(a)找到a的父节点编号,然后让a的父节点为b的根节点,即把a的一族归并到b里,即合并操作

 查找的路径压缩优化

你可能感兴趣的:(算法,图论,算法)