1. 程式人生 > >HDU-1081-“最大子矩陣和”---- 暴力優化:從6次冪到3次冪

HDU-1081-“最大子矩陣和”---- 暴力優化:從6次冪到3次冪

題目連結:

原題如下:

Problem
Given a two-dimensional array of positive and negative integers, a sub-rectangle is any contiguous sub-array of size 1 x 1 or greater located within the whole array. The sum of a rectangle is the sum of all the elements in that rectangle. In this problem the sub-rectangle with the largest sum is referred to as the maximal sub-rectangle.

As an example, the maximal sub-rectangle of the array:
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2
is in the lower left corner:
9 2
-4 1
-1 8
and has a sum of 15.

The input consists of an N x N array of integers. The input begins with a single positive integer N on a line by itself, indicating the size of the square two-dimensional array. This is followed by N 2 integers separated by whitespace (spaces and newlines). These are the N 2 integers of the array, presented in row-major order. That is, all numbers in the first row, left to right, then all numbers in the second row, left to right, etc. N may be as large as 100. The numbers in the array will be in the range [-127,127].

Output
Output the sum of the maximal sub-rectangle.

Example
Input
4
0 -2 -7 0 9 2 -6 2
-4 1 -4 1 -1
8 0 -2

Output
15

大概題意如下:
給出一個n*n(n<=100)的二維陣列,這裡面都是整數並且整數的範圍是[-127,127],要求我們找出其中的一個大於等於1*1的子矩陣,要求這個矩陣滿足其中所有數字的和是這個二維陣列中所有子矩陣中最大的。

很暴力的暴力:

題面上來說很好理解,用暴力就能寫出來,寫個6重for迴圈是一個方法。

迴圈步驟的程式碼如下:

for(int i0 = 0; i0 < n; i0++)
    {
        for(int j0 = 0; j0 < n; j0++)
        {
            for(int i1 = i0; i1 < n; i1++)
            {
                for(int j1 = j0; j1 < n; j1++)
                {
                    int temp = 0;
                    for(int p0 = i0; p0 <= i1; p0++)
                    {
                        for(int q0 = j0; q0 <= j1; q0++)
                        {
                            temp += ar[p0][q0];
                        }
                    }
                    if(temp > mx)
                        mx = temp;
                }
            }
        }
    }

解釋一下6重for迴圈:
由於我們要找的是子矩陣,那麼必須就要有一個起點和終點構成對角線來找到這個矩陣,然後求和,對於每個子矩陣的和,記錄下其中最大值即可。
① i0,j0的這兩重迴圈表示“起點的位置”;
② i1,j1的這兩重迴圈表示“終點的位置”,為了減少時間複雜度(這裡減少不了多少,本質沒有發生改變)我把i1,j1分別從i0,j0開始;
△以上這兩部操作遍歷了所有的可能的子矩陣
③ p0,q0和temp表示的所有子矩陣中的求和,每次用temp和mx作比較就能得到最大的mx。

完整程式碼如下:

#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
    int n;
    int ar[105][105] = {};
    int br[105][105] = {};
    cin >> n;
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j< n; j++)
        {
            cin >> ar[i][j];
        }
    }
    int mx = -2147868;
    for(int i0 = 0; i0 < n; i0++)
    {
        for(int j0 = 0; j0 < n; j0++)
        {
            for(int i1 = i0; i1 < n; i1++)
            {
                for(int j1 = j0; j1 < n; j1++)
                {
                    int temp = 0;
                    for(int p0 = i0; p0 <= i1; p0++)
                    {
                        for(int q0 = j0; q0 <= j1; q0++)
                        {
                            temp += ar[p0][q0];
                        }
                    }
                    if(temp > mx)
                        mx = temp;
                }
            }
        }
    }
    cout << mx << endl;
    return 0;
}

令人激動的是,過了樣例。
但是時間超時,想想為什麼會超時:這裡我用了6重for迴圈:n^6,如果n取到最大的100,那麼時間就會達到可怕的10^12,怎麼會不超時?
因此需要對暴力進行優化。

不太優美的暴力:

