我目前对动态规划还没有系统的学习过,所以说不上啥专业的术语,我的题解也是从别人的题解中受到了启发,然后加入了自己的理解。
其他几个解题报告有的的纯贴代码,有的题解看得不是很懂,我尽量用通俗易懂的方式展示自己的解题思路,希望能帮助到大家,同时也提升我自己。
最开始刷这道题的时候,状态不是很好,当时想着用穷举法试下运气。
穷举法比较简单,就是遍历每一块空地,求该空地的距离和,然后找出所有空地中距离和的最小值,我用的是PHP语言,反正穷举法是过不了,会超时(即使能过也没啥意思,毕竟没点技术含量

后来简单的想了下,最小值的空地应该会靠近这些房子的几何上的中心点,所以当时就想着找到这个中心点,然后简单的把中心点以及中心点附近的空地都遍历一遍,找出这些点中的最优解。怀着试一试的心态,居然AC了,后面我自己都发现有边界的测试用例,说明牛客网的测试用例还是不全面。
有个大神也是类似的思路,他提出了“等势线“的概念,即中点是绝对的最优解,如果中点不能建,就以中点为中心,向外扩展一圈,找这一圈中的最优解,如果这一圈没空地,则继续再往外扩一圈。
我跟他的思路大体相同,只是还是没有在数学上得到证明,所以理论上有风险。
贴下AC的代码(该解决方案有问题):
<?php
 
//调试开关,1为开启,开启则输出一些中间调试信息
define("DEBUG", 0);
 
/**
 * 计算坐标(i1,j1)到坐标(i2,j2)的距离
 * @param int $i1
 * @param int $j1
 * @param int $i2
 * @param int $j2
 */
function getDistance($i1, $j1, $i2, $j2)
{
    return abs($i1 - $i2) + abs($j1 - $j2);
}
 
/**
 * 计算$i $j位置的空地到各个房子之间的距离和
 * @param array $map 地图方阵阶数
 * @param int $blankI 空地坐标i
 * @param int $blankJ 空地坐标j
 */
function getDistanceSum($map, $blankI, $blankJ)
{
    $sum = 0;
    for ($i = 0; $i < count($map); $i++) {
        for ($j = 0; $j < count($map[$i]); $j++) {
            if ($map[$i][$j] === 1) {
                $sum += getDistance($blankI, $blankJ, $i, $j);
            }
        }
    }
 
    return $sum;
}
 
fscanf(STDIN, "%d", $num);
 
$map = [];
for ($i = 0; $i < $num; $i++) {
    $input = fgets(STDIN);
    $map[$i] = explode(' ', $input);
}
 
//将数据类型转成整形,php是弱类型语言,担心留坑
for ($i = 0; $i < $num; $i++) {
    for ($j = 0; $j < $num; $j++) {
        $map[$i][$j] = intval($map[$i][$j]);
    }
}
 
$shortDist = -1;//最小距离和
 
$houseCount = 0;
$sumI = 0;
$sumJ = 0;
for ($i = 0; $i < $num; $i++) {
    for ($j = 0; $j < $num; $j++) {
      /*  if ($map[$i][$j] === 0) {
            //计算当前位置的距离和
            $distance = getDistanceSum($map, $i, $j);
 
            if (constant('DEBUG') == 1) {
                echo $i . " " . $j . ":" . $distance . "\n";
            }
 
            if ($distance < $shortDist || $shortDist === -1) {
                $shortDist = $distance;
            }
        }*/
 
        if ($map[$i][$j] === 1) {
            $houseCount++;
            $sumI += $i;
            $sumJ += $j;
        }
    }
}
 
$averageI = intval($sumI / $houseCount);
$averageJ = intval($sumJ / $houseCount);
if (constant('DEBUG') == 1) {
    echo 'average i is ' . $averageI . "\n";
    echo 'average j is ' . $averageJ . "\n";
}
 
for ($i = $averageI - 1; $i <= $averageI + 1 && $i < $num; $i++) {
    if ($i < 0) {
        continue;
    }
 
    for ($j = $averageJ - 1; $j <= $averageJ + 1 && $j < $num; $j++) {
        if ($j < 0) {
            continue;
        }
 
        if ($map[$i][$j] === 0) {
            //计算当前位置的距离和
            $distance = getDistanceSum($map, $i, $j);
 
            if (constant('DEBUG') == 1) {
                echo $i . " " . $j . ":" . $distance . "\n";
            }
 
            if ($distance < $shortDist || $shortDist === -1) {
                $shortDist = $distance;
            }
        }
 
        if ($map[$i][$j] === 1) {
            $houseCount++;
            $sumI += $i;
            $sumJ += $j;
        }
    }
}
 
echo $shortDist . "\n";

大家看上面的代码,其实很不严谨,比如如果没有一座房子,应该会有除以0的边界,如果中心点附近都是房子,没有空地,也会无解。但是居然AC了。
虽然AC了,但是内心没有任何高兴,因为这样通过没任何收获,后面参看了其他几个大神给出的解题报告,加入自己的理解,整出了自己的解决方案。
穷举法之所以会超时是因为遍历所有空地时间复杂度是O(N2),计算每块空地的距离和时间复杂度又是O(N2),所以总的时间复杂度变成了O(N2)XO(N2)=O(N4)。
优化思路是看能不能减少计算每块空地的距离和的时间复杂度,后面经过分析建立了这样的数学模型。
我们建立一个如上图所示的平面直角坐标系,坐标系的左上角为原点,房子或空地坐落在网格上。
我们假定在原点建立物流中转站(即使原点是房子也没关系,不影响计算),假设各个房子到原点的距离和是distance00(可以遍历所有点算出来,这一步省不了),那么在(1,0)坐标出建立中转站的距离和能否便捷的算出来呢。
我们分析发现,将物流中转站从坐标(0,0)迁移到坐标(0,1)时,纵坐标方向上的距离没有任何影响,但是对于横坐标方向的距离有影响,影响如下:
对于横坐标x<=0的房子来说,距离增加了1,对于x>0的房子来说,距离减小了1,因此(0,1)坐标处的距离和为distance00+(x<=0的房子数量)-(x>0的房子数量),为了描述方便,
我们设房子的总数量为house,原点所在线及左边(x<=0)的房子的数量为a0,原点线右边(x>0)的房子总数为house-a0(左边房子数加右边房子数等于房子总数),
那么上面的公式就可以简化为:(0,1)坐标处的距离和为:distance00+a0-(house-a0)=distance+2*a0-house。
利用这个公式就是快速的计算出(0,1),(0,2)......(0,num)坐标的距离。
同样的道理,如果把物流中转站从坐标(0,0)迁移到坐标(1,0)时,假设y<=0的房子数量为b0,则(1,0)处坐标的距离和为:distance00+2*b0-house。
这样,我们只要能统计出a0,a1......anum,b0,b1......bnum的数据,然后计算出(0,0)位置的坐标距离和,就能快速计算出其他位置的距离和。
相对于穷举法,这种计算方式的时间复杂度是O(1)级别(就是四则运算)。
统计出a0,a1......anum,b0,b1......bnum的数据应该比较简单,建立两个辅助数组即可。
贴上代码:
<?php

/**
 *调试开关,1为开启,开启则输出一些中间调试信息
 */
define("DEBUG", 0);

class Solution
{
    /**
     * @var array 地图方阵数据
     */
    private $map = [];

    /**
     * @var array x轴的辅助数组,记录x<=i的房子的总数,对应解题报告中的ai
     */
    private $xHelp = [];

    /**
     * @var array y轴的辅助数组,记录x<=j的房子的总数,对应解题报告中的bj
     */
    private $yHelp = [];

    /**
     * @var array 各个位置的距离值
     */
    private $distances = [];

    /**
     * @var int 正方形的边长
     */
    private $num = 0;

    /**
     * @var int 房子数量,即矩阵中1的总数
     */
    private $house = 0;

    public function __construct()
    {
        $this->init();
        $this->calDistances();
    }

    /**
     * 初始化辅助数组
     */
    private function initHelp()
    {
        for ($i = 0; $i < $this->num; $i++) {
            $this->xHelp[$i] = [];
            $this->yHelp[$i] = [];

            for ($j = 0; $j < $this->num; $j++) {
                $this->xHelp[$i][$j] = 0;
                $this->yHelp[$i][$j] = 0;
            }
        }
    }

    /**
     * 初始化距离和结果数组(各个点的结果)
     */
    private function initDistances()
    {
        for ($i = 0; $i < $this->num; $i++) {
            $this->distances[$i] = [];

            for ($j = 0; $j < $this->num; $j++) {
                $this->distances[$i][$j] = 0;
            }
        }
    }

    /**
     *初始化
     */
    private function init()
    {
        $this->getInput();

        $this->initHelp();
        $this->initDistances();

        //将数据类型转成整形,php是弱类型语言,担心留坑
        for ($i = 0; $i < $this->num; $i++) {
            for ($j = 0; $j < $this->num; $j++) {
                $this->map[$i][$j] = intval($this->map[$i][$j]);

                if ($this->map[$i][$j] === 1) {
                    $this->house++;

                    $this->markXHelp($i);
                    $this->markYHelp($j);
                }
            }
        }
    }

    /**
     * 计算坐标(i1,j1)到坐标(i2,j2)的距离
     * @param int $i1
     * @param int $j1
     * @param int $i2
     * @param int $j2
     */
    private function getDistance($i1, $j1, $i2, $j2)
    {
        return abs($i1 - $i2) + abs($j1 - $j2);
    }

    /**
     * 计算$i $j位置区域到各个房子之间的距离和
     * @param int $blankI 空地坐标i
     * @param int $blankJ 空地坐标j
     */
    private function getDistanceSum($blankI, $blankJ)
    {
        $sum = 0;
        for ($i = 0; $i < $this->num; $i++) {
            for ($j = 0; $j < $this->num; $j++) {
                if ($this->map[$i][$j] === 1) {
                    $sum += $this->getDistance($blankI, $blankJ, $i, $j);
                }
            }
        }

        return $sum;
    }

    /**
     * 如果X轴$i位置有房子,则xHelp数组大于等于位$houseI置的元素值都需要加1
     * @param $houseI
     */
    private function markXHelp($houseI)
    {
        for ($i = $houseI; $i < $this->num; $i++) {
            for ($j = 0; $j < $this->num; $j++) {
                $this->xHelp[$i][$j]++;
            }
        }
    }

    /**
     * @param $houseJ
     */
    private function markYHelp($houseJ)
    {
        for ($i = 0; $i < $this->num; $i++) {
            for ($j = $houseJ; $j < $this->num; $j++) {
                $this->yHelp[$i][$j]++;
            }
        }
    }

    /**
     * 获取输入数据
     */
    private function getInput()
    {
        fscanf(STDIN, "%d", $this->num);

        for ($i = 0; $i < $this->num; $i++) {
            $input = fgets(STDIN);
            $this->map[$i] = explode(' ', $input);
        }
    }

    /**
     * 根据公式计算各个点的距离和,详见解题报告中公式的推导过程
     */
    private function calDistances()
    {
        $this->distances[0][0] = $this->getDistanceSum(0, 0);

        for ($i = 0; $i < $this->num; $i++) {
            if ($i > 0) {
                $this->distances[$i][0] = $this->distances[$i - 1][0] + 2 * $this->xHelp[$i - 1][0] - $this->house;
            }

            for ($j = 1; $j < $this->num; $j++) {
                $this->distances[$i][$j] = $this->distances[$i][$j - 1] + 2 * $this->yHelp[$i][$j - 1] - $this->house;
            }
        }
    }

    /**
     * 根据矩阵数据,计算最小的距离和
     * @return int
     */
    public function getMinDistSum()
    {
        $shortDist = -1;//最小距离和

        for ($i = 0; $i < $this->num; $i++) {
            for ($j = 0; $j < $this->num; $j++) {
                if ($this->map[$i][$j] === 0) {
                    $distance = $this->distances[$i][$j];

                    if (constant('DEBUG') == 1) {
                        echo $i . " " . $j . ":" . $distance . "\n";
                    }

                    if ($distance < $shortDist || $shortDist === -1) {
                        $shortDist = $distance;
                    }
                }
            }
        }

        return $shortDist;
    }
}

$solution = new Solution();
$minDistSum = $solution->getMinDistSum();
echo $minDistSum . "\n";
再贴下我自己的几个测试用例:
Input1:
5
1 1 1 1 1
1 0 0 0 1
1 0 0 0 1
1 0 0 0 1
1 1 1 1 1
Output1:
48

Input2:
5
1 1 1 1 1
1 0 0 0 1
1 0 1 0 1
1 0 0 0 1
1 1 1 1 1
Output2:
51

Input3:
4
0 1 1 0
1 1 0 1
0 0 1 0
0 0 0 0
Output3:
8

Input4:
7
1 1 1 1 1 1 1
1 0 0 0 0 0 1
1 0 1 1 1 0 1
1 0 1 1 1 0 1
1 0 1 1 1 0 1
1 0 0 0 0 0 1
1 1 1 1 1 1 1
Output4:
140

Input5:
7
1 1 1 1 1 1 1
1 0 0 1 0 0 1
1 0 1 1 1 0 1
1 0 1 1 1 0 1
1 0 1 1 1 0 1
1 0 0 1 0 0 1
1 1 1 1 1 1 1
Output5:
148


本人致力于编写通俗易懂的解题报告,如果对上面有任何没看明白的地方,请直接留言回复。我会尽我所能完善解题报告。
我建立了一个git仓库专门存放代码,以及测试用例等资料,仓库地址:https://github.com/S-Knight/nowcoder
如果你觉得本文对您有帮助,记得给我点赞哦,你们的点赞是我持续更新的动力。