C# 二维数组中相似项组的选择算法

C# 二维数组中相似项组的选择算法,c#,algorithm,C#,Algorithm,有一个2d项目数组(在我的例子中,它们被称为交叉点) 一个特定的项目作为开始。任务是查找直接或间接连接到此项的所有满足特定功能的项 所以基本算法是这样的: private IList<Intersection> SelectGroup ( Intersection start, Func<Intersection, Intersection, bool> select) { List<Intersection> result = new

有一个2d项目数组(在我的例子中,它们被称为
交叉点

一个特定的项目作为开始。任务是查找直接或间接连接到此项的所有满足特定功能的项

所以基本算法是这样的:

private IList<Intersection> SelectGroup (
    Intersection start,
    Func<Intersection, Intersection, bool> select)
{
    List<Intersection> result = new List<Intersection> ();

    Queue<Intersection> source = new Queue<Intersection> ();
    source.Enqueue (start);

    while (source.Any ()) {
        var s = source.Dequeue ();
        result.Add (s);

        foreach (var neighbour in Neighbours (s)) {
            if (select (start, neighbour)
                && !result.Contains (neighbour)
                && !source.Contains (neighbour)) {
                source.Enqueue (neighbour);
            }
        }
    }

    Debug.Assert (result.Distinct ().Count () == result.Count ());
    Debug.Assert (result.All (x => select (x, result.First ())));

    return result;
}

private List<Intersection> Neighbours (IIntersection intersection)
{
    int x = intersection.X;
    int y = intersection.Y;

    List<Intersection> list = new List<Intersection> ();

    if (x > 1) {
        list.Add (GetIntersection (x - 1, y));
    }
    if (y > 1) {
        list.Add (GetIntersection (x, y - 1));
    }
    if (x < Size) {
        list.Add (GetIntersection (x + 1, y));
    }
    if (y < Size) {
        list.Add (GetIntersection (x, y + 1));
    }

    return list;
}
XXX
X.X
XXX
将开始添加到结果列表中。 重复此操作,直到没有修改为止:将数组中满足函数要求且与结果列表中的任何项目接触的每个项目添加到结果列表中

我当前的实现如下所示:

private IList<Intersection> SelectGroup (
    Intersection start,
    Func<Intersection, Intersection, bool> select)
{
    List<Intersection> result = new List<Intersection> ();

    Queue<Intersection> source = new Queue<Intersection> ();
    source.Enqueue (start);

    while (source.Any ()) {
        var s = source.Dequeue ();
        result.Add (s);

        foreach (var neighbour in Neighbours (s)) {
            if (select (start, neighbour)
                && !result.Contains (neighbour)
                && !source.Contains (neighbour)) {
                source.Enqueue (neighbour);
            }
        }
    }

    Debug.Assert (result.Distinct ().Count () == result.Count ());
    Debug.Assert (result.All (x => select (x, result.First ())));

    return result;
}

private List<Intersection> Neighbours (IIntersection intersection)
{
    int x = intersection.X;
    int y = intersection.Y;

    List<Intersection> list = new List<Intersection> ();

    if (x > 1) {
        list.Add (GetIntersection (x - 1, y));
    }
    if (y > 1) {
        list.Add (GetIntersection (x, y - 1));
    }
    if (x < Size) {
        list.Add (GetIntersection (x + 1, y));
    }
    if (y < Size) {
        list.Add (GetIntersection (x, y + 1));
    }

    return list;
}
XXX
X.X
XXX
在本例中,共有两组:一组是由4个项目组成的中心组,另一组是位于左下角的单个项目组。选择组(例如,通过起始项[3,3])返回前者,而使用起始项和唯一返回值[1,4]可以选择后者

例2:

.A.. ..BB A.AA A. …BB A.AA
这次有4组。3 A组未连接,因此它们作为单独的组返回。较大的A组和B组是连接的,但A与B不相关,因此它们作为单独的组返回。

步骤1:微小的更改带来巨大的好处。
简单、即时的改进:您的
结果.Contains
源代码.Contains
成员身份都在列表类型上,因此它们的列表大小将为O(n),效率不高。由于您真的不关心任何特定的顺序,因此我将这两种顺序都更改为
HashSet
,以便进行恒定时间的查找。
请注意,在最坏的情况下,您当前的实现将是O(n^2),这发生在整个数组有效时(在插入最后几个元素时,您将根据网格的整个其余部分检查每个元素)

步骤2:进一步优化
更好的结构更改:保留一个与交叉点数组大小相同的布尔
已访问
数组,每次查看
交叉点
,将其标记为已访问。这样,您就不必每次都检查
result
source
中是否有内容,而且更好的是,您不必重新评估
select
谓词。否则,考虑到如下情况:

private IList<Intersection> SelectGroup (
    Intersection start,
    Func<Intersection, Intersection, bool> select)
{
    List<Intersection> result = new List<Intersection> ();

    Queue<Intersection> source = new Queue<Intersection> ();
    source.Enqueue (start);

    while (source.Any ()) {
        var s = source.Dequeue ();
        result.Add (s);

        foreach (var neighbour in Neighbours (s)) {
            if (select (start, neighbour)
                && !result.Contains (neighbour)
                && !source.Contains (neighbour)) {
                source.Enqueue (neighbour);
            }
        }
    }

    Debug.Assert (result.Distinct ().Count () == result.Count ());
    Debug.Assert (result.All (x => select (x, result.First ())));

    return result;
}

private List<Intersection> Neighbours (IIntersection intersection)
{
    int x = intersection.X;
    int y = intersection.Y;

    List<Intersection> list = new List<Intersection> ();

    if (x > 1) {
        list.Add (GetIntersection (x - 1, y));
    }
    if (y > 1) {
        list.Add (GetIntersection (x, y - 1));
    }
    if (x < Size) {
        list.Add (GetIntersection (x + 1, y));
    }
    if (y < Size) {
        list.Add (GetIntersection (x, y + 1));
    }

    return list;
}
XXX
X.X
XXX
您将对中心点的
选择
进行四次评估,如果价格昂贵,这可能会很糟糕。这边,你的

if (select (start, neighbour)
  && !result.Contains (neighbour)
  && !source.Contains (neighbour))
条件变为:
if(!visted(neighbor)&&select(start,neighbor)
,它在任何给定的交叉口上最多只计算一次
select


此外,如果您这样做,您甚至不需要再进行
结果
包含
散列,因为您不会对它们进行包含检查。

步骤1:为了巨大的好处而进行的微小更改
简单、即时的改进:您的
结果。包含
源代码。包含
成员身份都在列表类型上,因此它们在这些列表的大小上是O(n),不是很有效。因为您真的不关心任何特定的顺序,我会将它们都更改为
哈希集
,以便进行恒定时间的查找。
请注意,在最坏的情况下,您当前的实现将是O(n^2),这发生在整个数组有效时(在插入最后几个元素时,您将根据网格的整个其余部分检查每个元素)

步骤2:进一步优化
更好的结构更改:保留一个与交叉点数组大小相同的布尔
已访问
数组,并且每次查看
交叉点
,都将其标记为已访问。这样,您就不必每次都检查
结果
中是否有内容,而且更好的是,您不必重新评估<代码>选择
谓词。否则,给定如下内容:

private IList<Intersection> SelectGroup (
    Intersection start,
    Func<Intersection, Intersection, bool> select)
{
    List<Intersection> result = new List<Intersection> ();

    Queue<Intersection> source = new Queue<Intersection> ();
    source.Enqueue (start);

    while (source.Any ()) {
        var s = source.Dequeue ();
        result.Add (s);

        foreach (var neighbour in Neighbours (s)) {
            if (select (start, neighbour)
                && !result.Contains (neighbour)
                && !source.Contains (neighbour)) {
                source.Enqueue (neighbour);
            }
        }
    }

    Debug.Assert (result.Distinct ().Count () == result.Count ());
    Debug.Assert (result.All (x => select (x, result.First ())));

    return result;
}

private List<Intersection> Neighbours (IIntersection intersection)
{
    int x = intersection.X;
    int y = intersection.Y;

    List<Intersection> list = new List<Intersection> ();

    if (x > 1) {
        list.Add (GetIntersection (x - 1, y));
    }
    if (y > 1) {
        list.Add (GetIntersection (x, y - 1));
    }
    if (x < Size) {
        list.Add (GetIntersection (x + 1, y));
    }
    if (y < Size) {
        list.Add (GetIntersection (x, y + 1));
    }

    return list;
}
XXX
X.X
XXX
您将对中心点的
选择
进行四次评估,如果价格昂贵,这可能会很糟糕

if (select (start, neighbour)
  && !result.Contains (neighbour)
  && !source.Contains (neighbour))
条件变为:
if(!visted(neighbor)&&select(start,neighbor)
,它在任何给定的交叉口上最多只计算一次
select

此外,如果您这样做,您甚至不需要再进行
结果
包含
散列,因为您不会对它们进行包含检查。

我不太擅长C#但是阅读您的算法描述,我可以给您一些建议:

1/你知道动态规划或贝尔曼算法吗? 其思想是,对于每个迭代,您只会得到最新的结果并继续搜索。

例如:

第一次迭代:x1,x2

第二次迭代:x3,x4

在第三次迭代中,您只需搜索并与x3和x4进行比较。这将节省大量CPU计算和迭代

2/使用HashSet更好地搜索和获取包含的数据

我不太擅长C#但是阅读您的算法描述,我可以给您一些建议:

1/你知道动态规划或贝尔曼算法吗? 其思想是,对于每个迭代,您只会得到最新的结果并继续搜索。

例如:

第一次迭代:x1,x2

第二次迭代:x3,x4

在第三次迭代中,您只需搜索并与x3和x4进行比较。这将节省大量CPU计算和迭代


2/使用HashSet可以更好地搜索和获取包含的数据基于tzaman的回答,我使用了HashSet。由于队列不支持哈希,我发现了一个改进,只需要在结果列表中查找,而不是在结果和源列表中查找:

HashSet<Intersection> result = new HashSet<Intersection> ();
result.Add (start); // need to add first item in advance

Queue<Intersection> source = new Queue<Intersection> ();
source.Enqueue (start);

while (source.Any ()) {
    var s = source.Dequeue ();

    foreach (var neighbour in Neighbours (s)) {
        if (select (start, neighbour) && !result.Contains (neighbour)) {
            result.Add (neighbour); // add immediately to hashset
            source.Enqueue (neighbour);
        }
    }
}
HashSet结果=新的HashSet();
result.Add(start);//需要提前添加第一项
队列源=新队列();
source.Enqueue(启动);
while(source.Any()){
var s=source.Dequeue();
foreach(var邻居中的邻居){
if(选择(开始,相邻)和&!result.Contains(相邻)){
结果。添加(n)