先考慮這樣的一個問題:二維陣列的遍歷需要從頭到末尾找起點和終點,起點和終點都分別有兩個座標i、j,這樣的話就是4重迴圈了,再加上求和又是2重迴圈;由解高次方程降冪的思想來簡化問題的方式,我去考慮一維陣列的情況,一維陣列的遍歷同樣是需要起點和終點,不過起點和終點都只需要一個座標i就可以,而且遍歷的時候也只需要加1重迴圈就OK,這樣算來可以達到理論上的至少3重迴圈來解決問題。

那如何實現這個二維陣列轉變成一維陣列是現在的問題。
對於一個4*4的二維陣列來說:如果把它的每一列都壓縮成一個數,那就變成了一維,但是這樣的壓縮必須有前提條件,那就是對於每一列數的每一次壓縮方式必須相同,這是保證能拼成矩形的前提。

例如:
第二列壓縮了2、3,第三列壓縮了2、3、4,這顯然不能拼成一個矩形。
因此,我對每一列進行這樣的操作:
定義一個br[n]的陣列,存放每一列(共n列)求和的值(求和是一個壓縮的過程)。
現在去遍歷壓縮的過程:
對於每一列來說,都有相同數量的子串(前提是連續的子串),我們依次對各列同時進行相同的連續子串求和,每求一次就存入br[n]的陣列中,每存一次就求br[n]這個一維陣列的最大子段和,同時不斷更新mx。由於壓縮的時候遍歷了每一列的所有連續子串可能,並且遍歷了br[n]的所有子段的最大值(連續的br[i]和在二維陣列上表示的是一個矩形),所以所有的子矩陣的和都會被遍歷一遍。

現在用圖來表示:
a[1][1] a[1][2] a[1][3] a[1][4]
a[2][1] a[2][2] a[2][3] a[2][4]
a[3][1] a[3][2] a[3][3] a[3][4]
a[4][1] a[4][2] a[4][3] a[4][4]
壓縮指的是分別求出:a[1][j],a[1][j]+a[2][j],a[1][j]+a[2][j]+a[3][j],a[1][j]+a[2][j]+a[3][j]+a[4][j],a[2][j],a[2][j]+a[3][j],a[2][j]+a[3][j]+a[4][j],a[3][j],a[3][j]+a[4][j],a[4][j]。每一次的求和對j列進行的都是相同的,每求一次就存入一次br[n]。
br[1] br[2] br[3] br[4]
存完之後立刻對br找出最大子段,並更新mx。

舉個例子:
br[1] br[2] br[3] br[4]
如果欄位是br[2]和b[3]的和最大,那麼在二維陣列中表示的部分如下圖中的加粗的兩列的某次壓縮。
a[1][1] a[1][2] a[1][3] a[1][4]
a[2][1] a[2][2] a[2][3] a[2][4]
a[3][1] a[3][2] a[3][3] a[3][4]
a[4][1] a[4][2] a[4][3] a[4][4]

以下給出關鍵程式碼:

for(int i = 0; i < n; i++)    //從i行開始的連續子串求和
{
    for(int k = 0; k < n; k++)    //br陣列歸零
        br[k] = 0;
    for(int j = i; j < n; j++)    //當前行向下的連續子串求和
    {
        for(int k = 0; k < n; k++)
            br[k] += ar[j][k];
        for(int k = 0; k < n; k++)    //求一維陣列子段最大
        {
            int sum = 0, temp = -2147483648;
            for(int p = k; p < n; p++)
            {
                sum += br[p];
                temp = (temp > sum) ? temp : sum;
            }
            if(mx < temp)    //不斷更新最大
                mx = temp;
        }

    }
}

完整程式碼如下:

#include <cstdio>
#include <iostream>
#include <cmath>
using namespace std;
int main()
{
    int n, mx = -2147483648;
    int ar[105][105] = {};
    int br[105] = {};
    scanf("%d", &n);
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            scanf("%d", &ar[i][j]);
        }
    }
    for(int i = 0; i < n; i++)
    {
        for(int k = 0; k < n; k++)
            br[k] = 0;
        for(int j = i; j < n; j++)
        {
            for(int k = 0; k < n; k++)
                br[k] += ar[j][k];
            for(int k = 0; k < n; k++)
            {
                int sum = 0, temp = -2147483648;
                for(int p = k; p < n; p++)
                {
                    sum += br[p];
                    temp = (temp > sum) ? temp : sum;
                }
                if(mx < temp)
                    mx = temp;
            }
        }
    }
    cout << mx << endl;
    return 0;
}

