一個矩陣的所有子矩陣最大和問題、Kadane算法

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. 變量 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是我覺得寫的不錯的博客,推薦作爲擴展閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章