目录导航
题目链接:
https://wenku.baidu.com/view/d8253e24f90f76c660371ac2.html?from=search
问题描述:
XX大学城离市中心比较远,因此占地面积巨大,因而XX市团委准备充分利用资源,在大学城举办定向越野比赛,但规则与普通定向越野不同,每个队被要求从某个起点出发最后到达终点,只要是地图上每个标注的点都可以走,经过一个点时必须在打卡器上打卡做记录,记录该点的打卡器所在位置的海拔高度,高度用一个非负整数来度量,该数将会被保存在卡中。最后到达终点时,该队的成绩就为卡中记录的最大数与最小数之差,差最小的队伍将摘取桂冠。
ZZ和他的同学也参加了这次比赛,拿到地图后,他们想要迅速的找到一条最佳路线以确保获得冠军。
PS:其实光脑子好使能算出最佳路线还不够,还得能跑,但是我们假设ZZ他们队个个都是SUPERMAN,只要你能帮助他们找到最佳路线,它们就一定是冠军。
输入:
由多组数据组成,输入文件以EOF结尾每组数据的第一行包含一个 正整数n,表示校园地图上共有n*n被标注的点(n<=100),接下来n行每行有n个非负整数Ai, j,表示该点打卡器所在位置高度(Ai, j<=200),ZZ和他的同学从(1,1) 出发,目的地为(n,n)
输出:
每组数据对应一行输出,包含一个整数,及最小的高度差的值
输入样本:
5
1 1 3 6 8
1 2 2 5 5
4 4 0 3 3
8 0 2 2 4
4 3 0 3 1
输出样本:
3
Tips:
最佳路线为
(1, 1)→(1,2)→(2,2)→(2, 3)→(3,3)→(4, 3)→(4,4)→(5, 4)→(5, 5)
路线上最高高度为3,最低高度为0,所以答案为3.当然,最佳路线可能不止一条。
解法1:遍历排序
把所有路线都遍历出来,然后根据路线各自的最大高度差值(最大高度差 - 最小高度差)自小到大排序得出【最小的高度差的值】;
从起点开始遍历
起点(1,1)的下一步有两种可能:(1,2)或者(2,1),
以此类推,像烟花那样除了来时的方向外有3个可能方向;
所以【下一步】的排除条件有:
1.【下一步】必须在给定的坐标范围内,就是符合值域 x 和 y ∈ [ 1 , n ]
2.【下一步】必须不在已走过的路线内
3.【下一步】不能连续朝同一个方向拐弯两次
4.路线的起点和终点必须与题目一致
代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
static void Main(string[] args)
{
List<string> readLineStrings = new List<string> {// 方便测试
"5",
// 原题
"1 1 3 6 8",
"1 2 2 5 5",
"4 4 0 3 3",
"8 0 2 2 4",
"4 3 0 3 1",
// 中间有一层全排除
//"1 1 3 6 8",
//"1 2 2 8 5",
//"4 4 8 3 3",
//"8 8 2 2 4",
//"8 3 0 3 1",
// 排除开头
//"7 1 3 6 8",
//"1 2 2 5 5",
//"4 4 0 3 3",
//"8 0 2 2 4",
//"4 3 0 3 1",
// 排除结尾
//"1 1 3 6 8",
//"1 2 2 5 5",
//"4 4 0 3 3",
//"8 0 2 2 4",
//"4 3 0 3 7",
// 分层式排除
//"0 0 3 6 8",
//"1 0 7 7 7",
//"4 7 0 0 3",
//"7 0 2 0 4",
//"4 3 0 0 0",
// 分层式排除
//"0 0 0 6 8",
//"1 7 0 0 7",
//"7 0 0 7 3",
//"0 0 7 0 4",
//"4 0 0 0 0",
};
int n = int.Parse(readLineStrings[0]);
List<int> intKinds = new List<int>();// 高度值的种类
Dictionary<Point, int> coordinates = new Dictionary<Point, int>();// 原接收输入的值(还原是个立体坐标系)
string[] strList = null;
for (int y = 1; y <= n; y++)
for (int x = 1; x <= n; x++)
{
strList = readLineStrings[y].Split();
int z = int.Parse(strList[x - 1]);
coordinates.Add(new Point(x, y), z);
if (!intKinds.Contains(z)) // 收集不同的高度值
intKinds.Add(z);
}
intKinds.Sort((int i1, int i2) => { return i2 - i1; });// 自大到小排序不同的高度值
List<List<Point>> lines = new List<List<Point>> { new List<Point> { new Point(1, 1) } };// 穷举所有路线
int newLineCount = 0;
do
{
newLineCount = 0;
for (int i = lines.Count - 1; i >= 0; i--)
{
List<Point> line = lines[i];
Point lastPoint = line.Last();
if (line.Last() == new Point(n, n)) continue;// 跳过已到终点的路线
Point upPoint = new Point(lastPoint.X, lastPoint.Y - 1);
Point downPoint = new Point(lastPoint.X, lastPoint.Y + 1);
Point leftPoint = new Point(lastPoint.X - 1, lastPoint.Y);
Point rightPoint = new Point(lastPoint.X + 1, lastPoint.Y);
List<Point> nextPoints = new List<Point> { upPoint, downPoint, leftPoint, rightPoint };
foreach (Point nextPoint in nextPoints)
{
Point nextUpPoint = new Point(nextPoint.X, nextPoint.Y - 1);
Point nextDownPoint = new Point(nextPoint.X, nextPoint.Y + 1);
Point nextLeftPoint = new Point(nextPoint.X - 1, nextPoint.Y);
Point nextRightPoint = new Point(nextPoint.X + 1, nextPoint.Y);
List<Point> nextNextPoints = new List<Point> { nextUpPoint, nextDownPoint, nextLeftPoint, nextRightPoint };
if (!nextNextPoints.Any((nextNextPoint) =>
{
return nextNextPoint != lastPoint && line.Contains(nextNextPoint); // 下下一步不能重合原路线
})
&& coordinates.ContainsKey(nextPoint) // 下一步必须在路线内
&& !line.Contains(nextPoint) // 防止路线逆行
)
{
List<Point> newLine = new List<Point>(line) { nextPoint };
lines.Add(newLine);
newLineCount++;
}
}
if (newLineCount > 0)
{
lines.RemoveAll((removeLine) =>
{
if (removeLine.Count != line.Count || removeLine.Last() == new Point(n, n)) return false;
for (int j = 0; j < line.Count; j++)
if (removeLine[j] != line[j]) return false;
return true;
});
}
}
} while (newLineCount > 0);
lines.RemoveAll((removeLine) => { return removeLine.Last() != new Point(n, n); });// 去除不在终点结束的路线
SortedDictionary<int, List<int[]>> diffCombineList = new SortedDictionary<int, List<int[]>>();// 高度差值列表<高度差,对应高度差组合>
int startHeight = coordinates[new Point(1, 1)];// 起点高度
int finHeight = coordinates[new Point(n, n)];// 终点高度
// 当存在起点高度等于终点高度 & 这个高度值的数量大于等于最短路线的步数2n-1时,可以添加差值为0的组合
if (startHeight == finHeight && coordinates.Count((coordinate) => { return coordinate.Value == startHeight; }) >= 2 * n - 1)
diffCombineList.Add(0, new List<int[]> { new int[] { startHeight, finHeight } });
for (int i = 0; i < intKinds.Count; i++)
for (int j = i + 1; j < intKinds.Count; j++)
if (intKinds[i] >= startHeight && intKinds[j] <= startHeight && intKinds[i] >= finHeight && intKinds[j] <= finHeight)// ∵起点和终点是必经之地∴起点&终点必定在值域范围内
{
int diff = intKinds[i] - intKinds[j];// 由于高度值已排序所以intKinds[i] > intKinds[j]
if (diffCombineList.ContainsKey(diff))
diffCombineList[diff].Add(new int[] { intKinds[i], intKinds[j] });
else
diffCombineList.Add(diff, new List<int[]> { new int[] { intKinds[i], intKinds[j] } });
}
foreach (KeyValuePair<int, List<int[]>> diff_combine in diffCombineList)
{
int diff = diff_combine.Key;
bool isFind = false;// 是否找到有起点终点相通的路线
foreach (int[] combine in diff_combine.Value)// 遍历所有高度差值范围 [h1,h2],其中h1>h2
{
List<List<Point>> correctLines = lines.FindAll((line) =>
{
return line.All((point) =>
{
return coordinates[point] <= combine[0] && coordinates[point] >= combine[1];
});
});
if (correctLines != null && correctLines.Any())
{
List<Point> bestLine = correctLines[0];
foreach (List<Point> correctLine in correctLines)
if (correctLine.Count < bestLine.Count)
bestLine = correctLine;
Console.WriteLine("最小高度差:" + diff + "\t范围:[" + combine[1] + "," + combine[0] + "]");
StringBuilder stringBuilder = new StringBuilder("路线:");
foreach (Point point in bestLine)
{
stringBuilder.Append("(" + point.X + "," + point.Y + "," + coordinates[point] + ")");
stringBuilder.Append("→");
}
stringBuilder.Remove(stringBuilder.Length - 1, 1);
Console.WriteLine(stringBuilder);
Console.WriteLine();
isFind = true;
//break;// 题外:求所有路线
}
}
//if (isFind) break;// 题外:求其他次选方案
}
Console.ReadKey();
}
运行结果
解法2:排除筛选(迷宫模拟)
先把各个高度差值以及高度差的差值组合形成的值域遍历出来;
例如答案中【高度差值=3】【值域为[0,3]】
对这些差值及值域组合分类讨论,
讨论时,我们把点阵想象成一个N宫格的迷宫,值域之外的格子都视为【迷宫】的【墙】,值域内的格子视为【迷宫】的【路】;
然后,根据传说中的【迷宫万能解法】:只要出入口相通,从入口出发时只要贴着某一边的墙走就必定能走到出口。也就是说从【高度差值】小的【值域】组合开始试,最早遇到能走通的【迷宫】对应的【高度差值】就是所求的【最小的高度差的值】;
相反有符合所求的情况就有要排除的情况,而排除的情况就是:
1.起点开始最后沿着墙回到了起点
2.回到时方向跟最开始时一样(注意从【凸】形迷宫的中间开始走的话不管哪个方向开始都会经过起点,所以只有条件一不够)
也就是这个解法的前提不成立——出入口不相通;
PS.不知道还有多少人记得早期 Windows 的迷宫屏保呢…有的都老了,它并不是走最短路线的,但最终都能走出去,用的就是这个解法,虽然有个迷宫机关会上下旋转,但是颠倒之后会沿着另一边的墙走,所以还是贴回最初的墙继续走,直到走出迷宫。
需要注意的是【最佳路线】 不一定就是 【最短路线】(2 * n - 1步),因为有可能会有迂回的情况
例如(假设黑色的是【墙】):
□ □ □ □ □
■ ■ ■ ■ □
□ □ □ □ □
□ ■ ■ ■ ■
□ □ □ □ □
根据这个思路,那么就还要在外围加四面【墙】,而【墙】的值就设置为【-1】(想象成模型就是凹下去的一条槽,想方便理解或者可以设置成高墙int.最值),因为-1(或者int.最值)必定在输入的高度值的值域之外,加完后的效果如下:
按照题目答案造出来的【墙】以及贴着上边的【墙】走就有下面的路线图
由于路线是根据墙生成的,所以我们先确定【墙】的起点和终点以及贴【墙】直行、左/右转的变化;
首先,这个迷宫的坐标系是一个上下颠倒的【直角坐标系】,那么按照题目起点固定是【(1,1)】,其上边的【墙】坐标就是固定是【(1,0)】(沿着下边走的话就是【(0,1)】开始),从起点开始判断与其连接的【墙】是不是唯一的,从这里,是就继续判断那面唯一的【墙】(1,0)的下一面【墙】;
如果还思维不清晰的话,可以想象换成实际场景中【迷宫万能解法】贴墙走的情景,人站在某个点,面朝前方,左手试图扶墙:
1.直走:发现当前位置的左手边有墙可扶,前方还有路,那就往前走一格;
2.就地右转:发现当前位置的左手边有墙可扶,但前方没路,就地右转;
(如果来到死路就是3面墙的点,那么就会因为这个规则转2次方向,回头后手已经放在来时相反的另一边的墙上往回走)
3.左转往前:发现当前位置的左手边无墙可扶,无论前方情况,就地左转后往前走一格;
(左边不是墙,那么一定是路)
也就是只用讨论这3种情况
这里把上面思路情况具象化一下就是:
1.在起点(1,1),(可以扶的墙有两面,当选的是扶上边缘那面墙)左手有墙可扶,前方有路,符合直走条件,往前走就是(2,1);
2.同样在起点(1,1),如果选的是扶左边缘的墙时,左手有墙可扶,前方没路,就地右转
3.如下图情况时,左手无墙可扶,就地左转后往前走一格(白色箭头表示上一步的方向和位置)
小结分析
那么,总结优化一下,在九宫格里:
1.只需要讨论当前点左方+前方的格子情况,其他格子并不需要讨论;
2.只需要确定当前点坐标以及左墙坐标就能确定(面朝)方向;
3.情况1【直走】和情况2【右转】对比,情况2右转后新的墙坐标刚好是【直走】的新坐标;
4.情况3【左转+直走】的前一步绝对是情况1【直走】,也就是前面的点的左方不是墙就可以直接两步并一步处理,原点往原点左墙的方向斜着走一步,刚好就是再次直走后的左墙,左墙点不变;
因为4的情况的触发条件的必然的,而且触发后的新点必定【左手有墙可扶】所以,判断条件再优化一下:
1.直走:前方还有路,那就往前走一格,左墙点亦往前一格;
1.1.直走+左转+直走:发现将要直走(直走条件成立)的点的位置左手边无墙可扶,墙点位置不变,新站点就是第一次直走时的新左墙点;
2.原地右转:前方没路,原地右转(原点不变),左墙点就是原来应该往前的那一格;
最后转换到代码判断——判断前方及左前方的格子情况(扶左墙时):
0.先测前方是否有路
1.0 前方有路,前方的左边是否有墙可扶
→1.1 前方的左边有墙可扶——直走
→1.2 前方的左边无墙可扶——直走+左转+直走
2.1 前方无路——原地右转
PS.下方代码只是方便完成题目要求,如果要看怎么求迷宫最佳路线就继续看下文【题外扩展1】吧~
代码
List<string> readLineStrings = new List<string> {// 方便测试
"5",
// 原题
"1 1 3 6 8",
"1 2 2 5 5",
"4 4 0 3 3",
"8 0 2 2 4",
"4 3 0 3 1",
// 中间有一层全排除
//"1 1 3 6 8",
//"1 2 2 8 5",
//"4 4 8 3 3",
//"8 8 2 2 4",
//"8 3 0 3 1",
// 排除开头
//"7 1 3 6 8",
//"1 2 2 5 5",
//"4 4 0 3 3",
//"8 0 2 2 4",
//"4 3 0 3 1",
// 排除结尾
//"1 1 3 6 8",
//"1 2 2 5 5",
//"4 4 0 3 3",
//"8 0 2 2 4",
//"4 3 0 3 7",
// 分层式排除
//"0 0 3 6 8",
//"1 0 7 7 7",
//"4 7 0 0 3",
//"7 0 2 0 4",
//"4 3 0 0 0",
// 分层式排除
//"0 0 0 6 8",
//"1 7 0 0 7",
//"7 0 0 7 3",
//"0 0 7 0 4",
//"4 0 0 0 0",
};
int n = int.Parse(readLineStrings[0]);
List<int> intKinds = new List<int>();// 高度值的种类
Dictionary<Point, int> coordinates = new Dictionary<Point, int>();// 原接收输入的值(还原是个立体坐标系)
string[] strList = null;
for (int y = 0; y <= n + 1; y++)
for (int x = 0; x <= n + 1; x++)
if (y == 0 || y == n + 1 || x == 0 || x == n + 1) // 上边或底边的墙
coordinates.Add(new Point(x, y), -1);
else
{
strList = readLineStrings[y].Split();
int z = int.Parse(strList[x - 1]);
coordinates.Add(new Point(x, y), z);
if (!intKinds.Contains(z)) // 收集不同的高度值
{
intKinds.Add(z);
}
}
intKinds.Sort((int i1, int i2) => { return i2 - i1; });// 自大到小排序不同的高度值
SortedDictionary<int, List<int[]>> diffCombineList = new SortedDictionary<int, List<int[]>>();// 高度差值列表<高度差,对应高度差组合>
int startHeight = coordinates[new Point(1, 1)];// 起点高度
int finHeight = coordinates[new Point(n, n)];// 终点高度
// 当存在起点高度等于终点高度 & 这个高度值的数量大于等于最短路线的步数2n-1时,可以添加差值为0的组合
if (startHeight == finHeight && coordinates.Count((coordinate) => { return coordinate.Value == startHeight; }) >= 2 * n - 1)
diffCombineList.Add(0, new List<int[]> { new int[] { startHeight, finHeight } });
for (int i = 0; i < intKinds.Count; i++)
for (int j = i + 1; j < intKinds.Count; j++)
if (intKinds[i] >= startHeight && intKinds[j] <= startHeight && intKinds[i] >= finHeight && intKinds[j] <= finHeight)// ∵起点和终点是必经之地∴起点&终点必定在值域范围内
{
int diff = intKinds[i] - intKinds[j];// 由于高度值已排序所以intKinds[i] > intKinds[j]
if (diffCombineList.ContainsKey(diff))
diffCombineList[diff].Add(new int[] { intKinds[i], intKinds[j] });
else
diffCombineList.Add(diff, new List<int[]> { new int[] { intKinds[i], intKinds[j] } });
}
foreach (KeyValuePair<int, List<int[]>> diff_combine in diffCombineList)
{
int diff = diff_combine.Key;
foreach (int[] combine in diff_combine.Value)// 遍历所有高度差值范围 [h1,h2],其中h1>h2,数值范围外视为墙
{
// 在起点开始走
Point startPoint = new Point(1, 1), startLeftWall = new Point(1, 1 - 1), nowPoint, nowleftWall, nextPoint = new Point(0, 0), nextleftWall = new Point(0, 0);
nowPoint = startPoint;
nowleftWall = startLeftWall;// 构建模型时已经确定是墙
string direction = "";// 方向
List<string> directions = new List<string>();// 运行路线
List<Point> bestCrossPoints = new List<Point> { startPoint };// 最短路线
do
{
double nowX = nowPoint.X, nowY = nowPoint.Y;// 起点坐标
if (nowX == nowleftWall.X) // 同一竖线的一上一下
{
if (nowY > nowleftWall.Y) // 按照题目靠下方的坐标值大
direction = "right";
else if (nowY < nowleftWall.Y)
direction = "left";
}
else if (nowY == nowleftWall.Y) // 同一横线的一左一右
{
if (nowX > nowleftWall.X) // 按照题目靠右方的坐标值大
direction = "up";
else if (nowX < nowleftWall.X)
direction = "down";
}
else
throw new ArgumentOutOfRangeException("当前点坐标值有误,导致方向有误:nowX=" + nowX + "\tnowY:" + nowY);
switch (direction)// 求前方及前方左墙的点坐标
{
case "right":
nextPoint = new Point(nowX + 1, nowY);
nextleftWall = new Point(nowX + 1, nowY - 1);
break;
case "left":
nextPoint = new Point(nowX - 1, nowY);
nextleftWall = new Point(nowX - 1, nowY + 1);
break;
case "up":
nextPoint = new Point(nowX, nowY - 1);
nextleftWall = new Point(nowX - 1, nowY - 1);
break;
case "down":
nextPoint = new Point(nowX, nowY + 1);
nextleftWall = new Point(nowX + 1, nowY + 1);
break;
default:
throw new ArgumentOutOfRangeException("当前点坐标值有误,导致方向有误:nowX=" + nowX + "\tnowY:" + nowY);
}
bestCrossPoints.Add(nextPoint);// 路线记录
if (coordinates[nextPoint] > combine[0] || coordinates[nextPoint] < combine[1]) // 前方是墙,原地右转,右转后新的墙坐标刚好是上一步前方的坐标
{
nextleftWall = nextPoint;// 把前方变成下一步的左墙
nextPoint = nowPoint;// 退回原地
direction = "turnRight";
bestCrossPoints.RemoveAt(bestCrossPoints.Count - 1);// 路线记录:退回原地=删除刚刚的直走
}
else if (coordinates[nextleftWall] <= combine[0] && coordinates[nextleftWall] >= combine[1])// 前方及前左边都不是墙,直接符合情况3,左转+直走,跟当前的直走并起来就是直走+左转+直走,相当于墙点不变,原点往原点左墙的方向斜着走一步,刚好就是直走后的左墙
{
nextPoint = nextleftWall;
nextleftWall = nowleftWall;
bestCrossPoints.Add(nextPoint);// 路线记录
switch (direction)
{
case "right":
direction = "up&Right";
break;
case "left":
direction = "down&left";
break;
case "up":
direction = "up&left";
break;
case "down":
direction = "down&Right";
break;
default:
throw new ArgumentOutOfRangeException("当前点坐标值有误,导致方向有误:nowX=" + nowX + "\tnowY:" + nowY);
}
}
nowPoint = nextPoint;
nowleftWall = nextleftWall;
directions.Add(direction);
} while (bestCrossPoints.Count((p) => { return p == new Point(1, 1); }) < 2);
if (bestCrossPoints.Contains(new Point(n,n)))
{
Console.WriteLine("最小高度差:" + diff + "\t范围:[" + combine[1] + "," + combine[0] + "]");
// 下面是额外显示内容
for (int y = 1; y <= n; y++)
{
for (int x = 1; x <= n; x++)
{
Point point = new Point(x, y);
int z = coordinates[point];
if (z > combine[0] || z < combine[1])
Console.Write("(-,-," + z + ")");
else
Console.Write("(" + x + "," + y + "," + z + ")");
}
Console.WriteLine();
}
Console.Write("贴墙一圈路线:");
foreach (string d in directions)
switch (d)
{
case "right":
Console.Write("→ ");
break;
case "left":
Console.Write("← ");
break;
case "up":
Console.Write("↑ ");
break;
case "down":
Console.Write("↓ ");
break;
case "turnRight":
Console.Write("T ");
break;
case "up&Right":
Console.Write("↗ ");
break;
case "down&left":
Console.Write("↙ ");
break;
case "up&left":
Console.Write("↖ ");
break;
case "down&Right":
Console.Write("↘ ");
break;
default:
throw new ArgumentOutOfRangeException("当前点坐标值有误,导致方向有误!");
}
Console.WriteLine();
List<Point> leftWallPoints = new List<Point>();
List<Point> rightWallPoints = new List<Point>();
for (int i = 0; i <= bestCrossPoints.IndexOf(new Point(n, n)); i++)
leftWallPoints.Add(bestCrossPoints[i]);
Console.Write("贴左墙最佳路线:");
for (int i = 0; i <= leftWallPoints.IndexOf(new Point(n, n)); i++)
{
leftWallPoints.RemoveRange(i, leftWallPoints.LastIndexOf(leftWallPoints[i]) - i);// 走到重复点证明,这两个重复点之间是冤枉路所以去掉
Console.Write("(" + leftWallPoints[i].X + "," + leftWallPoints[i].Y + ")");
}
Console.WriteLine();
for (int i = bestCrossPoints.IndexOf(new Point(n, n)); i < bestCrossPoints.Count; i++)
rightWallPoints.Insert(0, bestCrossPoints[i]);
if (rightWallPoints.Count > 1)
{
Console.Write("贴右墙最佳路线:");
for (int i = 0; i <= rightWallPoints.IndexOf(new Point(n, n)); i++)
{
rightWallPoints.RemoveRange(i, rightWallPoints.LastIndexOf(rightWallPoints[i]) - i);// 走到重复点证明,这两个重复点之间是冤枉路所以去掉
Console.Write("(" + rightWallPoints[i].X + "," + rightWallPoints[i].Y + ")");
}
}
Console.WriteLine();
//break;// 放出则不求所有路线
}
}
//if (bestCrossPoints.Contains(new Point(n,n))) break;// 放出则不求其他次选方案
}
Console.ReadKey();
运行结果
PS.上面说的最佳路线只是指贴某边墙的最佳路线,但不包括不贴墙的最佳路线,如果想查验其他组合的路线的最大高度差可以把上面代码里2个带【题外】的注释位置进行【行注释】↓
题外扩展1——贴墙排查方式:
PS.这里试多了几个样式的迷宫后可以智能点识别迷宫减少循环,其实就是把上面的基本动作组合一套;
例如:
1.如果可以一直直走,那么就走到尽头;
1.1.尽头是死路,就回到原点或者当前原点的下一步反向后继续;
2.如果走到墙前必定右转,但如果右转后会触发【直走→转左→直走】,那么直接↗走
…
当然除了上面的优化外,还可以用【堵路法】——除起点终点,将3个方向都是墙的点都变为墙,循环N次,直到迷宫内没有这种点为止:
第一次循环后:
就这样,循环4次后就有最佳的贴墙走法:
但是这样子还不是适用所有迷宫的最佳走法,因为还有下面这种情况:
贴墙走的最佳路线要11步,但是我们肉眼可见最佳路线(不止一条)应该是9步:
那么我们需要堵住的路应该是(最佳步数会更趋于2*n-1):
这里就需要整理下思路,获取当前点的上下左右4个点的情况,3或4面墙的好判断,是堵上后不会有问题,但≤2面墙的,不仅要判断墙是否相连而且要判断堵上后是否影响原有道路的畅通,所以不能单纯用墙的数量作为依据去将该点变为【墙】。
那就先把畅通性作为首要条件,那么堵上(中点)后不影响畅通性的组合有:
PS.【 ?】为可【墙】可【路】;【0】为【路】;【-1】为【墙】;每个情况旋转90°讨论一次 × 4 ,重复板型则跳过
4 面墙:
3 面墙:
2 面墙:
PS.对立的两面墙的情况可以直接排除
1 面墙:
中间的【0】确实堵上都不会影响【通行】,但如果是这种情况反而会造成【绕路】,所以这种左边或右边还有路情况应当不执行堵路
目前想到的就是这2种情况是可以堵的
同样也要多次排查,只不过,可以把排查过的就不再重复排查,减少无效排查数;
然后发现如果对应高度差本来就是不通的话,新添加的墙肯定会随着循环而渐渐堵满除起点和终点的路,最后只剩起点和终点两个坐标,所以,如果出现这种情况直接视为排除的方案,这样,就可以直接优化迷宫路线的同时排除无解的不相通的迷宫布局,也就是上面的代码的处理记录路线部分变成了拿来二次验证;
由于最优解已经把冤枉路堵住了,所以原代码对冤枉路的处理就可以去掉了;
PS.下面代码里对【外墙】的定义坐标 x ∈ [ 1 , n ] & y ∈ [ 1 , n ]范围以外的都视为【外墙】
代码
List<string> readLineStrings = new List<string> {// 这里方便测试不设置输入
"5",
// 原题
"1 1 3 6 8",
"1 2 2 5 5",
"4 4 0 3 3",
"8 0 2 2 4",
"4 3 0 3 1",
// 双路线
//"1 1 1 1 1",
//"1 4 4 4 1",
//"1 4 4 4 1",
//"1 4 4 4 1",
//"1 1 1 1 1",
// 3路线
//"1 1 1 1 1",
//"1 1 1 4 1",
//"1 4 1 4 1",
//"1 4 1 1 1",
//"1 1 1 1 1",
// 中间有一层全排除
//"1 1 3 6 8",
//"1 2 2 8 5",
//"4 4 8 3 3",
//"8 8 2 2 4",
//"8 3 0 3 1",
// 排除开头
//"7 1 3 6 8",
//"1 2 2 5 5",
//"4 4 0 3 3",
//"8 0 2 2 4",
//"4 3 0 3 1",
// 排除结尾
//"1 1 3 6 8",
//"1 2 2 5 5",
//"4 4 0 3 3",
//"8 0 2 2 4",
//"4 3 0 3 7",
// 分层式排除
//"0 0 3 6 8",
//"1 0 7 7 7",
//"4 7 0 0 3",
//"7 0 2 0 4",
//"4 3 0 0 0",
// 分层式排除
//"0 0 0 6 8",
//"1 7 0 0 7",
//"7 0 0 7 3",
//"0 0 7 0 4",
//"4 0 0 0 0",
};
int n = int.Parse(readLineStrings[0]);
List<int> intKinds = new List<int>();// 高度值的种类
Dictionary<Point, int> coordinates = new Dictionary<Point, int>();// 原接收输入的值(还原是个立体坐标系)
string[] strList = null;
for (int y = 1; y <= n; y++)
for (int x = 1; x <= n; x++)
{
strList = readLineStrings[y].Split();
int z = int.Parse(strList[x - 1]);
coordinates.Add(new Point(x, y), z);
if (!intKinds.Contains(z)) // 收集不同的高度值
intKinds.Add(z);
}
intKinds.Sort((int i1, int i2) => { return i2 - i1; });// 自大到小排序不同的高度值
SortedDictionary<int, List<int[]>> diffCombineList = new SortedDictionary<int, List<int[]>>();// 高度差值列表<高度差,对应高度差组合>
int startHeight = coordinates[new Point(1, 1)];// 起点高度
int finHeight = coordinates[new Point(n, n)];// 终点高度
// 当存在起点高度等于终点高度 & 这个高度值的数量大于等于最短路线的步数2n-1时,可以添加差值为0的组合
if (startHeight == finHeight && coordinates.Count((coordinate) => { return coordinate.Value == startHeight; }) >= 2 * n - 1)
diffCombineList.Add(0, new List<int[]> { new int[] { startHeight, finHeight } });
for (int i = 0; i < intKinds.Count; i++)
for (int j = i + 1; j < intKinds.Count; j++)
// ∵起点和终点是必经之地∴起点&终点必定在值域范围内
if (intKinds[i] >= startHeight && intKinds[j] <= startHeight && intKinds[i] >= finHeight && intKinds[j] <= finHeight)
{
int diff = intKinds[i] - intKinds[j];// ∵高度值已排序∴intKinds[i] > intKinds[j]
if (diffCombineList.ContainsKey(diff))
diffCombineList[diff].Add(new int[] { intKinds[i], intKinds[j] });
else
diffCombineList.Add(diff, new List<int[]> { new int[] { intKinds[i], intKinds[j] } });
}
foreach (KeyValuePair<int, List<int[]>> diff_combine in diffCombineList)
{
int diff = diff_combine.Key;
foreach (int[] combine in diff_combine.Value)// 从最小高度差开始遍历所有高度差值范围 [h1,h2],其中h1>h2
{
List<Point> newWalls = new List<Point>();// 当前高度差值范围内视为墙的点
int newWallCount = 0;
// 将可以堵的路堵上
do
{
newWallCount = 0;
for (int y = 1; y <= n; y++)// 遍历所有点
for (int x = 1; x <= n; x++)
{
if ((x == 1 && y == 1) || (x == n && y == n))// 排除起点 & 终点
continue;
int wallCount = 0;// 当前点上下左右的墙数量
Point point = new Point(x, y);
if (!newWalls.Contains(point)) // 本身不是墙
{
int mid = coordinates.ContainsKey(point) ? coordinates[point] : -1;// 当前点上方的点高度
if (mid > combine[0] || mid < combine[1]) // 高度在范围外的直接视为墙
{
newWalls.Add(point);
newWallCount++;
continue;
}
bool hasUpWall = IsWall(new Point(x, y - 1), coordinates, newWalls, combine);// 当前点上方的点是否为墙
bool hasDownWall = IsWall(new Point(x, y + 1), coordinates, newWalls, combine);// 当前点下方的点是否为墙
bool hasLeftWall = IsWall(new Point(x - 1, y), coordinates, newWalls, combine);// 当前点左方的点是否为墙
bool hasRightWall = IsWall(new Point(x + 1, y), coordinates, newWalls, combine);// 当前点右方的点是否为墙
if (hasUpWall) wallCount++;
if (hasDownWall) wallCount++;
if (hasLeftWall) wallCount++;
if (hasRightWall) wallCount++;
if (wallCount == 0)
continue;
if (wallCount == 3 || wallCount == 4)// 3或4面都是墙 = 只有一面是路 就堵上
{
newWalls.Add(point);
newWallCount++;
continue;
}
else //if (wallCount <= 2)// 相邻2面都是墙 = 只有2面是路
{
bool hasUpLeftWall = IsWall(new Point(x - 1, y - 1), coordinates, newWalls, combine);// 左上方的点是否为墙
bool hasUpRightWall = IsWall(new Point(x + 1, y - 1), coordinates, newWalls, combine);// 右上方的点是否为墙
bool hasDownLeftWall = IsWall(new Point(x - 1, y + 1), coordinates, newWalls, combine);// 左下方的点是否为墙
bool hasDownRightWall = IsWall(new Point(x + 1, y + 1), coordinates, newWalls, combine);// 右下方的点是否为墙
if (
wallCount == 2// 墙数:有助于简化判断非墙的点
&& !((hasUpWall && hasDownWall) || (hasLeftWall && hasRightWall))// 这两面墙是【上&下】或者【左&右】的情况则忽略
&& (
(hasLeftWall && hasUpWall && !hasDownRightWall)// 两墙靠在左上角
|| (hasRightWall && hasUpWall && !hasDownLeftWall)// 两墙靠在右上角
|| (hasRightWall && hasDownWall && !hasUpLeftWall)// 两墙靠在右下角
|| (hasDownWall && hasLeftWall && !hasUpRightWall)// 两墙靠在左下角
)
)
{
newWalls.Add(point);
newWallCount++;
continue;
}
if (wallCount == 1)// 墙数:有助于简化判断非墙的点
{
bool hasUpLeftUpWall = IsWall(new Point(x - 1, y - 2), coordinates, newWalls, combine);// 左上上方的点是否为墙
bool hasUpLeftLeftWall = IsWall(new Point(x - 2, y - 1), coordinates, newWalls, combine);// 左上左方的点是否为墙
bool hasUpRightUpWall = IsWall(new Point(x + 1, y - 2), coordinates, newWalls, combine);// 右上上方的点是否为墙
bool hasUpRightRightWall = IsWall(new Point(x + 2, y - 1), coordinates, newWalls, combine);// 右上右方的点是否为墙
bool hasDownLeftLeftUpWall = IsWall(new Point(x - 2, y + 1), coordinates, newWalls, combine);// 左下左方的点是否为墙
bool hasDownLeftDownWall = IsWall(new Point(x - 1, y + 2), coordinates, newWalls, combine);// 左下下方的点是否为墙
bool hasDownRightRightWall = IsWall(new Point(x + 2, y + 1), coordinates, newWalls, combine);// 右下右方的点是否为墙
bool hasDownRightDownWall = IsWall(new Point(x + 1, y + 2), coordinates, newWalls, combine);// 右下下方的点是否为墙
if (
(hasLeftWall && !hasUpRightWall && !hasDownRightWall && (!hasDownRightDownWall || !hasUpRightUpWall))// 左墙
|| (hasUpWall && !hasDownLeftWall && !hasDownRightWall && (!hasDownLeftLeftUpWall || !hasDownRightRightWall))// 上墙
|| (hasRightWall && !hasUpLeftWall && !hasDownLeftWall && (!hasUpLeftUpWall || !hasDownLeftDownWall))// 右墙
|| (hasDownWall && !hasUpLeftWall && !hasUpRightWall && (!hasUpLeftLeftWall || !hasUpRightRightWall))// 下墙
)
{
newWalls.Add(point);
newWallCount++;
continue;
}
}
}
}
}
} while (newWallCount > 0);// 没有新的墙之后就结束循环
if (n * n - newWalls.Count < 2 * n - 1) // 如果本来就不能起点终点想通的排除到最后绝对就只剩起点和终点
continue;
Console.WriteLine("高度差:" + diff + "\t范围:[" + combine[1] + "," + combine[0] + "]");
// 下面是额外显示内容
for (int y = 1; y <= n; y++)
{
for (int x = 1; x <= n; x++)
{
Point point = new Point(x, y);
int z = coordinates[point];
if (z > combine[0] || z < combine[1])
Console.Write("(-,-," + z + ")");// 不在高度范围内的原题点
else if (newWalls.Contains(point))
Console.Write("(+,+," + z + ")");// 新添加的堵路点
else
Console.Write("(" + x + "," + y + "," + z + ")");
}
Console.WriteLine();
}
// 在起点开始走(这里其实就是验证作用而已)
Point startPoint = new Point(1, 1), startLeftWall = new Point(1, 1 - 1), nowPoint, nowleftWall, nextPoint = new Point(0, 0), nextLeftWall = new Point(0, 0);
nowPoint = startPoint;
nowleftWall = startLeftWall;// 构建模型时已经确定是墙
string direction = "";// 方向
List<Point> crossPoints = new List<Point> { startPoint };// 路线
do
{
double nowX = nowPoint.X, nowY = nowPoint.Y;// 起点坐标
if (nowX == nowleftWall.X) // 同一竖线的一上一下
{
if (nowY > nowleftWall.Y) // 按照题目靠下方的坐标值大
direction = "right";
else if (nowY < nowleftWall.Y)
direction = "left";
}
else if (nowY == nowleftWall.Y) // 同一横线的一左一右
{
if (nowX > nowleftWall.X) // 按照题目靠右方的坐标值大
direction = "up";
else if (nowX < nowleftWall.X)
direction = "down";
}
else
throw new ArgumentOutOfRangeException("当前点坐标值有误,导致方向有误:nowX=" + nowX + "\tnowY:" + nowY);
switch (direction)// 求前方及前方左墙的点坐标
{
case "right":
nextPoint = new Point(nowX + 1, nowY);
nextLeftWall = new Point(nowX + 1, nowY - 1);
break;
case "left":
nextPoint = new Point(nowX - 1, nowY);
nextLeftWall = new Point(nowX - 1, nowY + 1);
break;
case "up":
nextPoint = new Point(nowX, nowY - 1);
nextLeftWall = new Point(nowX - 1, nowY - 1);
break;
case "down":
nextPoint = new Point(nowX, nowY + 1);
nextLeftWall = new Point(nowX + 1, nowY + 1);
break;
default:
throw new ArgumentOutOfRangeException("当前点坐标值有误,导致方向有误:nowX=" + nowX + "\tnowY:" + nowY);
}
crossPoints.Add(nextPoint);// 路线记录
if (!coordinates.ContainsKey(nextPoint) || newWalls.Contains(nextPoint) || coordinates[nextPoint] > combine[0] || coordinates[nextPoint] < combine[1]) // 前方是墙,原地右转,右转后新的墙坐标刚好是上一步前方的坐标
{
nextLeftWall = nextPoint;// 把前方变成下一步的左墙
nextPoint = nowPoint;// 退回原地
direction = "turnRight";
crossPoints.RemoveAt(crossPoints.Count - 1);// 路线记录:退回原地=删除刚刚的直走
}
// 前方及前左边都不是墙,直接符合情况3,左转+直走,跟当前的直走并起来就是直走+左转+直走,相当于墙点不变,原点往原点左墙的方向斜着走一步,刚好就是直走后的左墙
else if (coordinates.ContainsKey(nextLeftWall) && !newWalls.Contains(nextLeftWall) && coordinates[nextLeftWall] <= combine[0] && coordinates[nextLeftWall] >= combine[1])
{
nextPoint = nextLeftWall;
nextLeftWall = nowleftWall;
crossPoints.Add(nextPoint);// 路线记录
switch (direction)
{
case "right":
direction = "up&Right";
break;
case "left":
direction = "down&left";
break;
case "up":
direction = "up&left";
break;
case "down":
direction = "down&Right";
break;
default:
throw new ArgumentOutOfRangeException("当前点坐标值有误,导致方向有误:nowX=" + nowX + "\tnowY:" + nowY);
}
}
if (nextPoint == new Point(0, 0) || nextLeftWall == new Point(0, 0))
new ArgumentOutOfRangeException("当前点坐标值有误,导致计算直走后的新坐标有误:nowX=" + nowX + "\tnowY:" + nowY);
nowPoint = nextPoint;
nowleftWall = nextLeftWall;
} while (crossPoints.Count((bcp) => { return bcp == new Point(1, 1); }) < 2);// 当有两个起点时意味着路线已经先沿着左墙再沿着右墙回到起点,即使没经过终点
if (crossPoints.Contains(new Point(n, n)))// 如果能走到出口则正解
{
Console.Write("贴墙路线:");
Console.WriteLine();
int rightEndIndex = 0;
Console.Write("贴左墙路线:");
for (int i = 0; i < crossPoints.Count; i++)
{
Console.Write("(" + crossPoints[i] + ")");
if (crossPoints[i] == new Point(n, n))
{
rightEndIndex = i;
break;
}
}
Console.WriteLine();
Console.Write("贴右墙路线:");
for (int i = crossPoints.Count-1; i >= rightEndIndex; i--)
{
Console.Write("(" + crossPoints[i] + ")");
if (crossPoints[i] == new Point(n, n))
{
rightEndIndex = i;
break;
}
}
//break;// 放出来则不求右墙路线
}
Console.WriteLine();
}
//if (bestCrossPoints.Contains(new Point(n, n))) break;// 放出来则不求其他高度差方案
}
Console.ReadKey();
/// <summary>
/// 当前点是否为墙
/// </summary>
/// <param name="point">点坐标</param>
/// <param name="coordinates">立体坐标系</param>
/// <param name="newWalls">当前高度差的墙坐标集合</param>
/// <param name="combine">当前高度差</param>
/// <returns></returns>
private static bool IsWall(Point point, Dictionary<Point, int> coordinates, List<Point> newWalls, int[] combine)
{
int high = coordinates.ContainsKey(point) ? coordinates[point] : -1;// 点高度
return newWalls.Contains(point) || high > combine[0] || high < combine[1];// 是否为墙
}
运行结果
题外扩展2——结合扩展1封路后用栈方式寻找最佳路线:
按扩展1逻辑,封路后能连通起点终点的情况下,用类似【蚯蚓走迷宫实验过程】试探出所有路线
最短的路线就显而易见
代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
static void Main(string[] args)
{
List<string> readLineStrings = new List<string> {// 这里方便测试不设置输入
"5",
// 原题
//"1 1 3 6 8",
//"1 2 2 5 5",
//"4 4 0 3 3",
//"8 0 2 2 4",
//"4 3 0 3 1",
// 双路线
//"1 1 1 1 1",
//"1 4 4 4 1",
//"1 4 4 4 1",
//"1 4 4 4 1",
//"1 1 1 1 1",
// 3路线
"1 1 1 1 1",
"1 1 1 4 1",
"1 4 1 4 1",
"1 4 1 1 1",
"1 1 1 1 1",
// 中间有一层全排除
//"1 1 3 6 8",
//"1 2 2 8 5",
//"4 4 8 3 3",
//"8 8 2 2 4",
//"8 3 0 3 1",
// 排除开头
//"7 1 3 6 8",
//"1 2 2 5 5",
//"4 4 0 3 3",
//"8 0 2 2 4",
//"4 3 0 3 1",
// 排除结尾
//"1 1 3 6 8",
//"1 2 2 5 5",
//"4 4 0 3 3",
//"8 0 2 2 4",
//"4 3 0 3 7",
// 分层式排除
//"0 0 3 6 8",
//"1 0 7 7 7",
//"4 7 0 0 3",
//"7 0 2 0 4",
//"4 3 0 0 0",
// 分层式排除
//"0 0 0 6 8",
//"1 7 0 0 7",
//"7 0 0 7 3",
//"0 0 7 0 4",
//"4 0 0 0 0",
};
int n = int.Parse(readLineStrings[0]);
List<int> intKinds = new List<int>();// 高度值的种类
Dictionary<Point, int> coordinates = new Dictionary<Point, int>();// 原接收输入的值(还原是个立体坐标系)
string[] strList = null;
for (int y = 1; y <= n; y++)
for (int x = 1; x <= n; x++)
{
strList = readLineStrings[y].Split();
int z = int.Parse(strList[x - 1]);
coordinates.Add(new Point(x, y), z);
if (!intKinds.Contains(z)) // 收集不同的高度值
intKinds.Add(z);
}
intKinds.Sort((int i1, int i2) => { return i2 - i1; });// 自大到小排序不同的高度值
SortedDictionary<int, List<int[]>> diffCombineList = new SortedDictionary<int, List<int[]>>();// 高度差值列表<高度差,对应高度差组合>
int startHeight = coordinates[new Point(1, 1)];// 起点高度
int finHeight = coordinates[new Point(n, n)];// 终点高度
// 当存在起点高度等于终点高度 & 这个高度值的数量大于等于最短路线的步数2n-1时,可以添加差值为0的组合
if (startHeight == finHeight && coordinates.Count((coordinate) => { return coordinate.Value == startHeight; }) >= 2 * n - 1)
diffCombineList.Add(0, new List<int[]> { new int[] { startHeight, finHeight } });
for (int i = 0; i < intKinds.Count; i++)
for (int j = i + 1; j < intKinds.Count; j++)
// ∵起点和终点是必经之地∴起点&终点必定在值域范围内
if (intKinds[i] >= startHeight && intKinds[j] <= startHeight && intKinds[i] >= finHeight && intKinds[j] <= finHeight)
{
int diff = intKinds[i] - intKinds[j];// ∵高度值已排序∴intKinds[i] > intKinds[j]
if (diffCombineList.ContainsKey(diff))
diffCombineList[diff].Add(new int[] { intKinds[i], intKinds[j] });
else
diffCombineList.Add(diff, new List<int[]> { new int[] { intKinds[i], intKinds[j] } });
}
foreach (KeyValuePair<int, List<int[]>> diff_combine in diffCombineList)
{
int diff = diff_combine.Key;
foreach (int[] combine in diff_combine.Value)// 从最小高度差开始遍历所有高度差值范围 [h1,h2],其中h1>h2
{
List<Point> newWalls = new List<Point>();// 当前高度差值范围内视为墙的点
int newWallCount = 0;
// 将可以堵的路堵上
do
{
newWallCount = 0;
for (int y = 1; y <= n; y++)// 遍历所有点
for (int x = 1; x <= n; x++)
{
if ((x == 1 && y == 1) || (x == n && y == n))// 排除起点 & 终点
continue;
int wallCount = 0;// 当前点上下左右的墙数量
Point point = new Point(x, y);
if (!newWalls.Contains(point)) // 本身不是墙
{
int mid = coordinates.ContainsKey(point) ? coordinates[point] : -1;// 当前点上方的点高度
if (mid > combine[0] || mid < combine[1]) // 高度在范围外的直接视为墙
{
newWalls.Add(point);
newWallCount++;
continue;
}
bool hasUpWall = IsWall(new Point(x, y - 1), coordinates, newWalls, combine);// 当前点上方的点是否为墙
bool hasDownWall = IsWall(new Point(x, y + 1), coordinates, newWalls, combine);// 当前点下方的点是否为墙
bool hasLeftWall = IsWall(new Point(x - 1, y), coordinates, newWalls, combine);// 当前点左方的点是否为墙
bool hasRightWall = IsWall(new Point(x + 1, y), coordinates, newWalls, combine);// 当前点右方的点是否为墙
if (hasUpWall) wallCount++;
if (hasDownWall) wallCount++;
if (hasLeftWall) wallCount++;
if (hasRightWall) wallCount++;
if (wallCount == 0)
continue;
if (wallCount == 3 || wallCount == 4)// 3或4面都是墙 = 只有一面是路 就堵上
{
newWalls.Add(point);
newWallCount++;
continue;
}
else //if (wallCount <= 2)// 相邻2面都是墙 = 只有2面是路
{
bool hasUpLeftWall = IsWall(new Point(x - 1, y - 1), coordinates, newWalls, combine);// 左上方的点是否为墙
bool hasUpRightWall = IsWall(new Point(x + 1, y - 1), coordinates, newWalls, combine);// 右上方的点是否为墙
bool hasDownLeftWall = IsWall(new Point(x - 1, y + 1), coordinates, newWalls, combine);// 左下方的点是否为墙
bool hasDownRightWall = IsWall(new Point(x + 1, y + 1), coordinates, newWalls, combine);// 右下方的点是否为墙
if (
wallCount == 2// 墙数:有助于简化判断非墙的点
&& !((hasUpWall && hasDownWall) || (hasLeftWall && hasRightWall))// 这两面墙是【上&下】或者【左&右】的情况则忽略
&& (
(hasLeftWall && hasUpWall && !hasDownRightWall)// 两墙靠在左上角
|| (hasRightWall && hasUpWall && !hasDownLeftWall)// 两墙靠在右上角
|| (hasRightWall && hasDownWall && !hasUpLeftWall)// 两墙靠在右下角
|| (hasDownWall && hasLeftWall && !hasUpRightWall)// 两墙靠在左下角
)
)
{
newWalls.Add(point);
newWallCount++;
continue;
}
if (wallCount == 1)// 墙数:有助于简化判断非墙的点
{
bool hasUpLeftUpWall = IsWall(new Point(x - 1, y - 2), coordinates, newWalls, combine);// 左上上方的点是否为墙
bool hasUpLeftLeftWall = IsWall(new Point(x - 2, y - 1), coordinates, newWalls, combine);// 左上左方的点是否为墙
bool hasUpRightUpWall = IsWall(new Point(x + 1, y - 2), coordinates, newWalls, combine);// 右上上方的点是否为墙
bool hasUpRightRightWall = IsWall(new Point(x + 2, y - 1), coordinates, newWalls, combine);// 右上右方的点是否为墙
bool hasDownLeftLeftUpWall = IsWall(new Point(x - 2, y + 1), coordinates, newWalls, combine);// 左下左方的点是否为墙
bool hasDownLeftDownWall = IsWall(new Point(x - 1, y + 2), coordinates, newWalls, combine);// 左下下方的点是否为墙
bool hasDownRightRightWall = IsWall(new Point(x + 2, y + 1), coordinates, newWalls, combine);// 右下右方的点是否为墙
bool hasDownRightDownWall = IsWall(new Point(x + 1, y + 2), coordinates, newWalls, combine);// 右下下方的点是否为墙
if (
(hasLeftWall && !hasUpRightWall && !hasDownRightWall && (!hasDownRightDownWall || !hasUpRightUpWall))// 左墙
|| (hasUpWall && !hasDownLeftWall && !hasDownRightWall && (!hasDownLeftLeftUpWall || !hasDownRightRightWall))// 上墙
|| (hasRightWall && !hasUpLeftWall && !hasDownLeftWall && (!hasUpLeftUpWall || !hasDownLeftDownWall))// 右墙
|| (hasDownWall && !hasUpLeftWall && !hasUpRightWall && (!hasUpLeftLeftWall || !hasUpRightRightWall))// 下墙
)
{
newWalls.Add(point);
newWallCount++;
continue;
}
}
}
}
}
} while (newWallCount > 0);// 没有新的墙之后就结束循环
if (n * n - newWalls.Count < 2 * n - 1) // 如果本来就不能起点终点想通的排除到最后绝对就只剩起点和终点
continue;
Console.WriteLine("高度差:" + diff + "\t范围:[" + combine[1] + "," + combine[0] + "]");
// 下面是额外显示内容
for (int y = 1; y <= n; y++)
{
for (int x = 1; x <= n; x++)
{
Point point = new Point(x, y);
int z = coordinates[point];
if (z > combine[0] || z < combine[1])
Console.Write("(-,-," + z + ")");// 不在高度范围内的原题点
else if (newWalls.Contains(point))
Console.Write("(+,+," + z + ")");// 新添加的堵路点
else
Console.Write("(" + x + "," + y + "," + z + ")");
}
Console.WriteLine();
}
// 在起点开始走(这里其实就是验证作用而已)
Point startPoint = new Point(1, 1), startLeftWall = new Point(1, 1 - 1), nowPoint, nowleftWall, nextPoint = new Point(0, 0), nextLeftWall = new Point(0, 0);
nowPoint = startPoint;
nowleftWall = startLeftWall;// 构建模型时已经确定是墙
Stack<KeyValuePair<Point, Dictionary<Point, bool>>> divergingPoint_points = new Stack<KeyValuePair<Point, Dictionary<Point, bool>>>();// 分叉路点
Point farestPoint = new Point(1, 1);// 当前路线走到最远的点 = 当前分岔点
do
{
Point farestPointUp = new Point(farestPoint.X, farestPoint.Y - 1);// 当前分岔点上方坐标
Point farestPointDown = new Point(farestPoint.X, farestPoint.Y + 1);// 当前分岔点下方坐标
Point farestPointLeft = new Point(farestPoint.X - 1, farestPoint.Y);// 当前分岔点左方坐标
Point farestPointRight = new Point(farestPoint.X + 1, farestPoint.Y);// 当前分岔点右方坐标
bool isUpRoad = !IsWall(farestPointUp, coordinates, newWalls, combine);// 当前分岔点上方能否走
bool isDownRoad = !IsWall(farestPointDown, coordinates, newWalls, combine);// 当前分岔点下方能否走
bool isLeftRoad = !IsWall(farestPointLeft, coordinates, newWalls, combine);// 当前分岔点左方能否走
bool isRightRoad = !IsWall(farestPointRight, coordinates, newWalls, combine);// 当前分岔点右方能否走
Dictionary<Point, bool> divergingPoints = new Dictionary<Point, bool>();// 当前点分岔路的点,是否未走过
if (divergingPoint_points.Any() && divergingPoint_points.Peek().Key == farestPoint)// 当前点是最近到过分岔点
divergingPoints = divergingPoint_points.Peek().Value;
else// 该方向能走 && 不是来时方向 == 添加新的分岔点 (如果是直路,当前分岔点则只有一个分岔点)
{
if (isUpRoad && !divergingPoint_points.Any((p) => { return p.Key == farestPointUp; }))
divergingPoints.Add(farestPointUp, true);
if (isDownRoad && !divergingPoint_points.Any((p) => { return p.Key == farestPointDown; }))
divergingPoints.Add(farestPointDown, true);
if (isLeftRoad && !divergingPoint_points.Any((p) => { return p.Key == farestPointLeft; }))
divergingPoints.Add(farestPointLeft, true);
if (isRightRoad && !divergingPoint_points.Any((p) => { return p.Key == farestPointRight; }))
divergingPoints.Add(farestPointRight, true);
divergingPoint_points.Push(new KeyValuePair<Point, Dictionary<Point, bool>>(farestPoint, divergingPoints));
}
bool isDeadLoad = false;// 是否走到尽头
// 不是冤枉路/回头路 && 该方向有路 && 该分岔路未走过
if (!divergingPoints.Any())// 无路可走/当前点所有岔路已经走过
isDeadLoad = true;
else if (isUpRoad && divergingPoints.ContainsKey(farestPointUp) && divergingPoints[farestPointUp])
farestPoint = farestPointUp;
else if (isDownRoad && divergingPoints.ContainsKey(farestPointDown) && divergingPoints[farestPointDown])
farestPoint = farestPointDown;
else if (isLeftRoad && divergingPoints.ContainsKey(farestPointLeft) && divergingPoints[farestPointLeft])
farestPoint = farestPointLeft;
else if (isRightRoad && divergingPoints.ContainsKey(farestPointRight) && divergingPoints[farestPointRight])
farestPoint = farestPointRight;
if (farestPoint == new Point(n, n))// 走到终点
{
List<KeyValuePair<Point, Dictionary<Point, bool>>> roads = new List<KeyValuePair<Point, Dictionary<Point, bool>>>(divergingPoint_points);
roads.Reverse();// 反向排序
foreach (KeyValuePair<Point, Dictionary<Point, bool>> road in roads) // 打印当前路线
Console.Write("(" + road.Key + ")");
Console.WriteLine("(" + farestPoint + ")");
isDeadLoad = true;
}
if (isDeadLoad)
{
do
{
divergingPoint_points.Pop();
} while (divergingPoint_points.Any() && divergingPoint_points.Peek().Value.All((p_b) => { return !p_b.Value; }));// 回到最近的分岔点
if (divergingPoint_points.Any())
farestPoint = divergingPoint_points.Peek().Key;
else
break;// 不存在未走过的分叉路
}
else
divergingPoints[farestPoint] = false;// 标志该岔路点已走过
} while (true);
}
//if (bestCrossPoints.Contains(new Point(n, n))) break;// 放出来则不求其他高度差方案
}
Console.ReadKey();
}
/// <summary>
/// 当前点是否为墙
/// </summary>
/// <param name="point">点坐标</param>
/// <param name="coordinates">立体坐标系</param>
/// <param name="newWalls">当前高度差的墙坐标集合</param>
/// <param name="combine">当前高度差</param>
/// <returns></returns>
private static bool IsWall(Point point, Dictionary<Point, int> coordinates, List<Point> newWalls, int[] combine)
{
int high = coordinates.ContainsKey(point) ? coordinates[point] : -1;// 点高度
return newWalls.Contains(point) || high > combine[0] || high < combine[1];// 是否为墙
}
运行结果
题外扩展3——拆边法寻找最佳路线(仅思路):
思路说明
这里简单画一个二叉树模型,两边的墙分别标红和绿,最佳路线标蓝
当走到分叉路口(图中剪头位置),最佳选择永远是同时占有不同颜色的岔路,这个思路偏向于简化选择
思路局限性
一方面对于那些有宽广区域的迷宫就不太好用,例如下图这种
另一方面对于存在其他(也许还很多道)与上下墙都不相连的墙体(例如下图的紫墙)时,在三种颜色的分叉路也许就要加个关于【不走重复路】,那如果还有其他颜色的墙,就要考虑不同岔路数量、颜色数量和排列组合等等那么无需回头的路线数量就随着后面的三、四色分叉路数量呈几何级别复杂
总结下就是最适合双墙窄路的迷宫。
题外扩展4——相似题:
之前有道相似的题目,相当于这题改成了固定只能走最短的步数(2*n-1),且求最小高度而非高度差;
其模型可以看成一个【菱形二叉树】,就是把输入样本顺时针转一小下(差不多45°的样子),这样就是跟题目里“飞”的路线一样,从起点一次“飞”一层往下“飞”到终点;
(但代码里在新列阵中要进行原坐标x,y与新点阵的转换运算,空间思维不强的可以在coordinateLists新列阵形成时添加(坐标,高度)的键值对作为添加对象,或者构造一个int[][][] 三维数组或者集合进行讨论,就是不旋转)
旋转后的新列阵:
新列阵中的原坐标:
从列阵坐标可以看出,每层的坐标值的和(x+y)一定且依次+1递增,且和的值域为【2 ~ 2 * n】,以此为规则在原坐标阵的基础上重新分组,一行一组;
然后使用排除法进行筛选方案得出最大高度的最小值:
例如:
排除[0,7]外的高度值后,列阵就变成下面的点阵:
(其中紫色的是因为红色的排除后没有上层的点够得着,所以也作为推算排除点一并排除)
排除到差值为3,组合为[0,3]的时候就有下面的图,同样形成【有路可走】:.
(其中蓝色的是因为红色的排除后没有下层的点够得着,所以也作为推算排除点一并排除)
但当排除到高度差值为2,值域为[0,2]时就会有下面的图:
这时终点是够不着,导致起点终点连不上,形成【无路可走】,所以这类高度差值组合就会排除;
然后我们从高度差值最小的开始找,最先发现能这里就讨论【有路可走】的条件——至少保留一条连接起点和终点的路线:
除了终点、起点外,每层至少存在一个同时连接其上层左上角或右上角以及下层的左下角或右下角的坐标点;
(也可以反过来从小的高度排查处理,直到遇到第一个【有路可走】:
每一层都有小于??高度且能连接上下的坐标点)
代码
List<string> readLineStrings = new List<string> {// 方便测试
"5",
// 原题
"1 1 3 6 8",
"1 2 2 5 5",
"4 4 0 3 3",
"8 0 2 2 4",
"4 3 0 3 1",
// 中间有一层全排除
//"1 1 3 6 8",
//"1 2 2 8 5",
//"4 4 8 3 3",
//"8 8 2 2 4",
//"8 3 0 3 1",
// 排除开头
//"7 1 3 6 8",
//"1 2 2 5 5",
//"4 4 0 3 3",
//"8 0 2 2 4",
//"4 3 0 3 1",
// 排除结尾
//"1 1 3 6 8",
//"1 2 2 5 5",
//"4 4 0 3 3",
//"8 0 2 2 4",
//"4 3 0 3 7",
// 分层式排除
//"0 0 3 6 8",
//"1 0 7 7 7",
//"4 7 0 0 3",
//"7 0 2 0 4",
//"4 3 0 0 0",
};
int n =int.Parse( readLineStrings[0]);
List<int> intKinds = new List<int>();// 高度值的种类
List<List<int>> coordinates = new List<List<int>>();// 原接收输入的值
for (int i = 1; i <= n; i++)
{
string[] strList = readLineStrings[i].Split();
List<int> intList = new List<int>();
for (int j = 0; j < n; j++)
{
string str = strList[j];
int height = int.Parse(str);
intList.Add(height);// 收集行内高度值
if (!intKinds.Contains(height)) // 收集不同的高度值
{
intKinds.Add(height);
}
}
coordinates.Add(intList);// 收集某行高度值
}
intKinds.Sort((int i1,int i2)=> { return i2 - i1; });// 自大到小排序不同的高度值
List<List<int>> coordinateLists = new List<List<int>>();// 新的列阵集合组
for (int i = 2; i <= 2 * n; i++) // 每层的坐标值的和(x + y)依次 + 1递增,且和的值域为【2 ~2 * n】
{
List<int> addList=new List<int>(); // 新的列阵单行集合
for (int j = 1; j <= n; j++)
{
for (int k = 1; k <= n; k++)
{
if (j + k == i)// 坐标值的和固定为:i=j+k
{
// 由于当前循环是在原坐标自上往下,而在新列阵是由原列阵顺时针旋转45°得到的,会从新列阵的队尾开始检索到,所以要反向排序
addList.Insert(0, coordinates[j-1][k-1]);
}
}
}
coordinateLists.Add(addList);
}
List<Point> points = new List<Point>();// 整个新列阵里能连接接上下层的原坐标
/************************这步只是想看下符合的坐标,不做这步也可以求出结果**********************/
Dictionary<int, string> results = new Dictionary<int, string>();// 排除不同高度值后的坐标点阵
Dictionary<int, List<Point>> deductivePoints = new Dictionary<int, List<Point>>();
/*******************************************************************************************/
string result = "";
for (int i = 0; i < intKinds.Count; i++)
{
/************************这步只是想看下符合的坐标,不做这步也可以求出结果**********************/
result = "";
for (int j = 1; j <= n; j++)
{
for (int k = 1; k <= n; k++)
{
if (i == 0 || points.Any((point) =>
{
return point.X == j && point.Y == k;
}))
{
result += "(" + j + "," + k + "," + coordinates[j - 1][k - 1] + ")\t";
}
else
{
result += "(-,-," + coordinates[j - 1][k - 1] + ")\t";
}
}
result += "\n";
}
results.Add(intKinds[i], result);
/*******************************************************************************************/
points.Clear();// 循环前清空收集结果
coordinateLists.ForEach((coordinateList) =>// 将新的列阵集合组里的【待排除高度值】改为-1,视为为不可走
{
for (int j = 0; j < coordinateList.Count; j++)// 由于涉及到改基础数据类型值,不合适ForEach方法遍历
{
if (coordinateList[j] == intKinds[i])
{
coordinateList[j] = -1;
}
}
});
bool hasLinkPoint = false;// 是否有上下连通的坐标点
for (int j = 0; j < coordinateLists.Count; j++)// 第j+1层
{
List<int> coordinateList = coordinateLists[j];
if (j == 0 || j == coordinateLists.Count - 1)// 起点、终点 特值处理
{
if (coordinateLists[j][0] != -1)
{
if (j == 0)
{
points.Add(new Point(1, 1));// 起点
}
else
{
points.Add(new Point(n, n));// 终点
}
continue;
}
else break;
}
hasLinkPoint = false;
for (int k = 0; k < coordinateList.Count; k++)
{
if (coordinateList[k] == -1) continue;// 非排除点
// 左上角、右上角、左下角、右下角高度值(默认-1)
int upLeft = -1, upRight = -1, downLeft = -1, downRight = -1;
if (j + 1 < n)// 新列阵上半部分必定有左下和右下的数
{
if (k > 0)// 非左上方的边上坐标才有左上角的坐标
{
upLeft = coordinateLists[j - 1][k - 1];
}
if (k < coordinateLists[j - 1].Count)// 非右上方的边上坐标才有右上角的坐标
{
upRight = coordinateLists[j - 1][k];
}
// 上半层的坐标必有左下角&右下角坐标
downLeft = coordinateLists[j + 1][k];
downRight = coordinateLists[j + 1][k + 1];
}
else if (j + 1 == n) // 列阵中间层(新列阵里最长的一层)
{
if (k > 0)// 除第一个坐标外都有左上角&左下角的坐标
{
upLeft = coordinateLists[j - 1][k - 1];
downLeft = coordinateLists[j + 1][k - 1];
}
if (k < coordinateLists[j - 1].Count)// 除最后一个坐标外都有右上角&右下角的坐标
{
upRight = coordinateLists[j - 1][k];
downRight = coordinateLists[j + 1][k];
}
}
else// (j + 1 > n) // 列阵下半部分必定有左上角和右上角的坐标
{
// 上半层的坐标必有左上角&右上角坐标
upLeft = coordinateLists[j - 1][k];
upRight = coordinateLists[j - 1][k + 1];
if (k > 0)// 非左下方边上的坐标才有左下角的坐标
{
downLeft = coordinateLists[j + 1][k - 1];
}
if (k < coordinateLists[j + 1].Count)// 非右下方边上的坐标才有左右角的坐标
{
downRight = coordinateLists[j + 1][k];
}
}
int x, y;// 原坐标值
if (j + 1 <= n) // 上半层及中间层
{
y = k + 1;
}
else// 下半层(中间层也适用)
{
y = n - (coordinateList.Count - (k + 1));
}
x = j + 1 + 1 - y;// 层数(j+1)+1是两个坐标的和
if ((downLeft != -1 || downRight != -1) // 能连接下层坐标(左下角或右下角坐标)
&& points.Any((point) =>// 能连接上层坐标且在已校验的结果中
{
return (upLeft != -1 && point.X == x && point.Y == y - 1) || (upRight != -1 && point.X == x - 1 && point.Y == y);
}))
{
points.Add(new Point(x, y));
hasLinkPoint = true;
}
else
{
if (deductivePoints.ContainsKey(intKinds[i]))
{
deductivePoints[intKinds[i]].Add(new Point(x, y));
}
else
{
deductivePoints.Add(intKinds[i], new List<Point> { new Point(x, y) });
}
}
}
if (!hasLinkPoint) break;// 只要有一层没有了连接上下层的点,那么排除后的所有路线都会变成【无路可走】,而当前排除值的前一个排除值就是答案
}
if (!hasLinkPoint)
{
Console.WriteLine("结果:" + intKinds[i]);
Console.WriteLine(results[intKinds[i]]);// 显示测试效果
Console.WriteLine("推算排除点:");// 测试用
if (i > 0)
{
Console.WriteLine("推算排除点:");// 测试用
deductivePoints[intKinds[i - 1]].ForEach((point) =>
{
Console.Write("(" + point.X + "," + point.Y + ")\t");
});
Console.WriteLine();
}
break;
}
}
Console.ReadKey();