從程式碼看出迴圈的重數從6變到了4,即從n^6變到了n^4,如果資料不是很過分的話,應該可以解決問題了。

就怕資料不巧來了個100,就要跑到100^4即10^8還可能會超時。

略優美的暴力:

在以上程式碼中求一維陣列子段最大的時候,考慮這樣的情況:如果br[i]使得sum小於0我們讓sum歸零不取b[i],如果b[i]使得sum大於0的話就繼續向下取,不斷更新最大值。最後總會出現一個最大值。
舉個例子:
br[0] = -1, br[1] = 3, br[2] = -2, br[3] = 5,br[4] = -7.
那麼這個的子段在取的時候:
sum = sum + br[0] = -1然後歸0同時b[0]不取sum = 0,temp = 0;
sum = sum + br[1] = 3然後br[1]取了後更新sum = 3,temp = 3;
sum = sum + br[2] = 1然後br[2]取了後更新sum = 1,temp = 3不變;
sum = sum + br[3] = 6然後br[3]取了更新sum = 6,temp = 6;
sum = sum + br[4] = -1然後br[4]不取更新sum = 0,temp = 6不變。
實際上是求出temp大於等於零且最大時的連續子段和。

注意:這樣的情況可以減少1重for迴圈,但是限制是必須在陣列中存在至少一個大於等於0的元素。

不過陣列中如果全都小於零也好辦,在輸入時進行最大值的更新,然後一步判斷最大值是否小於0,如果是就直接輸出,如果不是就進行上述過程。

關鍵程式碼:

for(int i = 0; i < n; i++)
{
    for(int j = 0; j < n; j++)
    {
        scanf("%d", &ar[i][j]);
        mx = (mx > ar[i][j]) ? mx : ar[i][j];    //更新最大
    }
}
for(int i = 0; i < n; i++)
{
    for(int k = 0; k < n; k++)
        br[k] = 0;
    for(int j = i; j < n; j++)
    {
        for(int k = 0; k < n; k++)
            br[k] += ar[j][k];
        int sum = 0, temp = -2147483648;
        for(int k = 0; k < n; k++)
        {
            sum += br[k];
            if(sum < 0)    //小於0就不取
                sum = 0;
            if(sum > temp)    //最大子段
                temp = sum;
        }
        if(mx < temp)    //更新mx
            mx = temp;
    }
}

完整程式碼:

#include <cstdio>
#include <iostream>
#include <cmath>
using namespace std;
int main()
{
    int n, mx = -2147483648, m = 0;
    int ar[105][105] = {};
    int br[105] = {};
    scanf("%d", &n);
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            scanf("%d", &ar[i][j]);
            mx = (mx > ar[i][j]) ? mx : ar[i][j];
            if(mx < 0)
                m++;
        }
    }
    if(m == n*n)
        cout << mx << endl;
    else
    {
        for(int i = 0; i < n; i++)
        {
            for(int k = 0; k < n; k++)
                br[k] = 0;
            for(int j = i; j < n; j++)
            {
                for(int k = 0; k < n; k++)
                    br[k] += ar[j][k];
                int sum = 0, temp = -2147483648;
                for(int k = 0; k < n; k++)
                {
                    sum += br[k];
                    if(sum < 0)
                        sum = 0;
                    if(sum > temp)
                        temp = sum;
                }
                if(mx < temp)
                    mx = temp;
            }
        }
        cout << mx << endl;
    }
    return 0;
}

這樣的話在輸入的階段就可以提前知道是否有大於等於0的數,沒有的話直接輸出最大負數,不需要任何操作,n^2級別不會超時;存在大於等於0的數時會進行剛才敘述的3重迴圈,僅僅是n^3,就算資料n達到了100,也只是10^6,不會超時。

筆者本人水平有限,當下僅能思考至此,如有更好的辦法,望多多指教。

小結:
是否T看暴力是否優美…望日後做題思考更加深入。