一个矩阵的所有子矩阵最大和问题、Kadane算法

来源:互联网 发布:房地产网络推广 编辑:程序博客网 时间:2024/06/04 18:07

Preface

  今天早上刷微博,看到LeetCode中国微博发了这样一条状态:

这里写图片描述

  已经好久没做题练练手了,于是想试试。LeetCode上,该题目的地址为:https://leetcode.com/problems/max-sum-of-sub-matrix-no-larger-than-k/

Analysis

  想了一上午,也没想出什么头绪。后来我看 LeetCode 上有不少人已经做出提交了。并且,在discuss页面里,有人公布了详细的解释与代码。
  我看了一下,他这个解法是基于Kadane Algorithm了。于是,先得学习一下什么是Kadane Algorithm。

Kadane Algorithm

  Kadane Algorithm 用于解决对一列数组中,求其中子序列的和最大的值。Kadane 的代码很多,各种语言的也都有,我下面摘取这个网站上的C++代码,理解分析一下:

#include <iostream>#include <climits>using namespace std;#define MAX(X, Y) (X > Y) ? X : Y#define POS(X) (X > 0) ? X : 0int kadane(int* row, int len){    int x;    //拿数组的第一个元素出来,若其大于0,则另sum = row[0]    //若其小于或等于0,则令sum = 0,    int sum = POS(row[0]);     int maxSum = INT_MIN; //INT_MIN是<climits>文件定义的,代表int类型最小值:-2147483648    for (x = 0; x < len; ++x)    {           //Kadane 算法的核心部分        //maxSum用于记录最大的子序列和,并每一次与sum进行比较,若当sum比之前的maxSum要大,则将现在的sum值赋予maxSum        //sum每加一个值,跟0进行一次比较,若加完row[x]都小于0了,那么就直接将sum置为0,接着开始一个新的子序列,并进行求和        maxSum = MAX(sum, maxSum);        sum = POS(sum + row[x]);    }    return maxSum;}int main(){    int N;    cout << "Enter the array length: ";    cin >> N;    int arr[N];    cout << "Enter the array: ";    for (int i = 0; i < N; i++)    {        cin >> arr[i];    }    cout << "The Max Sum is: "<<kadane(arr, N) << endl;    return 0;}


2D Kadane Algorithm

  由于我们这一题是二维矩阵,并不是一维数组。因此,要将 kadane 算法扩展到2维上。同样作者也推荐了一个视频,是位印度哥们,讲解的非常好。视频在 YouTube 上,地址:https://www.youtube.com/watch?v=yCQN096CwWM,保证听几遍就懂。
  下面我就他讲解的,用 Excel 表格展示这个二维 kadane 算法的过程。

  如图下面所示的矩阵,黄色黄色部分,4×5 的大小。先定义几个变量:
  1. 变量 L : 代表遍历时,当前子矩阵的左边位置
  2. 变量 R : 代表遍历时,当前子矩阵的右边位置
  3. 右边浅绿色,与矩阵的 row 相同的临时存储区,是将当前的 L 列、L+1 列、……、R1 列、R 列,进行列相加,然后再用 kadane 算法判断相加得到的列数组(此时即为一维数组了,可以用一般意义上的 kadane 算法),求此时元素连续和最大的子数组,并与之前的最大值进行比较(这一点会在下面的过程中体现出来);
  4. 变量 currentSum : 当前 LR 组成的子矩阵(注意:这个子矩阵的“行数量“与原来大矩阵相同),其中这个矩阵的子矩阵,产生的最大的和;
  5. 变量 maxSum : 纪录目前遍历下来的最大的子矩阵和;
  6. 变量 maxLeft : 纪录目前遍历下来的最大子矩阵的左边位置;
  7. 变量 maxRight : 纪录目前遍历下来的最大子矩阵的右边位置;
  8. 变量 maxUp : 纪录目前遍历下来的最大子矩阵的上面位置;
  9. 变量 maxDown : 纪录目前遍历下来的最大子矩阵的下面位置;
  注意:如果 currentSum 不大于 maxSum,则保持 maxSummaxLeftmaxRightmaxUpmaxDown 这几个变量值不变。

  第一次遍历,LR 都在矩阵的开始 0 处:

这里写图片描述

  第二次遍历, 此时将 R 向右移动一个位置到 1 处,保持 L 位置不变。将 LR 两行之间的矩阵进行列相加,得到 3 6 0 0,求这个 3 6 0 0 序列的和最大子序列。
  很容易看出,最大值为9,所以 currentSum 为9,那么发现9比之前的 maxSum=4 要大,所以,此时将 9 给 maxSum=9maxLeft=0 纪录此时的 L=0maxRight=1 纪录此时的 R=1maxUp 纪录此时最大子序列的上面开始位置:maxUp=0maxDown 纪录此时最大子序列的下面结束位置:maxDown=1

这里写图片描述

  第三次遍历:

这里写图片描述

  第四次遍历:

这里写图片描述

  第五次遍历:

这里写图片描述

  第六次遍历:

这里写图片描述

  第七次遍历:

这里写图片描述

  第八次遍历:

这里写图片描述

  第九次遍历:

这里写图片描述

  第十次遍历:

这里写图片描述

  第十一次遍历:

这里写图片描述

  第十二次遍历:

这里写图片描述

  第十三次遍历:

这里写图片描述

  第十四次遍历:

