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 : 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是<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. 变量
3. 右边浅绿色,与矩阵的
row数
相同的临时存储区,是将当前的
L
列、
4. 变量
5. 变量
maxSum
: 纪录目前遍历下来的最大的子矩阵和;
6. 变量
maxLeft
: 纪录目前遍历下来的最大子矩阵的左边位置;
7. 变量
maxRight
: 纪录目前遍历下来的最大子矩阵的右边位置;
8. 变量
maxUp
: 纪录目前遍历下来的最大子矩阵的上面位置;
9. 变量
maxDown
: 纪录目前遍历下来的最大子矩阵的下面位置;
注意:如果
currentSum
不大于
maxSum
,则保持
maxSum、maxLeft、maxRight、maxUp、maxDown
这几个变量值不变。
第一次遍历,
L、R
都在矩阵的开始 0 处:
第二次遍历, 此时将
R
向右移动一个位置到 1 处,保持
很容易看出,最大值为9,所以
currentSum
为9,那么发现9比之前的
maxSum=4
要大,所以,此时将 9 给
maxSum=9
。
maxLeft=0
纪录此时的
L=0
,
maxRight=1
纪录此时的
R=1
,
maxUp
纪录此时最大子序列的上面开始位置:
maxUp=0
,
maxDown
纪录此时最大子序列的下面结束位置:
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?。即如何找到序列中最大的子序列和并且小于一个给定的值:
直接看大神给的代码吧:
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[ ] 序列中,找到最小的这 i,i<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 = 5
, K = 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
- https://leetcode.com/discuss/109749/accepted-c-codes-with-explanation-and-references
- https://www.youtube.com/watch?v=yCQN096CwWM
- https://www.youtube.com/watch?v=86CQq3pKSUw
- 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
- http://www.hawstein.com/posts/20.12.html
- http://kubicode.me/2015/06/23/Algorithm/Max-Sum-in-SubMatrix/
注:参考5、6是我觉得写的不错的博客,推荐作为扩展阅读