一招教你巧用递归解决矩阵最大序列和问题

之前同事问了一道需要点脑洞的算法题,我觉得蛮有意思的,思路可能会给大家带来一些启发,在此记录一下

题目

现有一个元素仅为 0,1 的 n 阶矩阵,求连续相邻(水平或垂直,不能有环)值为 1 的元素组成的序列和的最大值。假设有如下矩阵

则此矩阵连续相邻值为 1 的元素组成的序列和分别为 4, 3,(如图示),可知这个矩阵符合条件的序列和的最大值为 4

解题思路

要算序列和的最大值,我们可以先找出所有可能的序列,自然就找到了序列和的最大值,那怎么找这些序列呢?首先我们发现,每个序列的起点和终点必然是 1,我们可以遍历矩阵的每一个元素,如果元素值为 1,则将其作为序列的起点开始查找所有以这个元素为起点的序列,我们知道序列是可以向垂直和水平方向延伸的,所以我们可以以这个元素为起点,查找它的上下左右值为 1 的元素,再以找到的这些元素为起点,继续在元素的上下左右查找值为 1 的元素,以此类推(递归),如果找不到符合条件的值,则序列终止,在遍历过程中保存每条序列遍历的元素,即可求得每条符合条件的序列,从而求得序列和的最大值

文字说得有点绕,接下来我们就以查找如下矩阵的最大序列和为例来详细看一下如何查找最大序列和

1. 从左到右,从上到下遍历所有值为 1 的元素,第一个符合条件的元素在右上角,所以以这个元素为起点来查找序列? ?

2. 以这个元素为起点,查找这个元素上下左右为值为 1 的元素,发现只有这个元素下面的元素符合条件?

3. 再以这个元素为起点查找这个元素前后左右值为 1 的元素,可以看到这个元素的上,左元素值为 1,左边的元素显然符合条件,而上面的元素由于是当前正在遍历序列中遍历过的元素,所以不符合条件(假设上面的元素符合条件,会发生什么?接下来会寻找以上面元素为起始点的序列,又回到了第一步,陷入无限循环,所以元素的下一个值为 1 的元素不能是当前正在遍历的序列中的元素!,这一点是解题的关键,务必要注意!)由此可知此时符合条件的元素如下红圈所示

4. 再寻找此元素上下左右都为 1 的元素,可以看到这个元素的左右下的元素都为 1,根据上一步的分析可知,右元素是当前正在遍历序列中已遍历过的元素,所以不符合条件,那么只剩下左,下元素符合条件

  1. 再次寻找这两个元素上下左右皆为 1 的元素,可知符合条件的元素为步骤 3 中的红框元素,由于此元素是当前正在遍历序列中已遍历过的元素,所以不符合条件,序列的遍历到此终止,至此我们可以知道,从右上角元素为起点的序列和的最大值为 4 ,连接遍历过的元素,如图示

6. 同理接下来再按照以上的步骤依次遍历剩余的值为 1 的元素,可知以这些元素为起点的序列和的最大值分别为 4, 3, 3, 4(如下图)

7. 综上可知,此矩阵连续相邻值为 1 的元素的序列和的最大值为 4

代码实现

好了,知道了解题思路,现在我们来看下代码该如何实现,首先我们要用一个数据结构来表示矩阵,显然矩阵用数组表示很合适,这里我们用一维数组来表示矩阵,Java 代码如下

public class Matrix {
    /**
     * @param matrix  矩阵
     * @param dimension 代表 dimension 阶矩阵
     * @return 矩阵序列的最大值
     */
    private static Integer getMaxSequetialSum(int[] matrix, int dimension) {
        int count = matrix.length;      // 矩阵的元素个数
        int maxSequentialSum = 0;       // 矩阵序列的最大值
        // 逐个遍历元素
        for (int index = 0; index < count; index++) {
            int elementValue = matrix[index];
            // 如果当前元素为1,则以此元素为起点,查找以此元素为起点的序列的和的最大值
            if (elementValue == 1) {
                // 记录以下标为 index 的元素为起点的序列遍历过的元素位置
                Set<Integer> traverseElementSet = new HashSet<>();
                traverseElementSet.add(index);
                // 以下标值为 index 的元素为起点的序列的最大值
                int currentSequetialSum = getCurrentVerticeSequetialSum(matrix, traverseElementSet, index, dimension);
                maxSequentialSum = Math.max(maxSequentialSum, currentSequetialSum);
            }
        }
        return maxSequentialSum;
    }

