算法的主题思想:

  1.优秀的算法因为能够解决实际问题而变得更为重要;

  2.高效算法的代码也可以很简单;

  3.理解某个实现的性能特点是一个挑战;

  4.在解决同一个问题的多种算法之间进行选择时,科学方法是一种重要的工具;

  5.迭代式改进能够让算法的效率越来越高效;

   1. 动态连通性

  动态连接:输入是一对整数对的序列,其中每个整数代表某种类型的对象(或触点),我们将整数对p q 解释为意味着p连接到q。我们假设“连接到”是等价关系:

  对称性:如果p连接到q,则q 连接到p。

  传递性:如果p连接到q且q 连接到r,则p连接到r。
  自反性:p与p连接。
  等价关系将对象划分为多个等价类 或连接的组件。等价类称为连通分量或分量。
  我们的目标是编写一个程序,以从序列中过滤掉多余的对:当程序从输入中读取整数对 p q时,只有在该对点不等价的情况下,才应将对写入到输出中,并且将p连接到q。如果等价,则程序应忽略整数对pq 并继续读取下对。

  

  动态连通性问题的应用:

    1.网络

    2.变量名等价性

    3.数学集合

      在更高的抽象层次上,可以将输入的所有整数看做属于不同的数学集合。

   2. 定义问题

  设计算法的第一个任务就是精确地定义问题。

  算法解决的问题越大,它完成任务所需的时间和空间可能越多。我们不可能预先知道这其间的量化关系,通常只会在发现解决问题很困难,或是代价巨大,或是发现算法所提供的信息比原问题所需要的更加有用时修改问题。例如,连通性问题只要求我们的程序能够判断出给定的整数对是否相连,但并没有要求给出两者之间的通路上的所有连接。这样的要求更难,并会得出另一组不同的算法。

  为了定义和说明问题,先设计一份API  来封装基本操作: 初始化,连接两个触点,查找某个触点的分量 ,判断两个触点是否属于同一分量,分量的数量:

    /// <summary>
/// 动态连通API
/// </summary>
public interface IUnionFind
{
/// <summary>
/// 连接
/// </summary>
/// <param name="p"></param>
/// <param name="q"></param>
void Union(int p, int q); /// <summary>
/// 查找触点 p 的分量标识符
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
int Find(int p); /// <summary>
/// 判断两个触点是否处于同一分量
/// </summary>
/// <param name="p"></param>
/// <param name="q"></param>
/// <returns></returns>
bool Connected(int p, int q); /// <summary>
/// 连通分量的数量
/// </summary>
/// <returns></returns>
int Count();
}

  为解决动态连通性问题设计算法的任务转化为实现这份API:

    1. 定义一种数据结构表示已知的连接;

    2. 基于此数据结构高效的实现API的方法;

  数据结构的性质会直接影响算法的效率。这里,以触点为索引,触点和连接分量都是用 int 值表示,将会使用分量中某个触点的值作为分量的标识符。所以,一开始,每个触点都是只含有自己的分量,分量标识符为触点的值。由此,可以初步实现一部分方法:

    public class FirstUnionFind:IUnionFind
{
private int[] id;//* 分量id 以触点作为索引
private int count;//分量数量 public FirstUnionFind(int n)
{
count = n;
id = new int[n];
for (var i = ; i < n; i++)
{
id[i] = i; // 第一个 i 作为触点,第二个 i 作为触点的值
}
} public int Count()
{
return count;
} public bool Connected(int p, int q)
{
return Find(p) == Find(q);
} public int Find(int p)
{ } public void Union(int p, int q)
{ }
}

  Union-find 的成本模型 是数组的访问次数(无论读写)。

   3. quick-find算法实现

  quick-find 算法是保证当且仅当 id[p] 等于 id[q] 时,p 和 q 是连通的。也就是说,在同一个连通分量中的所有触点在 id[ ] 中的值全部相等。

  所以 Find 方法只需返回 id[q],Union 方法需要先判断 Find(p)  是否等于 Find(q) ,若相等直接返回;若不相等,需要将 q 所在的连通分量中所有触点的 id [ ] 值全部更新为 id[p]。

    public class QuickFindUF: IUnionFind
{
private int[] id;//* 分量id 以触点作为索引
private int count;//分量数量 public QuickFindUF(int n)
{
count = n;
id = new int[n];
for (var i = ; i < n; i++)
{
id[i] = i; // 第一个 i 作为触点,第二个 i 作为触点的值
}
} public int Count()
{
return count;
} public bool Connected(int p, int q)
{
return Find(p) == Find(q);
} public int Find(int p)
{
return id[p];
} public void Union(int p, int q)
{
var pID = Find(p);
var qID = Find(q); if (pID == qID)
return; for (var i = ; i < id.Length; i++)
{
if (id[i] == qID)
id[i] = pID;
}
count--; //连通分量减少
} public void Show()
{
for(var i = ;i<id.Length;i++)
Console.WriteLine("索引:"+i+",值:"+ id[i] );
Console.WriteLine("连通分量数量:"+count);
}
}

  

  算法分析

  Find() 方法只需访问一次数组,所以速度很快。但是对于处理大型问题,每对输入 Union() 方法都需要扫描整个数组。

  每一次归并两个分量的 Union() 方法访问数组的次数在 N+3 到 2N+1 之间。由代码可知,两次 Find 操作访问两次数组,扫描数组会访问N次,改变其中一个分量中所有触点的值需要访问 1 到 N - 1 次(最好情况是该分量中只有一个触点,最坏情况是该分量中有 N - 1个触点),2+N+N-1。

  如果使用quick-find 算法来解决动态连通性问题并且最后只得到一个连通分量,至少需要调用 N-1 次Union() 方法,那么至少需要 (N+3)(N-1) ~ N^2 次访问数组,是平方级别的。

   4. quick-union算法实现

  quick-union 算法重点提高 union 方法的速度,它也是基于相同的数据结构 -- 已触点为索引的 id[ ] 数组,但是 id[ ] 的值是同一分量中另一触点的索引(名称),也可能是自己(根触点)——这种联系成为链接。

  在实现 Find() 方法时,从给定触点,链接到另一个触点,知道到达根触点,即链接指向自己。同时修改 Union() 方法,分别找到 p q 的根触点,将其中一个根触点链接到根触点。

