题目地址: Forming a Magic Square | HackerRank题目大意:
定义 n 阶魔方( magic square )为行、列、对角线元素之和均相等的 n * n 矩阵,且矩阵中元素值为 1 ~ n^2,且各位置元素各不相同。
给定任意 3 * 3 矩阵,判断将其转换为 3 阶矩阵所需调整的数字差值。
例子:
5 3 4
1 5 8
6 4 2答案:
可以将其转换为:
8 3 4
1 5 9
6 7 2其代价为:假定矩阵为
matrix
,变换过程中改变了matrix[0][0]
、matrix[1][2]
、matrix[2][1]
的值,因而其代价为:|8 - 5| + |9 - 8| + |7 - 4| = 7编程语言:Scala
1. 参考思路
本想找一些很巧妙的解决方法,但无奈没有找到。但就这个问题而言,先枚举出所有的 3 阶魔方解集,依次遍历求得代价是肯定可以解决的。查看了题目的解析,发现也是采用这个方法:Forming a Magic Square | HackerRank。
由于 3 阶魔方的解集只有 8 个元素,手算出所有解法并写死在代码中也是可以解决的。但这里讨论一下编码得到解集的方法。
在大题思路上,可以采用:
- 得到一个可能的解集;
- 按照 3 阶魔方的定义对解集进行过滤。
对于第一步,可选择解集有:
- 3 * 3 数组,数组中数字大小为:1 ~ 9。其总数为 9 ^ 9。
- 以数字 1 ~ 9 放置的 3 * 3 数组。其总数为 P(9, 9),即 1 ~ 9 的全排列。
得到这两种可能解集的方法都比较简单,在 3 阶情况下,其量级也都可以接受。以下讨论另一种可能解集:
我们知道,对于 3 阶魔方,其中心数字一定为 5,且以 5 为中心的四条线所连接的剩下位置,需要由四对值:(1, 9)
、(2, 8)
、(3, 7)
、(4, 6)
填充( 这里暂不考虑每一对中数字的顺序 )。
也就是说,我们可以将该四对值,放置到魔方中剩余的位置,并将该结果集当做可能集,从而进一步缩小其数量。
其中四组坐标为:
(0, 0), (2, 2),
(0, 1), (2, 1),
(0, 2), (2, 0),
(1, 0), (1, 2)
对于这个问题,为了得到所有的放置方式,我们可以借助排列( Permutation )问题辅助求解。即先获得 (0, 1, 2, 3)
四个值全排列的所有解集,即 P(4, 4)。然后,对于解集中的任意一个解,以其值为该值对( value pair )对应的位置索引。( 关于排列组合的编码和思路问题可以参考:获取排列组合的结果集 )
比如,对于排列 (3, 1, 2, 0)
,意味着我们将 (1, 9)
放置在 3 号位置 (1, 0), (1, 2)
中、将 (2, 8)
放置在 1 号位置 (0, 1), (2, 1)
中,以此类推。
注意,由于将 (1, 9)
放置在 (1, 0), (1, 2)
中实际上又有两种方式,即 matrix(1)(0) = 1 & matrix(1)(2) = 9
与 matrix(1)(0) = 9 & matrix(1)(2) = 1
。所以对于任意的一种组合,考虑四对值均需考虑顺序,其又存在 2 2 2 * 2 种可能的方式。
2. 参考代码
按照上述思路,参考代码如下:
2.1 魔方验证代码
def isValid(target: Array[Array[Int]]): Boolean = {
val STANDARD = 15
var valid = true
var index = 0
if (STANDARD.equals(target(1)(1) + target(0)(0) + target(2)(2)) == false) valid = false
if (STANDARD.equals(target(1)(1) + target(0)(2) + target(2)(0)) == false) valid = false
while(index < 3 && valid) {
if (STANDARD.equals(target(0)(index) + target(1)(index) + target(2)(index)) == false) valid = false
if (STANDARD.equals(target(index)(0) + target(index)(1) + target(index)(2)) == false) valid = false
index += 1
}
valid
}
2.2 辅助类
为了使代码更直观,这里我们新建一些辅助类,用以表示矩阵的坐标和坐标对:
case class Point(x: Int, y: Int)
坐标对,即以中心为对称的坐标:
case class Pair(left: Point, right: Point)
2.3 获取全排列
这里该出获取排列的代码,详细解释可参考:获取排列组合的结果集。
def getAllPermutations(count: Int, candidates: ArrayBuffer[Int]): ArrayBuffer[ArrayBuffer[Int]] = {
val container = ArrayBuffer[ArrayBuffer[Int]]()
traverse(count, candidates, ArrayBuffer[Int](), container)
container
}
def traverse(count: Int, candidates: ArrayBuffer[Int], bag: ArrayBuffer[Int], container: ArrayBuffer[ArrayBuffer[Int]]): Unit = {
val candidatesLength = candidates.length
if (candidatesLength >= count) {
if (count.equals(1)) {
for (item <- candidates) {
val finalBag = bag.clone()
finalBag += item
container += finalBag
}
} else {
for ((item, index) <- candidates.zipWithIndex) {
val nextBag = bag.clone()
nextBag += item
val nextCandidates = candidates.slice(0, index) ++ candidates.slice(index + 1, candidatesLength)
traverse(count - 1, nextCandidates, nextBag, container)
}
}
}
}
2.4 获取可能集
def getAllCandidates(): Array[Array[Array[Int]]] = {
val numberPairs = Array[Array[Int]](
Array[Int](1, 9),
Array[Int](2, 8),
Array[Int](3, 7),
Array[Int](4, 6)
)
val positions = ArrayBuffer[Array[Pair]]()
val A = Array[Pair](Pair(Point(0, 0), Point(2, 2)), Pair(Point(2, 2), Point(0, 0)))
val B = Array[Pair](Pair(Point(0, 1), Point(2, 1)), Pair(Point(2, 1), Point(0, 1)))
val C = Array[Pair](Pair(Point(0, 2), Point(2, 0)), Pair(Point(2, 0), Point(0, 2)))
val D = Array[Pair](Pair(Point(1, 0), Point(1, 2)), Pair(Point(1, 2), Point(1, 0)))
val permutations = getAllPermutations(4, ArrayBuffer[Int](0, 1, 2, 3))
for (item <- permutations) {
for (a <- A){
for (b <- B){
for (c <- C){
for (d <- D){
val temp = Array[Pair](a, b, c, d)
positions += Array[Pair](
temp(item(0)), temp(item(1)), temp(item(2)), temp(item(3))
)
}
}
}
}
}
val candidates = ArrayBuffer[Array[Array[Int]]]()
for (pairs <- positions) {
val matrix = Array.ofDim[Int](3, 3)
matrix(1)(1) = 5
for ((pair, index) <- pairs.zipWithIndex) {
matrix(pair.left.x)(pair.left.y) = numberPairs(index)(0)
matrix(pair.right.x)(pair.right.y) = numberPairs(index)(1)
}
candidates += matrix
}
candidates.toArray.filter(isValid(_))
}
2.5 获取最小代价
这一步比较简单,即遍历候选矩阵和目标矩阵,得到代价最小的解。
def formingMagicSquare(s: Array[Array[Int]]): Int = {
val candidates = getAllCandidates
var cost = Int.MaxValue
for (candidate <- candidates) {
var currentCost = 0
for ((row, rowIndex) <- candidate.zipWithIndex) {
for ((item, columnIndex) <- row.zipWithIndex) {
currentCost += Math.abs(item - s(rowIndex)(columnIndex))
}
}
if (currentCost < cost) cost = currentCost
}
cost
}
2.6 测试
def main(args: Array[String]): Unit = {
val target = Array[Array[Int]](
Array[Int](4, 8, 2),
Array[Int](4, 5, 7),
Array[Int](6, 1, 6)
)
println(formingMagicSquare(target))
}
代码 Gist 地址:Magic Square:https://www.hackerrank.com/challenges/magic-square-forming/problem · GitHub
参考链接
- 获取排列组合的结果集 - DB.Reid - SegmentFault 思否
- scala 2 dimensional array - Stack Overflow