这里写图片描述

  第十五次遍历:

这里写图片描述

  经过十五次的遍历后,我们终于找到了这个矩阵,就是上图中红色区域部分。这个大矩阵(4×5) 的最大元素和为18。

  这就是2D kadane算法的过程。这个算法的空间复杂度为: O(row),时间复杂度为:O(column×column×row)

Find the max sum no more than K

  解决了如何寻找子矩阵的最大和问题,现在题目中还有一个限制。就是这个和不能大于给定的 K,这个作者也推荐了Quora上的一个帖子:Given an array of integers A and an integer k, find a subarray that contains the largest sum, subject to a constraint that the sum is less than k?。即如何找到序列中最大的子序列和并且小于一个给定的值:K,回答这个问题的人也是一位大神。
  直接看大神给的代码吧:

int best_cumulative_sum(int ar[], int N, int K){    set<int> cumset;    cumset.insert(0);    int best = 0, cum = 0;    for(int i = 0; i < N; i++)    {        cum += ar[i];        //upper_bound(), 返回指向容器中第一个值在给定搜索值之后的元素的迭代器        set<int>::iterator sit = cumset.upper_bound(cum - K);        if(sit != cumset.end())            best = max(best, cum - *sit);        cumset.insert(cum);    }    return best;}

First thing to note is that sum of subarray (i,j] is just the sum of the first j elements less the sum of the first i elements. Store these cumulative sums in the array cum. Then the problem reduces to finding i,j such that i<j and cum[j]cum[i] is as close to k but lower than it.
所谓子序列 (i,j] 元素之和,就是这个序列的 j 元素之和减去(less)这个序列的前 i 个元素之和。所以问题转化为找到这样的 i,j(i<j),使得 cum[j]cum[i]尽可能的大,接近给定的限制值 k,但是小于这个 k

To solve this, scan from left to right. Put the cum[i] values that you have encountered till now into a set. When you are processing cum[j] what you need to retrieve from the set is the smallest number in the set such which is bigger than cum[j]k. This lookup can be done in Ologn using upper_bound. Hence the overall complexity is O(nlog(n)).
从左到右的遍历这个序列。将这个序列的前 i(i<N)i0 开始) 号元素之和存放到一个 set 中(注意:set 是按小到大顺序对元素排序的),当你处理前 j 个元素之和 cum[j] 时,你需要在 cum[ ] 序列中,找到最小的这 ii<j,它的前 i 个序列之和为 cum[i]

cum[j]cum[i]<K cum[j]K<cum[i]

这就是代码中set<int>::iterator sit = cumset.upper_bound(cum - K),这一行的由来。

  有些难理解,举个例子。这里,一开始的数组值为:ar[] = [-4 6 -3 8 -9],给定的N = 5K = 12.
  这个函数的变量变化见下表:

这里写图片描述


Show the Code

  解决了这个问题中的两个关键问题,下面就是写这个二维矩阵子矩阵之和最大问题的代码了。下面是作者给出的代码:

int maxSumSubmatrix(vector<vector<int> >& matrix, int k) {    //判断矩阵是否为空矩阵    if (matrix.empty())        return 0;    int row = matrix.size(), col = matrix[0].size(), res = INT_MIN;    //就像前面演示的那样,l代表变量L,r代表变量R    for (int l = 0; l < col; ++l)     {        //之前演示的,临时存储区,与矩阵的row相同,单列;同时,开始值赋予0        vector<int> sums(row, 0);        //r从每一次的l处开始:r = l,直到最右边col:r < col        for (int r = l; r < col; ++r)        {            for (int i = 0; i < row; ++i)            {                //对当前列,加上之前的列(从l开始,到当前的r列),进行列相加。                //即,当r向右移动时,每一行保持之前的值存在sum[i](i: [0,row)),                //接着,再加上新的列(r)上同一行新出现的元素                sums[i] = sums[i] + matrix[i][r];            }            // 对当前的临时存储区的列,求其最大子序列            // 这部分的代码就是上面Quora上的代码            // Find the max subarray no more than K             set<int> accuSet;            accuSet.insert(0);            int curSum = 0, curMax = INT_MIN;            for (int sum : sums)            {                curSum = curSum + sum;                set<int>::iterator it = accuSet.lower_bound(curSum - k);                if (it != accuSet.end())                    curMax = std::max(curMax, curSum - *it);                accuSet.insert(curSum);            }            // 拿当前的最大子矩阵之和与之前求得的最大子矩阵之和做比较,保留最大值            res = std::max(res, curMax);        }    }    return res;}

  这段代码的精华之处太多,应多细细体会。

  至此,这一题解决。

Reference

  1. https://leetcode.com/discuss/109749/accepted-c-codes-with-explanation-and-references
  2. https://www.youtube.com/watch?v=yCQN096CwWM
  3. https://www.youtube.com/watch?v=86CQq3pKSUw
  4. https://www.quora.com/Given-an-array-of-integers-A-and-an-integer-k-find-a-subarray-that-contains-the-largest-sum-subject-to-a-constraint-that-the-sum-is-less-than-k
  5. http://www.hawstein.com/posts/20.12.html
  6. http://kubicode.me/2015/06/23/Algorithm/Max-Sum-in-SubMatrix/

  注:参考5、6是我觉得写的不错的博客,推荐作为扩展阅读

1 0