紫书 例题 11-14 UVa 1279 (动点最小生成树)(详细解释)

这道题写了好久……

在三维空间里面有动的点, 然后求有几次最小生成树。

其实很容易发现, 在最小生成树切换的时候,在这个时候一定有两条边相等

而且等一下更大的那条边在最小生成树中,等一下更小的边不在最小生成树中。

这样的话过了这个时刻,等一下更小的边就会代替等一下更大的边, 从而最小生成树切换

然后我们讨论怎么实现


第一步, 建边

因为这里边的长度是随时间变化的, 所以我们可以把其写成一个二次函数。

那么显然根据两点间距离公式, 长度的平方等于x方向距离的平方+y方向距离的平方+z方向距离的平方

我们假设两个点i j

那么x方向距离的平方为当前两点x坐标相减的平方

当前的x坐标为原来的x坐标加上后来走的距离

也就是(dx是速度, x是原来的x坐标)

(edges[i].dx-edges[j].dx) * t + (edges[i].x-edges[j].x))^2

那么展开就可以得到at^2 + bt + c这样的式子

那么y方向和z方向也一样。

最终的a是三个方向的a加起来, b和c也一样

所以最后结果是这样

#define f1(a) (point[i].a - point[j].a)
#define f2(a) pow(f1(a), 2)

a =f2(dx) + f2(dy) + f2(dz)

b = 2 * (f1(dx) * f1(x) + f1(dy) * f1(y) + f1(dz) * f1(z))

c = f2(x) + f2(y) + f2(z)

然后当前边再记录起点终点。

然后我们根据开始时候的长度从小到大来排序, 等价于at^2 + bt + c中的c。


第二步, 建立事件

事件也就是开头所讲的边长度相等的时刻。

这些事件不一定都能切换最小生成树, 但切换最小生成树一定在这些事件当中。

那么怎么建立事件呢?

就暴力枚举每两条边之间是否会在某一时刻相等, 若相等则是一个事件

那么判断是否相等就是解一元二次方程

数学问题

事件记录的是时间, 新边, 旧边

这里要注意区分新边和旧边

代码里面有注释, 这里不讲了。


第三步, 判断切换次数, 也就是答案

首先先做第一次最小生成树, 后面会切换

然后去判断新边能否代替旧的边

可以的话就更新答案。


具体看代码,注意解方程那一段最好拿纸画一下图像,因为一定要清楚哪个是

旧的边哪个是新的边,不能弄错。

#include<cstdio>
#include<vector>
#include<algorithm>
#include<cmath>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
using namespace std;

const int MAXN = 60;
const int MAXM = MAXN * (MAXN + 1) / 2; 
const double EPS = 1e-8; //题目中最小单位是10^-6 

struct Point
{
	double x, y, z, dx, dy, dz;
	void read()	{ scanf("%lf%lf%lf%lf%lf%lf", &x, &y, &z, &dx, &dy, &dz); }
}point[MAXN];

struct Edge
{
	double a, b, c;
	int u, v;
	bool operator < (const Edge& rhs) const
	{
		return c < rhs.c;
	}
};
vector<Edge> edges;

struct Event
{
	double t;
	int newe, olde;
	bool operator < (const Event& rhs) const
	{
		return t < rhs.t;
	}
};
vector<Event> events;

int f[MAXN], n;

#define f1(a) (point[i].a - point[j].a)
#define f2(a) pow(f1(a), 2)
void make_edge() //建边 
{
	edges.clear();
	REP(i, 0, n)
		REP(j, i + 1, n)
			edges.push_back(Edge{f2(dx) + f2(dy) + f2(dz), 2 * (f1(dx) * f1(x) + f1(dy) * f1(y) + f1(dz) * f1(z)), f2(x) + f2(y) + f2(z), i, j}); //见解析	
	sort(edges.begin(), edges.end());
}

