[HackerRank] Forming a Magic Square 魔方代价问题

题目地址: 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 个元素,手算出所有解法并写死在代码中也是可以解决的。但这里讨论一下编码得到解集的方法。

在大题思路上,可以采用:

  1. 得到一个可能的解集;
  2. 按照 3 阶魔方的定义对解集进行过滤。

对于第一步,可选择解集有:

  1. 3 * 3 数组,数组中数字大小为:1 ~ 9。其总数为 9 ^ 9。
  2. 以数字 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) = 9matrix(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

参考链接

  1. 获取排列组合的结果集 - DB.Reid - SegmentFault 思否
  2. scala 2 dimensional array - Stack Overflow

你可能感兴趣的:(算法)