问题描述
我想找到“质心”,即每个房间中可以看到最多其他单元格的位置。例如,“centroid”1 可以看到 500 个其他单元格,“centroid”2 可以看到其他 400 个单元格(不包括在前 500 个中),依此类推(如果有更好的名称,请告诉我)。
我目前正在使用以下代码执行此操作。
public void SetCentroids(CellWalkableState[,] grid)
{
centroids = new List<((int,int),int)>();
List<(int,int)> cellscopy = new List<(int,int)>();
for (int i = 0; i < cells.Count; i++)
{
cellscopy.Add(cells[i]);
}
Debug.Log(DateTime.Now.ToString("o") + " - Setting centroids for room with " + cells.Count + " cells");
var perCellInView = cellscopy.AsParallel().Select(x => (x,Staticclass.FindInView(x,grid))).ToList();
var force_start = perCellInView.First();
Debug.Log(DateTime.Now.ToString("o") + " - got in view");
var perCellInViewOrdered = perCellInView.AsParallel().OrderByDescending(xxx => xxx.Item2.Count);
var force_start_1 = perCellInViewOrdered.First();
Debug.Log(DateTime.Now.ToString("o") + " - sorted");
List<(int,int)> roomCellsAdded = new List<(int,int)>();
while(roomCellsAdded.Count < (cells.Count*0.9))
{
if(cellscopy.Count == 0)
{
Debug.LogError("something is wrong here.");
}
var centroid = perCellInViewOrdered.First().x;
var centroidCells = perCellInViewOrdered.First().Item2;
if(centroidCells.Count == 0)
{
Debug.Log("this shouldnt be happening");
break;
}
roomCellsAdded.AddRange(centroidCells);
centroids.Add((centroid,centroidCells.Count));
Debug.Log(DateTime.Now.ToString("o") + " - added centroids," + roomCellsAdded.Count + " cells in view");
var loopPerCellInView = perCellInView.AsParallel().Where(x => centroids.Select(y => y.Item1).Contains(x.x) == false).Select(x => (x.x,x.Item2.Except(roomCellsAdded).ToList())).ToList();
Debug.Log(DateTime.Now.ToString("o") + " - excluded");
perCellInViewOrdered = loopPerCellInView.AsParallel().OrderByDescending(xxx => xxx.Item2.Count);
Debug.Log(DateTime.Now.ToString("o") + " - resorted");
}
}
public static List<(int,int)> FindInView((int,int) start,CellWalkableState[,] grid)
{
List<(int,int)> visible = new List<(int,int)>() { start };
bool alive = true;
int r = 1;
var length_x = grid.GetLength(0);
var length_y = grid.GetLength(1);
List<(int,int)> searched = new List<(int,int)>();
List<double> angles = new List<double>();
while(alive)
{
//alive = false;
int newR = r;
int count = CountFromr(newR);
var angleInc = 360.0 / count;
var rNexts = Enumerable.Repeat(1,count).ToArray();
for (int i = 0; i < count; i++)
{
var angle = angleInc * i;
if(angles.Contains(angle) == false)
{
angles.Add(angle);
float cos = Mathf.Cos((float)(Mathf.Deg2Rad * angle));
float sin = Mathf.Sin((float)(Mathf.Deg2Rad * angle));
var b = r;
var p = i % (r * 2);
var d = math.sqrt(math.pow(b,2) + math.pow(p,2));
var dScaled = d / r;
bool keepGoing = true;
while(keepGoing)
{
var rCur = dScaled * (rNexts[i]);
var loc = (start.Item1 + Mathf.RoundToInt(rCur * cos),start.Item2 + Mathf.RoundToInt(rCur * sin));
if (searched.Contains(loc) == false)
{
searched.Add(loc);
if (loc.Item1 >= 0 && loc.Item1 < length_x && loc.Item2 >= 0 && loc.Item2 < length_y)
{
if (grid[loc.Item1,loc.Item2] == CellWalkableState.Interactive || grid[loc.Item1,loc.Item2] == CellWalkableState.Walkable)
{
visible.Add(loc);
}
else
{
keepGoing = false;
}
}
else
{
keepGoing = false; // invalid,stop
}
}
else
{
if (visible.Contains(loc) == false)
{
keepGoing = false; // can stop,because we can't see past this place
}
}
if(keepGoing)
{
rNexts[i]++;
}
}
}
}
angles = angles.distinct().ToList();
searched = searched.distinct().ToList();
visible = visible.distinct().ToList();
if(rNexts.All(x => x <= r))
{
alive = false;
}
else
{
r = rNexts.Max();
}
}
return visible;
}
static int CountFromr(int r)
{
return 8 * r;
}
上面代码的“简短”总结是每个位置首先确定它可以看到自己周围的哪些单元格。这变成了一个元组列表 List<((int,List<(int,int)>)>
,其中第一项是位置,第二项是它查看的所有单元格。该主列表按子列表的计数排序,因此具有最多 cell-it-can-vew 的项目排在最前面。这是作为质心添加的,它可以查看的所有单元格都添加到第二个(“已处理”)列表中。形成了一个修改过的“主列表”,现在每个子列表都排除了第二个列表中的任何内容。它会循环执行此操作,直到添加了 90% 的单元格。
一些输出:
2021-04-27T15:24:39.8678545-04:00 - 为具有 7129 个单元格的房间设置质心 2021-04-27T15:45:26.4418515-04:00 - 进入视野 2021-04-27T15:45:26.4578551-04:00 - 排序 2021-04-27T15:45:27.3168517-04:00 - 添加质心,查看 4756 个单元格 2021-04-27T15:45:27.9868523-04:00 - 排除 2021-04-27T15:45:27.9868523-04:00 - 重新使用 2021-04-27T15:45:28.1058514-04:00 - 添加了质心,可查看 6838 个单元格 2021-04-27T15:45:28.2513513-04:00 - 排除 2021-04-27T15:45:28.2513513-04:00 - 重新使用 2021-04-27T15:45:28.2523509-04:00 - 为具有 20671 个单元格的房间设置质心
这对我来说太慢了。任何人都可以建议这样做的替代方法吗?对于所有的细胞,基本上唯一的信息是它们是否“打开”或者是否可以看穿它们(相对于像墙一样的东西)。
解决方法
具有固定整数斜率的近似解
要大幅加速(从房间数量的二次方变为线性),您可以决定只检查每个点的几个整数斜率。这些是可见性的等价类,即如果单元格 x 可以看到沿着这样一条线的单元格 y,并且单元格 y 可以看到沿着同一条线的单元格 z,那么所有 3 个单元格都可以看到彼此。然后你只需要沿着特定的线计算每个“可见性间隔”一次,而不是每个单元。
您可能至少要检查水平、垂直和 45 度对角线斜率。对于水平,从单元格 (1,1) 开始,然后向右移动直到碰到墙,假设在 (5,1)。然后单元格 (1,1),(2,(3,1) 和 (4,1) 都可以沿着这个斜率看到彼此,所以虽然你从 (1,1) 开始,但没有必要对其他 3 个单元重复计算——只需将此列表的副本(或什至是指向它的指针,速度更快)添加到所有 4 个单元的可见性列表中。继续向右行驶,并在碰到非墙壁时立即重复该过程。然后在下一行重新开始。
45 度对角线的能见度检查稍微困难一些,但不是很多,检查水平前进 1 和垂直前进 k 的其他斜坡(反之亦然)大致相同。 (检查一般的斜坡,比如每 2 步向右上 3 步,可能有点棘手。)
如果您使用指针而不是列表副本,对于给定的斜率,这种方法会在每个单元格中花费固定的分摊时间:虽然找到某个单元格的 k 个水平可见邻居需要 O(k) 时间,但这意味着不需要进一步的水平处理需要为他们中的任何一个做。因此,如果您检查恒定数量的斜率(例如,我建议的四个),则处理 n 个单元格的总体时间为 O(n)。相比之下,我认为您当前的方法至少需要 O(nq) 时间,其中 q 是一个单元格可见的单元格平均数。