#define d(x) edges[s1].x - edges[s2].x
void make_event()
{
	events.clear();
	REP(i, 0, edges.size())
		REP(j, i + 1, edges.size()) //以下是解二元一次方程 
		{
			int s1 = i, s2 = j;
			if(edges[s1].a < edges[s2].a) swap(s1, s2); //这里的s1的新边,也就是等一下会小于旧边 。规定a > 0方便后面计算 
			
			double a = d(a), b = d(b), c = d(c); //解方程中的移项 
			if(fabs(a) < EPS) //a等于0,也就是说变成一元一次方程 
			{
				if(fabs(b) < EPS) continue; //b=0, 那么c = 0,无解(c是原来边的长度, 肯定不等于0) 
				if(b > 0) swap(s1, s2), b = -b, c = -c; // 规定一下b < 0,可以结合一次函数图像
				if(c > 0) events.push_back(Event{-c / b, s1, s2});//这样再过一会儿函数值小于0,s1 - s2 < 0
				continue;						//所以s1是小的,是新边。 当b < 0时,只有c > 0时t的值才为正
			}
			
			double delta = b * b - 4 * a * c; //判别式 
			if(delta < EPS) continue; //判别式小于0没有实根 
			delta = sqrt(delta); 
			double t1 = (-b - delta) / (2 * a); //图像与x轴的左交点 
			double t2 = (-b + delta) / (2 * a); //图像与x轴的右交点
			if(t1 > 0) events.push_back(Event{t1, s1, s2}); //注意前面规定了 a > 0 如果是左交点, 那么再过一会儿
			if(t2 > 0) events.push_back(Event{t2, s2, s1});	//函数值小于0,也就是s1-s2 < 0, 也就是说s1会更小, 
															// 也就是新的边。同理右交点,s2会更小,那么s2才是新的边 
		}
	sort(events.begin(), events.end());
}

int find(int x)
{
	if(f[x] != x)
		f[x] = find(f[x]);
	return f[x];
}

int solve()
{
	int pos[MAXM], e[MAXN], num = 0; //pos[i]表示第i条边在最小生成树中是第几条边, 值为0表示不在最小生成树中 
	REP(i, 0, n) f[i] = i;          //e[i]表示最小生成树中的第i条边的编号 
	REP(i, 0, edges.size()) pos[i] = 0;
	
	REP(i, 0, edges.size()) //先做第一次最小生成树 
	{
		int u = find(edges[i].u), v = find(edges[i].v);
		if(u != v)
		{
			f[u] = v;
			e[pos[i] = ++num] = i; 
		}
		if(num == n - 1) break;
	}
	
	int ans = 1; //等于1是因为前面做过一次了 
	REP(i, 0, events.size())
		if(pos[events[i].olde] && !pos[events[i].newe]) //旧边在最小生成树且新边不在 
		{
			REP(i, 0, n) f[i] = i;
			int oldpos = pos[events[i].olde];
			REP(j, 1, n)
				if(j != oldpos) //做一次没有旧边的最小生成树 
				{
					int u = find(edges[e[j]].u), v = find(edges[e[j]].v);
					if(u != v) f[u] = v;
				}
			
			int u = find(edges[events[i].newe].u), v = find(edges[events[i].newe].v); 
			if(u != v)  //如果做完后发现新边可以加进去,那么新边就可以代替旧边, 就可以替换 
			{
				ans++;
				pos[events[i].olde] = 0; //替换边 
				pos[events[i].newe] = oldpos;
				e[oldpos] = events[i].newe;
			}
		}
	
	return ans;
}


