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

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

题目

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

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

解题思路

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

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

  1. 从左到右,从上到下遍历所有值为 1 的元素,第一个符合条件的元素在右上角,所以以这个元素为起点来查找序列
  2. 以这个元素为起点,查找这个元素上下左右为值为 1 的元素,发现只有这个元素下面的元素符合条件
  3. 再以这个元素为起点查找这个元素前后左右值为 1 的元素,可以看到这个元素的上
    ,左元素值为 1,左边的元素显然符合条件,而上面的元素由于是当前正在遍历序列中遍历过的元素,所以不符合条件(假设上面的元素符合条件,会发生什么?接下来会寻找以上面元素为起始点的序列,又回到了第一步,陷入无限循环,所以元素的下一个值为 1 的元素不能是当前正在遍历的序列中的元素!,这一点是解题的关键,务必要注意!)
    由此可知此时符合条件的元素如下红圈所示
  4. 再寻找此元素上下左右都为 1 的元素,可以看到这个元素的左右下的元素都为 1,根据上一步的分析可知,右元素是当前正在遍历序列中已遍历过的元素,所以不符合条件,那么只剩下左,下元素符合条件
  5. 再次寻找这两个元素上下左右皆为 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
    }
}

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

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

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

仔细考虑一下可知以上下左右四个元素为起点的序列和的计算次数可以认为是一样的
从而有
f(n) = 4f(左)
假设矩阵元素个数为N,则
f(n) = 4N
由于有 N 个元素,所以可知总的时间复杂度为 O(4N2),即 O(n2)

总结

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

个人微信号「geekoftaste」,欢迎加微信一起交流,共同进步

原文地址:https://www.cnblogs.com/xiekun/p/11802596.html

时间: 2024-11-12 01:03:36

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

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

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

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

一位法国数学家曾编写过一个印度的古老传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针.印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的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

python动态演示动态规划解决矩阵连乘

矩阵连乘:给定n个矩阵:A1,A2,...,An,其中Ai与Ai+1是可乘的,i=1,2...,n-1.确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少.输入数据为矩阵个数和每个矩阵规模,输出结果为计算矩阵连乘积的计算次序和最少数乘次数. 若A是一个p*q的矩阵,B是一个q*r的矩阵,则其乘积C=AB是一个p*r的矩阵.数乘次数是p*q*r. 动态规划算法与分治法类似,其基本思想也就是将待求解的问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解,简

使用RNN解决NLP中序列标注问题的通用优化思路

/* 版权声明:可以任意转载,转载时请标明文章原始出处和作者信息 .*/ author: 张俊林 序列标注问题应该说是自然语言处理中最常见的问题,而且很可能是最而没有之一.在深度学习没有广泛渗透到各个应用领域之前,传统的最常用的解决序列标注问题的方案是最大熵.CRF等模型,尤其是CRF,基本是最主流的方法.随着深度学习的不断探索和发展,很可能RNN模型会取代CRF的传统霸主地位,会成为解决序列标注问题的标配解决方案. 本文主要抽象出利用RNN解决序列标注问题的通用优化思路.这个RNN优化思路应该

使用递归解决一些问题

1:字符串反转 "abcde"=>"edcba" 直接贴代码: private static String f(String str) {  if(str.length()==0) return"";  return f(str.substring(1))+str.charAt(0); } 2:将 ABCD全排列 分析: A B C D 对于以上递归我们只要找到解决问题的相似处,然后在找到出口,即可解决.对于该问题,我们试着将A与其他元素依

转:使用RNN解决NLP中序列标注问题的通用优化思路

http://blog.csdn.net/malefactor/article/details/50725480 /* 版权声明:可以任意转载,转载时请标明文章原始出处和作者信息 .*/ author: 张俊林 序列标注问题应该说是自然语言处理中最常见的问题,而且很可能是最而没有之一.在深度学习没有广泛渗透到各个应用领域之前,传统的最常用的解决序列标注问题的方案是最大熵.CRF等模型,尤其是CRF,基本是最主流的方法.随着深度学习的不断探索和发展,很可能RNN模型会取代CRF的传统霸主地位,会成

【Android自定义ViewGroup】不一样的轮子,巧用类变量解决冲突,像IOS那样简单的使用侧滑删除,一个控件搞定Android item侧滑删除菜单。

================================================================================== [1 序言] 侧滑删除的轮子网上有很多,最初在github上看过一个,还是ListView时代,那是一个自定义ListView 实现侧滑删除的,当初就觉得这种做法不是最佳,万一我项目里又同时有自定义ListView的需求,会增加复杂度. 写这篇文章之前又通过毒度搜了一下,排名前几的CSDN文章,都是通过自定义ListVIew和Vie