Genghis Khan the Conqueror----HDU_4126----最小生成树_DFS_DP

本文介绍了一种高效的算法,用于处理在给定图上进行多次边权更新后最小生成树(MST)权值的变化问题。该算法首先使用Prim算法预处理得到初始最小生成树,并通过深度优先搜索(DFS)对树进行编号。接着利用动态规划(DP)优化计算各节点间的最短距离。对于每次询问,算法能够快速更新MST权值并求平均。

题目地址:  题目地址,打开自动链接

这个题目的意思很明确,就是给了你一张图,然后有Q次询问,每次会将一条边的权值变大,注意:是变大哦亲。然后要你求出每改变一次之后的最小生成树的权值,最后权值之和除以询问的次数。首先有一个很简单的想法,那就是每询问一次就求一次最小生成树,最后相加。但是这无疑太耗时间,TLE绝对会在那等着你。那么有木有别的方法可以解决这个问题呢?在经历了近一天的查询和思索之后,总算把这个题A了。

根据某位大牛的博客而来的。

首先是进行一次prime的最小生成树算法,在这个算法的过程中记录下每次加进来的点的路径,也就是说这个点是由哪个加进来的。以及每次加进来的边长,这是为了计算生成树的权值用的,分别是pre和total这两个数组。然后从生成树的0号开始DFS算法,给生成树上的每个点进行一次编号。目的就是为了在计算生成树上点i到其他子树距离的时候用的。这里用到了两个数组,一个是点x是第几个,用到是dfn,另外一个就是ndfn意思是第i个是x。然后就是通过work函数来求出点i到其余子树的最近距离。这里就用到了DP的算法。针对第j个在生成树上的点,i到它的前驱所在树的距离是i与pre[j]的距离以及i到j所在树的距离的较小值,

状态转移方程为:mins[i][ dfn[ pre[ ndfn[j] ] ] ] = min(mins[i][j], mins[i][dfn[pre[ndfn[j]]]]);

这些都做完之后,接下来要处理的就是询问的问题了。首先把还没改动的最小生成树的值加上,然后后面的每次值针对改动的加就可以了。首先如果x,y两点之间的边根本就不在生成树中那么也就每必要进行修改(我的代码里面说的很清楚)。如果在,那么我们就要保证x是y的父亲节点,也就是x在生成树中的序要比y小。如果不是,进行交换。然后就先求出y到x所在树上的最小值,然后再求x到y的所在树上的最小值,取最小的那一个,那样求出来的就是x所在树和y所在树的之间的最短距离了。但是要注意的是这里x和y之间的原来那条边并没有拿进来比较,因为这条边正在被修改,所以是不在考虑范围内的。刚刚求出来的那个最小值在和修改的值比较,取两者较小的那一个然后再减去原来那条边的值除以Q加到我们的SUM中去。这样经历Q次循环,我们就求出了最终结果了,下面附上完美注释版:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

#define MAXN 3005
#define INF 999999999
int n,m,mark;
int map[MAXN][MAXN];
int pre[MAXN];
int total[MAXN];
int dfn[MAXN];
int ndfn[MAXN];
int sons[MAXN];
int mins[MAXN][MAXN];
int ans[MAXN];

bool set[MAXN];

void init()
{
	memset(map,-1,sizeof(map));
	for(int i=0;i<m;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		scanf("%d",&map[x][y]);
		map[y][x] = map[x][y];
	}
	mark = 0;
}

void prime()
{
	memset(set,false,sizeof(set));
	memset(total,-1,sizeof(total));
	pre[0] = -1;
	total[0] = 0;
	bool f = true;
	int x = 0;
	//下面开始求最小生成树
	while(f)
	{
		//put the x in the set
		set[x] = true;


		/*
		 * 这里要找出在把x加入到集合中之后,不在集合中的到集合中的最近的点
		 * 其中total就是表示的不在集合中的i到集合中的某个点最近的距离
		 * 而pre表示的就是某个点i加入到生成树中是由哪个点连过来的
		 * 也就是我们常说的前驱点
		 */


		for(int i=0;i<n;i++)
		{
			//if x and i are connected and i is not in the set
			if(map[x][i] >= 0 && !set[i])
			{
				if(total[i] < 0 || map[x][i] < total[i])
				{
					pre[i] = x;
					total[i] = map[x][i];
				}
			}
		}

		//下面的这个过程就是找最近的过程
		//不懂的话就好好的体会一下
		f = false;
		for(int i=0;i<n;i++)
		{
			if(!set[i] && total[i]>=0)
			{
				if(!f || total[i] < total[x])
				{
					x = i;
					f= true;
				}
			}
		}
	}
}


