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

Preface

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

一个矩阵的所有子矩阵最大和问题、Kadane算法_第1张图片

  已经好久没做题练练手了,于是想试试。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 
#include 
using namespace std;

#define MAX(X, Y) (X > Y) ? X : Y
#define POS(X) (X > 0) ? X : 0

int 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是文件定义的,代表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: "<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 处:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第2张图片

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

一个矩阵的所有子矩阵最大和问题、Kadane算法_第3张图片

  第三次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第4张图片

  第四次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第5张图片

  第五次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第6张图片

  第六次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第7张图片

  第七次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第8张图片

  第八次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第9张图片

  第九次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第10张图片

  第十次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第11张图片

  第十一次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第12张图片

  第十二次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第13张图片

  第十三次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第14张图片

  第十四次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第15张图片

  第十五次遍历:

一个矩阵的所有子矩阵最大和问题、Kadane算法_第16张图片

  经过十五次的遍历后,我们终于找到了这个矩阵,就是上图中红色区域部分。这个大矩阵( 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) i 0 开始) 号元素之和存放到一个 set 中(注意:set 是按小到大顺序对元素排序的),当你处理前 j 个元素之和 cum[j] 时,你需要在 cum[ ] 序列中,找到最小的这 ii<j ,它的前 i 个序列之和为 cum[i]

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

这就是代码中 set::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是我觉得写的不错的博客,推荐作为扩展阅读

你可能感兴趣的:(LeetCode,Developer)