public class QuickUnionUF : IUnionFind
{
private int[] id;
private int count; public QuickUnionUF(int n)
{
count = n;
id = new int[n];
for (var i = ; i < n; i++)
{
id[i] = i; // 第一个 i 作为触点,第二个 i 作为触点的值
}
} public int Count()
{
return count;
} public bool Connected(int p, int q)
{
return Find(p) == Find(q);
} public int Find(int p)
{
while (p != id[p])
p = id[p];
return p;
} public void Union(int p, int q)
{
var pRoot = Find(p);
var qRoot = Find(q); if (pRoot == qRoot)
return; id[pRoot] =qRoot;
count--; //连通分量减少 } public void Show() { for (var i = ; i < id.Length; i++) Console.WriteLine("索引:" + i + ",值:" + id[i]); Console.WriteLine("连通分量数量:" + count); } }

  

  森林表示

  id[ ] 数组用父链接的形式表示一片森林,用节点表示触点。无论从任何触点所对应的节点随着链接查找,最后都将到达含有该节点的根节点。初始化数组之后,每个节点的链接都指向自己。

  算法分析

  定义:一棵树的大小是它的节点的数量。树中一个节点的深度是它到根节点的路径上链接数。树的高度是它的所有节点中的最大深度。

  quick-union 算法比 quick-find 算法更快,因为它对每对输入不需要遍历整个数组。

  分析quick-union 算法的成本比 quick-find 算法的成本要困难,因为quick-union 算法依赖于输入的特点。在最好的情况下,find() 方法只需访问一次数组就可以得到一个触点的分量表示;在最坏情况下,需要 2i+1 次数组访问(i 时触点的深度)。由此得出,该算法解决动态连通性问题,在最佳情况下的运行时间是线性级别,最坏情况下的输入是平方级别。解决了 quick-find 算法中 union() 方法总是线性级别,解决动态连通性问题总是平方级别。

  quick-union 算法中 find() 方法访问数组的次数为 1(到达根节点只需访问一次) 加上 给定触点所对应节点的深度的两倍(while 循环,一次读,一次写)。union() 访问两次 find() ,如果两个触点不在同一分量还需加一次写数组。

  假设输入的整数对是有序的 0-1, 0-2,0-3 等,N-1 对之后N个触点将全部处于相同的集合之中,且得到的树的高度为 N-1。由上可知,对于整数对 0-i , find() 访问数组的次数为 2i + 1,因此,处理 N 对整数对所需的所有访问数组的总次数为 3+5+7+ ......+(2N+1) ~ n^2

  

  

  5.加权 quick-union 算法实现

  简单改动就可以避免 quick-union算法 出现最坏情况。quick-union算法 union 方法是随意将一棵树连接到另一棵树,改为总是将小树连接到大树,这需要记录每一棵树的大小,称为加权quick-union算法。

  代码:

    public class WeightedQuickUnionUF: IUnionFind
{
int[] sz;//以触点为索引的 各个根节点对应的分量树大小
private int[] id;
private int count; public WeightedQuickUnionUF(int n)
{
count = n;
id = new int[n];
sz = new int[n];
for (var i = ; i < n; i++)
{
id[i] = i; // 第一个 i 作为触点,第二个 i 作为触点的值
sz[i] = ;
}
} public int Count()
{
return count;
} public bool Connected(int p, int q)
{
return Find(p) == Find(q);
} public int Find(int p)
{
while (p != id[p])
p = id[p];
return p;
} public void Union(int p, int q)
{
var pRoot = Find(p);
var qRoot = Find(q); if (pRoot == qRoot)
return; if (sz[pRoot] < sz[qRoot])
{
id[pRoot] = qRoot;
}
else
{
id[qRoot] = pRoot;
} count--; //连通分量减少
} public void Show()
{
for (var i = ; i < id.Length; i++)
Console.WriteLine("索引:" + i + ",值:" + id[i]);
Console.WriteLine("连通分量数量:" + count);
}
}

  算法分析

  加权 quicj-union 算法最坏的情况:

  

  这种情况,将要被归并的树的大小总是相等的(且总是 2 的 冥),都含有 2^n 个节点,高度都正好是 n 。当归并两个含有 2^n 个节点的树时,得到的树含有 2 ^ n+1 个节点,高度增加到 n+1 。

  节点大小: 1  2  4  8  2^k = N

  高       度: 0  1  2  3  k

  k = logN

  所以加权 quick-union 算法可以保证对数级别的性能。

  对于 N 个触点,加权 quick-union 算法构造的森林中的任意节点的深度最多为logN。

  对于加权 quick-union 算法 和 N 个触点,在最坏情况下 find,connected 和 union 方法的成本的增长量级为 logN。

  对于动态连通性问题,加权 quick-union 算法 是三种算法中唯一可以用于解决大型问题的算法。加权 quick-union 算法 处理 N 个触点和 M 条连接时最多访问数组 c M logN 次,其中 c 为常数。

  

  三个算法处理一百万个触点运行时间对比:

  

  

  三个算法性能特点:

  6.最优算法 - 路径压缩

  在检查节点的同时将它们直接连接到根节点。

  实现:为 find 方法添加一个循环,将在路径上的所有节点都直接链接到根节点。完全扁平化的树。

  

  研究各种基础问题的基本步骤:

  1. 完整而详细地定义问题,找出解决问题所必须的基本抽象操作并定义一份API。

  2. 简洁地实现一种初级算法,给出一个精心组织的开发用例并使用实际数据作为输入。

  3. 当实现所能解决的问题的最大规模达不到期望时决定改进还是放弃。

  4. 逐步改进实现,通过经验性分析和数学分析验证改进后的效果。

  5. 用更高层次的抽象表示数据结构或算法来设计更高级的改进版本。

  6. 如果可能尽量为最坏情况下的性能提供保证,但在处理普通数据时也要有良好的性能。

  7.在适当的时候将更细致的深入研究留给有经验的研究者并解决下一个问题。

4. union-find算法的更多相关文章

  1. 算法与数据结构基础 - 合并查找(Union Find)

    Union Find算法基础 Union Find算法用于处理集合的合并和查询问题,其定义了两个用于并查集的操作: Find: 确定元素属于哪一个子集,或判断两个元素是否属于同一子集 Union: 将 ...

  2. LeetCode编程训练 - 合并查找(Union Find)

    Union Find算法基础 Union Find算法用于处理集合的合并和查询问题,其定义了两个用于并查集的操作: Find: 确定元素属于哪一个子集,或判断两个元素是否属于同一子集 Union: 将 ...

  3. 最小生成树——Kruskal(克鲁斯卡尔)算法

    [0]README 0.1) 本文总结于 数据结构与算法分析, 源代码均为原创, 旨在 理解 Kruskal(克鲁斯卡尔)算法 的idea 并用 源代码加以实现: 0.2)最小生成树的基础知识,参见 ...

  4. Kruscal算法求图的最小生成树

    Kruscal算法求图的最小生成树 概述   和Prim算法求图的最小生成树一样,Kruscal算法求最小生成树也用到了贪心的思想,只不过前者是贪心地选择点,后者是贪心地选择边.而且在算法的实现中,我 ...

  5. MySQL 优化之 index merge(索引合并)

    深入理解 index merge 是使用索引进行优化的重要基础之一.理解了 index merge 技术,我们才知道应该如何在表上建立索引. 1. 为什么会有index merge 我们的 where ...

  6. MYSQL-联合索引

    深入理解 index merge 是使用索引进行优化的重要基础之一.理解了 index merge 技术,我们才知道应该如何在表上建立索引. 1. 为什么会有index merge 我们的 where ...

  7. MySQL index merge

    深入理解 index merge 是使用索引进行优化的重要基础之一. [ index merge]       当where谓词中存在多个条件(或者join)涉及到多个字段,它们之间进行 AND 或者 ...

  8. MySQL 查询优化之 Index Merge

    MySQL 查询优化之 Index Merge Index Merge Intersection 访问算法 Index Merge Union 访问算法 Index Merge Sort-Union ...

  9. MySQL 优化之 index_merge (索引合并)

    深入理解 index merge 是使用索引进行优化的重要基础之一.理解了 index merge 技术,我们才知道应该如何在表上建立索引. 1. 为什么会有index merge 我们的 where ...

  10. HNCU1323:算法2-1:集合union (线性表)

    http://hncu.acmclub.com/index.php?app=problem_title&id=111&problem_id=1323 题目描述 假设利用两个线性表LA和 ...

