今天早上刷微博,看到LeetCode中国微博发了这样一条状态:
想了一上午,也没想出什么头绪。后来我看 LeetCode 上有不少人已经做出提交了。并且,在discuss页面里,有人公布了详细的解释与代码。
我看了一下,他这个解法是基于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;
}
由于我们这一题是二维矩阵,并不是一维数组。因此,要将 kadane 算法扩展到2维上。同样作者也推荐了一个视频,是位印度哥们,讲解的非常好。视频在 YouTube 上,地址:https://www.youtube.com/watch?v=yCQN096CwWM,保证听几遍就懂。
下面我就他讲解的,用 Excel 表格展示这个二维 kadane 算法的过程。
如图下面所示的矩阵,黄色黄色部分, 4×5 的大小。先定义几个变量:
1. 变量 L : 代表遍历时,当前子矩阵的左边位置;
2. 变量 R : 代表遍历时,当前子矩阵的右边位置;
3. 右边浅绿色,与矩阵的 row数 相同的临时存储区,是将当前的 L 列、 L+1 列、……、 R−1 列、 R 列,进行列相加,然后再用 kadane 算法判断相加得到的列数组(此时即为一维数组了,可以用一般意义上的 kadane 算法),求此时元素连续和最大的子数组,并与之前的最大值进行比较(这一点会在下面的过程中体现出来);
4. 变量 currentSum : 当前 L、R 组成的子矩阵(注意:这个子矩阵的“行数量“与原来大矩阵相同),其中这个矩阵的子矩阵,产生的最大的和;
5. 变量 maxSum : 纪录目前遍历下来的最大的子矩阵和;
6. 变量 maxLeft : 纪录目前遍历下来的最大子矩阵的左边位置;
7. 变量 maxRight : 纪录目前遍历下来的最大子矩阵的右边位置;
8. 变量 maxUp : 纪录目前遍历下来的最大子矩阵的上面位置;
9. 变量 maxDown : 纪录目前遍历下来的最大子矩阵的下面位置;
注意:如果 currentSum 不大于 maxSum ,则保持 maxSum、maxLeft、maxRight、maxUp、maxDown 这几个变量值不变。
第一次遍历, L、R 都在矩阵的开始 0 处:
第二次遍历, 此时将 R 向右移动一个位置到 1 处,保持 L 位置不变。将 L、R 两行之间的矩阵进行列相加,得到 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 :
第三次遍历:
第四次遍历:
第五次遍历:
第六次遍历:
第七次遍历:
第八次遍历:
第九次遍历:
第十次遍历:
第十一次遍历:
第十二次遍历:
第十三次遍历:
第十四次遍历:
第十五次遍历:
经过十五次的遍历后,我们终于找到了这个矩阵,就是上图中红色区域部分。这个大矩阵( 4×5 ) 的最大元素和为18。
这就是2D kadane算法的过程。这个算法的空间复杂度为: O(row) ,时间复杂度为: O(column×column×row)
解决了如何寻找子矩阵的最大和问题,现在题目中还有一个限制。就是这个和不能大于给定的 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[ ] 序列中,找到最小的这 i,i<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 = 5
, K = 12
.
这个函数的变量变化见下表:
解决了这个问题中的两个关键问题,下面就是写这个二维矩阵子矩阵之和最大问题的代码了。下面是作者给出的代码:
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;
}
这段代码的精华之处太多,应多细细体会。
至此,这一题解决。
注:参考5、6是我觉得写的不错的博客,推荐作为扩展阅读