分析之前的代码,主要是两个问题。
问题1:在一个新圆加入后,要对所有和新圆相关的组都进行一次完整的覆盖算法。
问题2:对每两个圆都要反复求交点,基于问题1上就更严重了。(200+个随机圆,求交点函数调用次数达到6000+)
首先解决比较简单的问题2。在参数中传入两圆的唯一id,存储两圆的交点,下次调用时直接返回。这样就避免了重复计算。(从6000+直降到1000)
计算交点函数修改代码如下:
private Dictionary<int,Dictionary<int,Vector3[]>> intersectionBuffer = new Dictionary<int, Dictionary<int, Vector3[]>>();
void CalIntersection(Vector3 a, Vector3 b, out Vector3 intersec1, out Vector3 intersec2,int aid,int bid)
{
if(intersectionBuffer.ContainsKey(aid) && intersectionBuffer[aid].ContainsKey(bid))
{
Vector3[] rets = intersectionBuffer[aid][bid];
intersec1 = rets[0];
intersec2 = rets[1];
return;
}
//圆a需要在圆b的左下方以方便计算夹角
if (a.x > b.x || a.y > b.y)
{
Vector3 i = a;
a = b;
b = i;
}
float dx = b.x - a.x;
float dy = b.y - a.y;
float dis2 = dx*dx + dy*dy;
float t = Mathf.Atan2(dy, dx);
float t2 = Mathf.Acos(dis2 / (2 * radius *(Mathf.Sqrt(dis2))));
intersec1 = new Vector3(a.x + radius * Mathf.Cos(t + t2), a.y + radius * Mathf.Sin(t + t2), 0);
intersec2 = new Vector3(a.x + radius * Mathf.Cos(t - t2), a.y + radius * Mathf.Sin(t - t2), 0);
if (!intersectionBuffer.ContainsKey(aid))
{
intersectionBuffer[aid] = new Dictionary<int, Vector3[]>();
}
Vector3[] res = new Vector3[2];
res[0] = intersec1;
res[1] = intersec2;
intersectionBuffer[aid][bid] = res;
}
然后解决问题1。在之前的算法中,针对每个目标圆,每组都要完整地进行覆盖算法,直到遇到未被覆盖的交点才返回。因此有大量的圆都被重复计算了。实际上,只需要保存上一次计算完的所有未被覆盖的交点,每次新增新圆,判断旧交点是否在新圆内。再计算新圆和所有旧圆产生的交点,判断是否在某个旧圆内。如果某次添加新圆,消除了所有旧交点,且没有产生新的未覆盖交点,则说明该目标圆完全被覆盖。
新的算法代码如下:
private Dictionary<int,List<Vector3>> uncoveredGroups = new Dictionary<int,List<Vector3>>();
bool IsCovered2(GameObject[] circles)
{
int length = circles.Length;
//两个非目标圆才能产生交点
if (length <= 2) return false;
GameObject baseCircle = circles[0];
GameObject newCircle = circles[length - 1];
if (!uncoveredGroups.ContainsKey(baseCircle.GetInstanceID()))
{
List<Vector3> newUncoveredList = new List<Vector3>();
uncoveredGroups[baseCircle.GetInstanceID()] = newUncoveredList;
}
List<Vector3> uncoveredList = uncoveredGroups[baseCircle.GetInstanceID()];
//如果没有产生过未覆盖交点,则肯定未被覆盖
bool newListTag = uncoveredList.Count == 0;
//判断旧交点是否在新圆内
for (int i = uncoveredList.Count - 1;i >= 0; i--)
{
if (InCircle(uncoveredList[i], newCircle.transform.position))
{
uncoveredList.RemoveAt(i);
}
}
for (int i = length - 2; i >= 0; i--)
{
Vector3 target = circles[i].transform.position;
if (IsIntersecting(target, newCircle.transform.position))
{
Vector3 interSec1;
Vector3 interSec2;
CalIntersection(target, newCircle.transform.position, out interSec1, out interSec2, circles[i].GetInstanceID(), newCircle.GetInstanceID());
if (InCircle(interSec1, baseCircle.transform.position))
{
bool inList = false;
for (int k = 1; k < circles.Length; k++)
{
if (k != i)
{
if (InCircle(interSec1, circles[k].transform.position))
{
inList = true;
break;
}
}
}
//有交点不在任意非目标圆内,加入缓存
if (inList == false)
{
uncoveredList.Add(interSec1);
}
}
if (InCircle(interSec2, baseCircle.transform.position))
{
bool inList = false;
for (int k = 1; k < circles.Length; k++)
{
if (k != i)
{
if (InCircle(interSec2, circles[k].transform.position))
{
inList = true;
break;
}
}
}
if (inList == false)
{
uncoveredList.Add(interSec2);
}
}
}
}
//被覆盖的判断:产生过新交点,且当前没有新交点没被覆盖
return !newListTag && uncoveredList.Count == 0;
}
性能分析:在200+筹码时缓存算法和直接算法耗时基本相当,原因是缓存算法需要计算所有未覆盖交点,而直接算法遇到未覆盖交点就返回了。缓存算法节省的时间正好被多出来的求交点填上了。(200+筹码时缓存算法计算交点次数大致为1400+,而直接算法上面有写是1000+)
但是当筹码数达到一个更高数量级,2000+筹码时,缓存算法的优势就明显体现出来了。耗时仅0.5秒,而直接算法需要3秒多。
所以第二步优化还需要看需要计算的圆的数量而定,毕竟缓存算法还额外耗费了内存。