公司有一个显示屏连线识别的需求,是感知整个显示屏的分辨率尺寸HxW,根据我司的最小显示单元大小,自动识别显示屏的组成结构,然后使用一条连线顺序把各个显示模板串联起来,根据我司固定的规则希望软件能列出这种连线的所有可能性。
根据我司的显示单元有2*2,2*1和3*1 三种结构,还有固定规则(优先使用2*2,最右边不够则用2*1填充,最下面不够则用3*1填充),识别出各个显示单元的用量和布局, 这个是简单的数学问题,就不发代码了,大家也可以试下无固定规则的随意组合的遍历算法。
然后是单元间的连线。每个单元可与紧挨的上下左右单元进行连线,连线要求所有的单元都能经过而且只能经过一次,不能横跨两个单元进行连线。 看到这个需求的时候很容易就想到了大学数据结构课程里的图路径问题,翻了翻书本,应该是算哈密尔路径问题,但关于这个问题怎么求解倒没有说明,网上能搜到的大多是穷举递归的解法。
今天把我的实现记下来,也希望有缘人能指点一下。
那我们把问题先建个类吧。最新显示单元,编号从左到右从上到下顺序编排,记录单元可以连线的邻接序号。 一条连线(Router)就记录顺序遍历的显示单元编号。
/// <summary>
/// 每一个灯板
/// </summary>
internal class LedBoard
{
/// <summary>
/// 灯板序号
/// </summary>
public int SerialNo;
/// <summary>
/// 相邻的灯板号
/// </summary>
public List<int> ClinkNos;
/// <summary>
/// 横向位置X
/// </summary>
public double ModLeft;
/// <summary>
/// 纵向位置Y
/// </summary>
public double ModTop;
}
/// <summary>
/// 一条灯板连线
/// </summary>
internal class BaseRouter
{
/// <summary>
/// 连线顺序
/// </summary>
public List<int> rPoints;
}
构造整个屏体的显示单元模型方法,我司有固定的规则,就是只有最左和最右可以上下连线,中间的显示单元只能左右连线,git地址里也写了上下左右都可以连线的生成方法。
/// <summary>
/// 简单遍历整行再弯曲
/// </summary>
private void GeneratePuzzleSimple(LedBoard[] gridArr)
{
for (int i = 0; i < gridArr.Length; i++)
{
LedBoard gridItem = gridArr[i];
gridItem.ClinkNos = new List<int>();
int topId = -1, bottomId = -1, leftId = -1, rightId = -1;
int curRow = i / colunmCount;
int curCol = i % colunmCount;
bool isLastCol = false;
if (curRow >= (rowCount - 1))
{
curCol = i - (rowCount - 1) * colunmCount;
curRow = (rowCount - 1);
isLastCol = curCol == (lastRowColumns - 1);
}
else
{
isLastCol = curCol == (colunmCount - 1);
}
if (curRow > 0)
{
//有上方
if (curRow == (rowCount - 1) && lastRowColumns != colunmCount)
{
//两个3*1屏上方接一个屏*2
topId = i - curCol - colunmCount + curCol / 2;
gridItem.ClinkNos.Add(topId);
}
else
{
topId = i - colunmCount;
gridItem.ClinkNos.Add(topId);
}
}
if (curRow < (rowCount - 1))
{
//有下方
if (curRow == (rowCount - 2) && lastRowColumns != colunmCount)
{
//一个屏下方接两个3*1屏
bottomId = i - curCol + colunmCount + curCol * 2;
gridItem.ClinkNos.Add(bottomId);
//最右的屏下方可能只有一个
bottomId += 1;
if (bottomId < totalCount)
{
gridItem.ClinkNos.Add(bottomId);
}
}
else
{
bottomId = i + colunmCount;
gridItem.ClinkNos.Add(bottomId);
}
}
if (curCol > 0)
{
//有左方
leftId = i - 1;
gridItem.ClinkNos.Add(leftId);
}
if (!isLastCol)
{
//有右方
rightId = i + 1;
gridItem.ClinkNos.Add(rightId);
}
//左右有点的,不能穿上下
if (leftId >= 0 && rightId >= 0)
{
gridItem.ClinkNos.Clear();
gridItem.ClinkNos.Add(leftId);
gridItem.ClinkNos.Add(rightId);
}
}
}
而找连线的方法, 是递归方法,每连一个单元入栈一个序号,当发现没有下一个可连的序号(可连序号都在栈里),则终止,如果连线的数量等于单元的总数量,则保存在总连线方案里。
发现递归单元数在100以上,速度就会开始下降,在想能不能并行执行,像GPU加速、C++并行计算那样,而目前只有在第一个点的时候能发起线程,不知道有没有好的修改方法。
/// <summary>
/// 计算起始点的连线数量
/// </summary>
/// <param name="node">起始点</param>
/// <param name="total">整屏的灯板</param>
private void GetPusslzeLines(LedBoard node, LedBoard[] total)
{
if (node == null)
{
return;
}
//Todo 只能在第一个点并行展开计算
Parallel.ForEach(node.ClinkNos, (lineNo) => {
RouterInfo route = new RouterInfo(node.SerialNo, total.Length);
route.AddNo(node.SerialNo);
route.AddNo(lineNo);
try
{
FindNextRoute(route, lineNo, total);
}
catch(InvalidOperationException ex)
{
Debug.WriteLine(ex.Message);
}
route.RemoveNo(lineNo);
});
}
/// <summary>
/// 递归增加节点
/// </summary>
/// <param name="route"></param>
/// <param name="lineNo"></param>
/// <param name="total"></param>
/// <exception cref="InvalidOperationException"></exception>
private void FindNextRoute(RouterInfo route, int lineNo, LedBoard[] total)
{
LedBoard curGrid = total[lineNo];
//遍历当前节点的所有相邻点
foreach (var linkNo in curGrid.ClinkNos)
{
if (!route.ContainNo(linkNo))
{
//增加相邻点
route.AddNo(linkNo);
//完成连线,将连线结果放到routeList集合
if (route.IsFinish())
{
BaseRouter answerNode = new BaseRouter();
int[] answerArr = new int[route.rLen];
route.rPoints.CopyTo(answerArr,0);
answerNode.rPoints = answerArr.ToList();
routeList.Add(answerNode);
if(routeList.Count> ParamStruct.KMaxModuleCount)
{
throw new InvalidOperationException("Exceed Length");
}
}
else
{
FindNextRoute(route, linkNo, total);
}
//移除相邻点
route.RemoveNo(linkNo);
}
}
}
码云地址