void dfs(int x)
{
	sons[x] = 1;
	/*
	 * dfn表示的是这个点在这个树中是第几个节点
	 * ndfn表示的是第几个节点是x
	 * 这些都是针对已经在最小树上来说的
	 */
	dfn[x] = mark;
	ndfn[mark++] = x;

	for(int i=0;i<n;i++)
	{
		/*
		 * 如果i是由x连来的话,则进行dfs
		 */
		if(pre[i] == x)
		{
			dfs(i);
			sons[x]+=sons[i];
		}
	}
}

void work()
{
	//这是对mins的初始化
	//mins表示的就是树上第i个节点到第j个节点所在的树上的最小距离
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			mins[dfn[i]][dfn[j]] = map[i][j]>=0 ? map[i][j]:INF;
		}
	}

	for(int i=0;i<n;i++)
	{
		/*
		 * dfn[ pre[ ndfn[j] ] ]表示的这颗树上第j个点的前驱的点的是这个树上的第几个节点
		 * mins[i][ dfn[ pre[ ndfn[j] ] ] ] = min(mins[i][j], mins[i][dfn[fa[ndfn[j]]]]);
		 * 表示树上的第i个到第j的前一个的点所在的最小距离要么是i到j的树的最小距离要么就是自己本身
		 * 因为这是一个联通的,你不知道到底是从哪里走过来是最小的
		 * 这里的思想是比较巧妙的,因为通过pre这个数组,就将和j相连的连在了一起
		 * 然后通过一个巧妙的dp方法,就求出的第i个到第j个的树上的距离求出来了
		 */
		for(int j=n-1;j>0;j--)
		{
			mins[i][ dfn[ pre[ ndfn[j] ] ] ] = min(mins[i][j], mins[i][dfn[pre[ndfn[j]]]]);
		}
	}
}

void query()
{
	memset(ans,-1,sizeof(ans));
	int q;
	double sum = 0;
	for(int i=0;i<n;i++)
	{
		//这里所做的事情就是算出了最小生成树的值
		//后面我们只需要加上改动后所带来的值就OK了
		sum+=total[i];
	}

	scanf("%d",&q);

	//下面开始处理询问
	for(int k=0;k<q;k++)
	{
		int x,y,w;
		scanf("%d%d%d",&x,&y,&w);

		//如果改动的这条边根本不在树上则不用理
		/*
		 * 因为我们已经求得是最小生成树
		 * 而且题目已经告诉我们改动的都是增大的
		 * 不在树中的边本来就已经是大的
		 * 继续增大则也不可能加入到MST中来
		 * 所来我们可以略过
		 */
		if(pre[x] != y && pre[y] != x)
			continue;

		//如果x是由y连着的,也就是y是x的父亲的话,则交换
		if(pre[x] == y)
			swap(x,y);

		if(ans[y] == -1)
		{
			ans[y] = INF;
			for(int i=0;i<n;i++)
			{
				//这里的判断条件就是i和y不在一颗子树上
				//然后找出y到x所在树上的最小距离
				if(x != i && (dfn[i] < dfn[y] || dfn[i] > dfn[y] + sons[y] - 1))
				{
					ans[y] = min(ans[y],mins[dfn[i]][dfn[y]]);
				}
			}
			//下面就是要找出x到y所在子树上的最小距离
			for(int i=1;i<sons[y];i++)
			{
				ans[y] = min(ans[y],mins[dfn[x]][dfn[y]+i]);
			}

			/*
			 * 我们可以看到这里面并没有求到x,y两个点之间的距离
			 * 因为我们要清楚这时候x和y之间的已经在修改了
			 */
		}
		sum+=(min(w,ans[y]) - total[y])*1.0/q;
	}
	printf("%.4lf\n",sum);
}

int main()
{
	while(scanf("%d%d",&n,&m) != EOF && n)
	{
		init();
		prime();
		dfs(0);
		work();
		query();
		//cout<<n<<m<<endl;
	}

	return 0;
}






评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值