对于许多问题,我看到推荐的解决方案是使用union-find数据结构。我试着阅读它并思考它是如何实现的(使用C ++)。我目前的理解是它只是一组集合。因此,要查找元素所属的集合,我们需要n*log n
个操作。当我们必须执行union时,我们必须找到需要合并的两个集合并对它们执行set_union
。这对我来说看起来并不十分有效。我对这个数据结构的理解是正确的还是我错过了什么?
答案 0 :(得分:17)
这是很晚的回复,但这可能在stackoverflow上没有得到解答,因为这是搜索union-find的人的最顶层页面,这里是详细的解决方案。
Find-Union是一种非常快速的操作,在接近恒定的时间内执行。 它遵循Jeremie对路径压缩和跟踪集合大小的见解。对每个查找操作本身执行路径压缩,从而采用摊销的lg *(n)时间。 lg *类似于逆Ackerman函数,增长非常慢,很少超过5(至少直到n <2 ^ 65535)。 Union / Merge集合是懒惰的,只需将1个根指向另一个,特别是较小的set的根到较大的set的根,这是在恒定时间内完成的。
中的以下代码public partial class Form1 : Form
{
Random rnd = new Random { };
double xEnd = 0;
double yEnd = 0;
double xOrigin = 30;
double yOrigin = 450;
double xFire;
double yFire;
double xTarget = 500;
double yTarget;
double yTargetEnd;
double xHor = 30;
double yHor = 350;
double xVert = 130;
double yVert = 450;
double fireLine = 750;
double lineLength = 50;
double targetLine = 50;
public Form1()
{
xEnd = xOrigin + lineLength;
yEnd = yOrigin;
xFire = xOrigin + fireLine;
yFire = yOrigin;
InitializeComponent();
}
private void LineDrawEvent(object sender, PaintEventArgs paint)
{
Graphics drawSurface = paint.Graphics;
Pen turretLine = new Pen(Color.Blue);
Pen graphHorizontal = new Pen(Color.Red);
Pen graphVertical = new Pen(Color.Red);
Pen firedLine = new Pen(Color.Blue);
Pen targetLine = new Pen(Color.Black);
float[] dashValues = { 5, 2 };
firedLine.DashPattern = dashValues;
drawSurface.DrawLine(graphVertical, (int)xOrigin, (int)yOrigin, (int)xHor, (int)yHor);
drawSurface.DrawLine(graphHorizontal, (int)xOrigin, (int)yOrigin, (int)xVert, (int)yVert);
drawSurface.DrawLine(firedLine, (int)xOrigin, (int)yOrigin, (int)xFire, (int)yFire);
drawSurface.DrawLine(targetLine, (int)xTarget, (int)yTarget, (int)xTarget, (int)yTargetEnd);
double angleInRadians = ConvertDegsToRads((double)trckBarAngle.Value);
xEnd = xOrigin + lineLength * Math.Cos(angleInRadians / 2.0);
yEnd = yOrigin - lineLength * Math.Sin(angleInRadians / 2.0);
drawSurface.DrawLine(turretLine, (int)xOrigin, (int)yOrigin, (int)xEnd, (int)yEnd);
this.Refresh();
}
private void trckBarAngle_Scroll(object sender, EventArgs e)
{
lblAngle.Text = "Angle is:" + Convert.ToString((double)trckBarAngle.Value / 2.0);
}
private double ConvertDegsToRads(double degrees)
{
return degrees * (Math.PI / 180.0);
}
private void btnFire_Click(object sender, EventArgs e)
{
double angleInDegrees = trckBarAngle.Value;
double angleInRadians = ConvertDegsToRads(angleInDegrees);
xFire = xOrigin + fireLine * Math.Cos(angleInRadians / 2.0);
yFire = yOrigin - fireLine * Math.Sin(angleInRadians / 2.0);
this.Refresh();
}
private void btnRedraw_Click(object sender, EventArgs e)
{
yTargetEnd = yTarget - targetLine;
yTarget = rnd.Next(100, 500);
}
}
答案 1 :(得分:5)
数据结构可以表示为树,其中分支被反转(而不是向下指向,分支向上指向父级 - 并将子项与其父级链接)。
如果我没记错的话,可以(很容易)显示:
该路径压缩(每当您查找集合A的“父”时,您“压缩”该路径,以便以后对这些路径的每次调用将在时间O(1)中提供父将)导致每次呼叫的O(log n)复杂度;
平衡(你大致跟踪每组儿童的数量,以及当你必须“联合”两套时,你制作一个儿童少一个孩子的那个)也每次通话都会导致O(log n)复杂度。
更复杂的证明可以表明,当你结合两种优化时,你得到的平均复杂度是逆Ackermann函数,写成α(n),这是Tarjan在这种结构中的主要发明。
我相信,后来证明,对于某些特定的使用模式,这种复杂性实际上是不变的(尽管对于所有实际目的,ackermann的逆约为4)。根据Union-Find的维基百科页面,在1989年,任何等效数据结构的每次操作的摊余成本显示为Ω(α(n)),证明当前的实现是渐近最优的。
答案 2 :(得分:2)
正确的union-find数据结构在每次查找期间都使用路径压缩。这会使成本摊销,然后每个操作都与ackermann函数的倒数成比例,这基本上使它保持不变(但不完全)。
如果您从头开始实施,那么我建议使用基于树的方法。
答案 3 :(得分:0)
一个简单的联合集结构保持一个数组(元素 - >设置),使得查找设置恒定时间;更新它们是按时间分摊,并且连接列表是不变的。不像上面的一些方法那么快,但是编程很简单,而且足以改善Kruskal的最小生成树算法的Big-O运行时间。