随机推荐

  1. 12 . Python3之网络编程

    互联网的本质 两台计算机之间的通信与两个人打电话原理是一样的. # 1. 首先要通过各种物理连接介质连接 # 2. 找准确对方计算机(准确到软件)的位置 # 3. 通过统一的标准(一般子协议)进行数据 ...

  2. Javascript中target事件属性,事件的目标节点的获取。

    window.event.srcElement与window.event.target 都是指向触发事件的元素,它是什么就有什么样的属性 srcElement是事件初始化目标html元素对象引用,因为 ...

  3. Java实现 LeetCode 783 二叉搜索树节点最小距离(遍历)

    783. 二叉搜索树节点最小距离 给定一个二叉搜索树的根节点 root,返回树中任意两节点的差的最小值. 示例: 输入: root = [4,2,6,1,3,null,null] 输出: 1 解释: ...

  4. Java实现蓝桥杯第十一届校内模拟赛

    有不对的地方欢迎大佬们进行评论(ง •_•)ง 多交流才能进步,互相学习,互相进步 蓝桥杯交流群:99979568 欢迎加入 o( ̄▽ ̄)ブ 有一道题我没写,感觉没有必要写上去就是给你多少MB然后求计 ...

  5. Java实现 LeetCode 50 Pow(x,n)

    50. Pow(x, n) 实现 pow(x, n) ,即计算 x 的 n 次幂函数. 示例 1: 输入: 2.00000, 10 输出: 1024.00000 示例 2: 输入: 2.10000, ...

  6. java实现第七届蓝桥杯取球博弈

    题目9.取球博弈 取球博弈 两个人玩取球的游戏. 一共有N个球,每人轮流取球,每次可取集合{n1,n2,n3}中的任何一个数目. 如果无法继续取球,则游戏结束. 此时,持有奇数个球的一方获胜. 如果两 ...

  7. js事件的一些兼容写法

    事件兼容 事件对象的兼容 获取键码兼容 默认行为兼容 阻止事件冒泡兼容 事件监听兼容 ---- 封装 删除事件监听兼容 ---- 封装 事件委托->获取事件源兼容

  8. @Ajax.ActionLink跳转页面的问题解决方案 MVC Ajax不支持问题

    [JavaScriptResult]在客户端执行服务器返回的JavaScript代码当一个内置的Ajax辅助方法请求一个操作方法,该方法会返回一个在客户端执行立即的脚本. public ActionR ...

  9. 恕我直言,我怀疑你并不会用 Java 枚举

    开门见山地说吧,enum(枚举)是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,默认继承自 java.lang.Enum. 为了证明这一点,我们来新建一个枚举 PlayerType: p ...

  10. (二)linux三剑客之awk

    1.awk是什么和上一节的grep有什么区别? 2.awk解决了哪些问题? 3.awk的工作原理? 4.awk的基础用法? 5.awk技术常用[收藏] 1.awk是什么? awk 用于处理文本,gre ...