    /**
     * @param matrix  矩阵
     * @param traverseElementSet 序列中已遍历过的元素的位置
     * @param index     元素的位置,序列的起点
     * @param dimension dimension 阶矩阵
     * @return 以位置为 index 的元素为起点的序列的最大值
     */
    private static Integer getCurrentVerticeSequetialSum(int[] matrix, Set<Integer> traverseElementSet, int index, int dimension) {
        // 查找 矩阵中位置为 index 的元素上下左右元素对应的位置
        int left = index - 1;
        int right = index + 1;
        int up = index - dimension;
        int down = index + dimension;

        // 以左元素为起点的序列的最大值
        int leftIndexSum = 0;

        // 以右元素为起点的序列的最大值
        int rightIndexSum = 0;

        // 以上元素为起点的序列的最大值
        int upIndexSum = 0;

        // 以下元素为起点的序列的最大值
        int downIndexSum = 0;

        /**
         * 以下四个 if else 旨在检查每一个元素位置的有效性,值必须为 1
         * 需要注意的是元素不能是序列已遍历过的元素!
         * 如果上下左右元素不合法,则序列终止,打点此遍历序列的元素和
         */

        if (left >= 0 && matrix[left] == 1 && !traverseElementSet.contains(left)) {
            Set<Integer> leftTraverseElementSet = new HashSet<>(traverseElementSet);
            leftTraverseElementSet.add(left);
            leftIndexSum = getCurrentVerticeSequetialSum(matrix, leftTraverseElementSet, left, dimension);
        } else {
            leftIndexSum = traverseElementSet.size();
        }

        // 右元素必须与位置为index的元素在同一行上
        if (right / dimension == index / dimension && matrix[right] == 1 && !traverseElementSet.contains(right)) {
            traverseElementSet.add(right);
            Set<Integer> rightTraverseElementSet = new HashSet<>(traverseElementSet);
            rightTraverseElementSet.add(right);
            rightIndexSum = getCurrentVerticeSequetialSum(matrix, rightTraverseElementSet, right, dimension);
        } else {
            rightIndexSum = traverseElementSet.size();
        }

        if (up >= 0 && matrix[up] == 1 && !traverseElementSet.contains(up)) {
            Set<Integer> upTraverseElementSet = new HashSet<>(traverseElementSet);
            upTraverseElementSet.add(up);
            upIndexSum = getCurrentVerticeSequetialSum(matrix, upTraverseElementSet, up, dimension);
        } else {
            upIndexSum = traverseElementSet.size();
        }

        if (down < matrix.length && matrix[down] == 1 && !traverseElementSet.contains(down)) {
            Set<Integer> downTraverseElementSet = new HashSet<>(traverseElementSet);
            downTraverseElementSet.add(down);
            downIndexSum = getCurrentVerticeSequetialSum(matrix, downTraverseElementSet, down, dimension);
        } else {
            downIndexSum = traverseElementSet.size();
        }

        // 查找以位置为 index 的元素为起点各向上下左右延伸的序列的最大值
        return Collections.max(Arrays.asList(leftIndexSum, rightIndexSum, upIndexSum, downIndexSum));
    }

    public static void main(String[] args) {
        // 初始化矩阵,假设此矩阵为 5 x 5 矩阵
        int[] matrix1 = {
                0,0,0,0,1,
                0,0,1,1,1,
                0,0,0,1,0,
                0,0,0,0,0,
        };
        int max = Matrix.getMaxSequetialSum(matrix1, 5);
        System.out.println(max);  // 打印4

        int[] matrix2 = {
                0,0,0,0,1,
                0,0,1,1,1,
                0,0,1,1,0,
                0,0,0,0,0,
        };
        max = Matrix.getMaxSequetialSum(matrix2, 5);
        System.out.println(max);  // 打印6
    }
}