int main()
{
	int kase = 0;
	while(~scanf("%d", &n))
	{
		REP(i, 0, n) point[i].read();
		make_edge();
		make_event();
		printf("Case %d: %d\n", ++kase, solve());
	}
	return 0;	
} 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本书是国际算法大师乌迪·曼博(Udi Manber)博士撰写的一本享有盛誉的著作。全书共分12章:第1章到第4章为介绍性内容,涉及数学归纳法、算法分析、数据结构等内容;第5章提出了与归纳证明进行类比的算法设计思想;第6章到第9章分别给出了4个领域的算法,如序列和集合的算法、图算法、几何算法、代数和数值算法;第10章涉及归约,也是第11章的序幕,而后者涉及NP完全问题;第12章则介绍了并行算法;最后是部分习题的答案及参考文献。本书的特色有二,旨在提高读者的问题求解能力,使读者能够理解算法设计的过程和思想:一是强调算法设计的创造性过程,注重算法设计背后的创造性思想,而不拘泥于某个具体算法的详细讨论;二是将算法设计类比于定理归纳证明,揭示了算法设计的基本思想和本质。 本书的组织结构清晰且易于理解,强调了创造性,具有浓郁特色,时至今日仍有其巨大的价值,并且适合作为计算机及相关专业算法和高级算法课程的教材。 第1章 引论 第2章 数学归纳法 2.1 引言 2.2 三个简单的例子 2.3 平面内区域的计数 2.4 简单的着色问题 2.5 复杂一些的加法题 2.6 一个简单的不等式 2.7 欧拉公式 2.8 图论中的一个问题 2.9 格雷码 2.10 在图上寻找无重边的路 2.11 数学平均数和几何平均数定理 2.12 循环不变量:将十进制数转换为二进制数 2.13 常见的错误 2.14 小结 第3章 算法分析 3.1 引言 3.2 符号O 3.3 时间与空间复杂度 3.4 求和 3.5 递推关系 3.5.1 巧妙地猜测 3.5.2 分治关系 3.5.3 涉及全部历史的递推关系 3.6 一些有用的证明论据 3.7 小结 第4章 数据结构简介 4.1 引言 4.2 基本数据结构 4.2.1 元素 4.2.2 数组 4.2.3 记录 4.2.4 链表 4.3 树 4.3.1 树的表示 4.3.2 堆 4.3.3 二叉搜索树 4.3.4 AVL树 4.4 散列 4.5 合并?查找问题 4.6 图 4.7 小结 第5章 基于归纳的算法设计 5.1 引言 5.2 多项式求值 5.3 最大导出子图 5.4 寻找一对一映射 5.5 社会名流问题 5.6 分治算法:轮廓问题 5.7 在二叉树中计算平衡因子 5.8 寻找最大连续子序列 5.9 增强归纳假设 5.10 动态规划:背包问题 5.11 常见的错误 5.12 小结 第6章 序列和集合的算法 6.1 引言 6.2 二叉搜索的几种形式 6.2.1 纯二叉搜索 6.2.2 循环序列的二叉搜索 6.2.3 二叉搜索特殊下标 6.2.4 二叉搜索长度未知的序列 6.2.5 重叠子序列问题 6.2.6 解方程 6.3 内插搜索 6.4 排序 6.4.1 桶排序和基数排序 6.4.2 插入排序和选择排序 6.4.3 归并排序 6.4.4 快速排序 6.4.5 堆排序 6.4.6 排序问题的下界 6.5 顺序统计 6.5.1 最大数和最小数 6.5.2 查找第k小的数 6.6 数据压缩 6.7 串匹配 6.8 序列比较 6.9 概率算法 6.9.1 随机数 6.9.2 着色问题 6.9.3 将拉斯维加斯算法变换成确定性算法 6.10 查找众数 6.11 三个展现有趣证明方法的问题 6.11.1 最长递增序列 6.11.2 查找集合中两个最大的元素 6.11.3 计算多重集合的模 6.12 小结 第7章 图算法 7.1 引言 7.2 欧拉图 7.3 图的遍历 7.3.1 深度优先搜索 7.3.2 广度优先搜索 7.4 拓扑排序 7.5 单源最短路径 7.6 最小代价生成树 7.7 全部最短路径 7.8 传递闭包 7.9 图的分解 7.9.1 双连通分支 7.9.2 强连通分支 7.9.3 利用图分解的例子 7.10 匹配 7.10.1 非常稠密图中的完美匹配 7.10.2 偶图匹配 7.11 网络流量 7.12 哈密尔顿旅行 7.12.1 反向归纳 7.12.2 在非常稠密图中找哈密尔顿回路 7.13 小结 第8章 几何算法 8.1 引言 8.2 判定点是否在多边形内部 8.3 构造简单多边形 8.4 凸包 8.4.1 直接方法 8.4.2 礼品包裹算法 8.4.3 Graham扫描算法 8.5 最近点对 8.6 水平线段和竖直线段的交点 8.7 小结 第9章 代数和数值算法 9.1 引言 9.2 求幂运算 9.3 欧几里得算法 9.4 多项式乘法 9.5 矩阵乘法 9.5.1 Winograd算法 9.5.2 Strassen算法 9.5.3 布尔矩阵 9.6 快速傅里叶变换 9.7 小结 第10章 归约 10.1 引言 10.2 归约的例子 10.2.1 简单字符串匹配问题 10.2.2 特殊代表集 10.2.3 关于序列比较的归约 10.2.4 在无向图中寻找三角形 10.3 有关线性规划的归约 10.3.1 概述与定义 10.3.2 归约到线性规划的例子 10.4 下界的归约 10.4.1 寻找简单多边形算法复杂度的下界 10.4.2 关于矩阵的简单归约 10.5 常见的错误 10.6 小结 第11章 NP完全问题 11.1 引言 11.2 多项式时间归约 11.3 非确定性和Cook定理 11.4 NP完全性的证明例子 11.4.1 顶点覆盖问题 11.4.2 支配集问题 11.4.3 3SAT问题 11.4.4 团问题 11.4.5 3着色问题 11.4.6 一般经验 11.4.7 更多的NP完全问题 11.5 处理NP完全问题的技术 11.5.1 回溯法和分枝限界法 11.5.2 确保性能的近似算法 11.6 小结 第12章 并行算法 12.1 引言 12.2 并行计算模型 12.3 共享存储器算法 12.3.1 并行加 12.3.2 寻找最大数的算法 12.3.3 并行前缀问题 12.3.4 在链表中查寻秩 12.3.5 欧拉遍历技术 12.4 互连网络上的算法 12.4.1 阵列上的排序 12.4.2 排序网络 12.4.3 在树中查找第k个最小元素 12.4.4 网孔上的矩阵乘法 12.4.5 超立方体中的路由 12.5 脉动计算 12.5.1 矩阵与向量相乘 12.5.2 卷积问题 12.5.3 序列的比较 12.6 小结 部分习题答案 参考文献
prim算法是一种用于解决最小生成树(Minimum Spanning Tree,MST)问题的算法。最小生成树是指在一个连通加权图中找到一棵包含所有顶点并且边权值之和最小的树。 下面以一个例题图来解释prim算法的过程。假设我们有一个加权图,顶点分别为A、B、C、D、E,边的权值为: AB: 2 AC: 3 AD: 7 BC: 8 BE: 4 CE: 5 DE: 6 首先选择一个任意顶点作为起始点,我们选择A点作为起始点。将A点标记为已访问,然后找到与A点相邻的边中权值最小的边,即AB,将B点标记为已访问。此时A—B这条边就成为了最小生成树的一部分。 接下来,我们需要找到与A、B点相邻的边中权值最小的边。分别是AC和BE,我们选择AC这条边,将C点标记为已访问。此时A—B和A—C这两条边就成为了最小生成树的一部分。 然后,我们找到与A、B、C点相邻的边中权值最小的边。分别是AD和CE,我们选择CE这条边,将E点标记为已访问。此时A—B、A—C和C—E这三条边就成为了最小生成树的一部分。 最后,我们找到与A、B、C、E点相邻的边中权值最小的边,即DE。将D点标记为已访问。此时A—B、A—C、C—E和D—E这四条边就组成了最小生成树。 通过上述过程,我们得到了最小生成树,其包含了ABCED这5个顶点,使得边的权值之和最小。这就是prim算法的过程,通过不断选择最小的边来构建最小生成树
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值