HDU-1081-“最大子矩阵和”---- 暴力优化:从6次幂到3次幂

题目链接:

http://acm.hdu.edu.cn/showproblem.php?pid=1081
http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=1074


原题如下:

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 
#include 
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 
#include 
#include 
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 
#include 
#include 
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看暴力是否优美…望日后做题思考更加深入。

你可能感兴趣的:(枚举)