##时间复杂度与空间复杂度分析

任何算法,如果不谈时间复杂度与空间复杂度都是耍流氓,接下来我们看下以上解法的时间复杂度和空间复杂度。首先来看空间复杂,由于在在遍历过程中我们用了记录遍历序列元素位置的 traverseElementSet,所以空间复杂度显然是 O(n),这道题用了递归,时间复杂度确实挺复杂的,也比较考验程序员的水平,直观上看不出来,那我们看下怎么推导,我们用 f(n) 来表示以位置为 n 的元素为起点的序列和的计算次数,从以上的推导可知,只要计算出以此元素的上下左右元素为起点的序列和的最大值,也自然知道了 f(n)。即计算以位置 n 为起点的序列和次数换算成计算以此元素的上下左右元素为起点的序列和的次数

f(n) = f(左) + f(右) + f(上) + f(下)

仔细考虑一下可知以上下左右四个元素为起点的序列和的计算次数可以认为是一样的从而有 f(n) = 4f(左) 假设矩阵元素个数为N,则f(n) = 4N由于有 N 个元素,所以可知总的时间复杂度为 O(4N<sup>2</sup>),即 O(n<sup>2</sup>),如果你有更优的时间复杂度解法,欢迎一起探讨!

总结

这道题乍一看确实没什么头绪,无法像反转二叉树那样比较容易地看出使用递归的思路去解决,我们需要耐心地去分析,学会把问题分解,分解思路如下:求连续序列的最大值转化为如何求所有的序列 ----> 观察到序列起点的元素必须是 1 ----> 想到如何找寻以值为 1 的元素为起点的所有序列 ----> 只要找到以这个元素上下左右值为 1 的元素为起点的所有序列和 ----> 再以上下左右元素值为 1 的元素为起点递归找寻以它们各自的上下左右值为 1 的元素为起点的所有序列 ----> 找到所有的序列后自然就找到了最大序列。

转载自公众号:码海

原文地址:https://blog.51cto.com/14570694/2466500

时间: 2024-07-29 10:43:01

一招教你巧用递归解决矩阵最大序列和问题的相关文章

巧用递归解决矩阵最大序列和问题

之前同事问了一道需要点脑洞的算法题,我觉得蛮有意思的,思路可能会给大家带来一些启发,特意在此记录一下 题目 现有一个元素仅为 0,1 的 n 阶矩阵,求连续相邻(水平或垂直,不能有环)元素值为 1 的序列和的最大值 假设有如下矩阵 则此矩阵连续相邻元素为 1 的序列和分别为 4, 3,(如图示),可知这个矩阵序列和的最大值为 4 解题思路 要算序列和的最大值,我们可以先找出所有可能的序列和,然后取其中的最大值,那怎么找这些序列呢? 首先我们发现,每个序列的起点和终点必然是 1,我们可以遍历矩阵的

汉诺塔递归解决方法经典分析

一位法国数学家曾编写过一个印度的古老传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针.印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔.不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片:一次只移动一片,不管在哪根针上,小片必须在大片上面.僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔.庙宇和众生也都将同归于尽. 虽然这只是一个传说,但也给我们提出了一个问题,

递归解决换零钱问题--回顾总结之递归的表达能力

前面为了保持叙述的流畅,没有做太多的引申,把总结推迟到了后面. 补上一些总结,以防止出现"下面呢?下面没有了"的尴尬. 方向性问题 虽然题目在一开始就暗示了这一点,但首先,我们还是要问,它能用递归解决吗? 有点怀疑精神是好的,既要低头走路,更要抬头看路,以防止发生方向性错误,导致缘木求鱼的后果. 说这个问题能用递归解决,这种信心或者判断的依据来自于哪呢? 有人可能知道了,换零钱这个问题在<计算机程序的构造和解释>(SICP:Structure and Interpretat

hive使用技巧(四)——巧用MapJoin解决数据倾斜问题

相关文章推荐: hive使用技巧(一)自动化动态分配表分区及修改hive表字段名称 hive使用技巧(二)--共享中间结果集 hive使用技巧(三)--巧用group by实现去重统计 hive使用技巧(四)--巧用MapJoin解决数据倾斜问题 Hive的MapJoin,在Join 操作在 Map 阶段完成,如果需要的数据在 Map 的过程中可以访问到则不再需要Reduce. 小表关联一个超大表时,容易发生数据倾斜,可以用MapJoin把小表全部加载到内存在map端进行join,避免reduc

本文来自作者?余博伦?在?GiCa?上分享 「韭菜种四招教你学会甄别

Docker默认空间大小分为两个,一个是池空间大小,另一个是容器空间大小.池空间大小默认为:100G容器空间大小默认为是:10G所以修改空间大小也分为两个:这 如今已经大获市场成功的<王者荣耀>一直是业内各方关注的对象,而我们也知道这款产品在成为国民级游戏之 如果你有了还要创建密码,gi会提示你是否需要覆盖(y n)?Y:确认????????N:取消 如图:生成如上图所示标识生成成功了.其存放路径为 在我理解,面向对象是向现实世界模型的自然延伸,这是一种"万物皆对象"的编程

怎么把PDF文件转换成Word?三招教你轻松搞定

PDF和Word两个文件之间,相互转换是在我们的工作中经常需要用到的,也曾经是很多职场新人的困扰,很多人都想知道怎么把PDF文件转换成Word?今天呢就来给大家分享三个简单的方法,只需三招就能轻松搞定. 方法1:打开方式为Word 怎么把PDF文件转换成Word?这个方法其实很简单的,只需要点击PDF文档,选择打开方式为Word,利用Word打开即可.步骤:点击PDF文件--右击鼠标--选择[打开方式]--点击Word:方法2:专业转换器 如果在觉得通过Word打开方式不太好用的话,可以使用专业

10招教你解决 Windows 7无法开机

Windows 7先进的功能及方便的界面令人耳目一新!而且比起XP及Vista来说,Win 7又更加稳定了许多,不过再怎么稳定的系统也会因为某些因素而当机,万一哪天当机当到死机了,该如何自救呢?这边要教你怎么修复无法开机的情况,让Win 7故障也能不必重灌! Win 7自动帮你诊断修复错误 在电脑无法开机时,千万别着急,其实Win 7已经针对无法开机的情形有很大的改进,只要是轻微的系统问题,两三下就能用内建工具排除,假使排除不了也有好几种应对方式,让你不再看到电脑无法开机就干着急喔! Step

一招教你解决大数据量下的各种报表使用问题

在我们日常制作报表分析过程中,总会遇到各种问题.比如,报表底层数据日益增多.报表加载超慢,这些情况该怎么解决? 数据库是最常见的能处理大数据的计算方案,而永洪能利用数据库来完成数据计算.但是,有些报表的计算较为复杂,使用SQL实现会非常困难,这时,我们就会采用永洪报表呈现的直观计算方式来完成数据的计算,这种方式导致后台要直接处理大数据,不仅性能低下,而且很容易造成报表加载时间过长. 今天这篇文章,主要介绍永洪数据集市数据集,这种方式提供强大.便捷的数据处理方式,用户可以轻松应对大数据量场景下报表

怎样在ps中制作对话气泡?一招教你轻松解决

PS是在工作中经常使用的平面设计软件,利用ps可以实现很多操作.换天,换发色,添加亮灯等操作都是比较常见的,今天将为大家分享怎样在ps中制作对话气泡的方法,希望能给大家带来帮助.绘制工具:PS绘制方法:1.在新建文件中创建一个线的画布,设定需要的像素大小,开始下一步的编辑.2.在左侧的工具栏中选择形状工具确定所要绘制气泡的样式,长按鼠标左键在面板中画出相同形状的图形.3.这时要对颜色进行选择,选择前景色中的RGB颜色按住AIL+Delete对气泡颜色进行填充.4.在左侧工具栏中选择字体操作,对字