每天写一点代码----最大子段和问题。

    1. 问题描述:

      有n个数(以下都视为整数),每个数有正有负,现在要在n个数中选取相邻的一段,使其和最大,输出最大的和。(最初看到这个问题是在《编程之美》这本书上)。例如:对于这样一组数 -10, 5 ,2 ,3 ,4 ,-5 ,-23 ,3 ,7 ,-21 .   其最大子段和为 14  (最大字段为 5,2,3,4)。

     2.问题分析:

      方法一: 首先想到的是遍历这一组数中的所有子段,然后比较,找到和最大的子段。( 用i和j表示序列i到j的子段,然后用k求i到j之间的和,然后判断这个子段的和是否是当前已计算的子段中的最大的,是就记录,直到遍历完这组数的所有子段。该算法的时间复杂度为 O(n^3)  )。代码如下:

/**
     * 常规算法, 枚举
     * 
     * @param a
     * @return
     */
    private int maxSubArr1(int[] a) {
        int maxSub = Integer.MIN_VALUE;
        for (int i = 0; i < a.length; i++) {
            for (int j = i; j < a.length; j++) {
                int sumSofar = 0;
                for (int k = i; k <= j; k++) {
                    sumSofar += a[k]; // 计算 a[i]到a[j] 这个子段的和。
                }
                if (sumSofar > maxSub) {// 判断该子段和是否是当前最大的
                    maxSub = sumSofar;
                }
            }
        }
        return maxSub;
    }



      方法二上面的算法中在计算 a[i]到a[j]这个子段的和时用的是一个for循环,但事实上们可以令一个数组sum,sum[i]代表了序列从1到i的和,这样 a[i]到a[j] 这个子段的和 可以用 sum[j] - sum[i-1],这样就可以去掉方法一种的第三重循环 ,使其时间复杂度将为 O(n^2); 代码如下:

   /**
     * 常规算法。枚举优化
     * 
     * @param a
     * @return
     */
    private int maxSubArr2(int[] a) {
        int sum[] = new int[a.length];
        sum[0] = a[0];
        for (int i = 1; i < a.length; i++) {
            sum[i] = sum[i - 1] + a[i];
        }

        int maxSub = Integer.MIN_VALUE;
        for (int i = 0; i < a.length; i++) {
            for (int j = i; j < a.length; j++) {
                int sumSofar = 0;
                // 计算 a[i]到a[j] 这个子段的和。
                if (i > 0) {
                    sumSofar = sum[j] - sum[i - 1];
                } else {
                    sumSofar = sum[j];
                }
                if (sumSofar > maxSub) {// 判断该子段和是否是当前最大的
                    maxSub = sumSofar;
                }

            }
        }
        return maxSub;
    }
 


      方法三:分治法。我们不妨从小规模数据分析,当序列只有一个元素的时候,最大的和只有一个个可能,就是选取本身;当序列有两个元素的时候,只有三种可能,选取左边元素、选取右边元素、两个都选,这三个可能中选取一个最大的就是当前情况的最优解;对于多个元素的时候,最大的和也有三个情况,从左区间中产生、从右区间产生、左右区间各选取一段。因此不难看出,这个算法是基于分治思想的,每次二分序列,直到序列只有一个元素或者两个元素。当只有一个元素的时候就返回自身的值,有两个的时候返回3个中最大的,有多个元素的时候返回左、右、中间的最大值。因为是基于二分的思想,所以时间效率能达到O(nlgn). 代码如下:

/**
     * 分治法
     * 
     * @param a
     * @param left
     * @param right
     * @return
     */
    private int maxSubArr3(int[] a, int left, int right) {
        if (left > right) {
            return 0;
        }
        int m = (left + right) / 2;
        if (left == right) { // 边际条件:当区间元素只有一个的时候返回自身
            return a[m];
        }
        if (right - left == 1) { // 边际条件:当区间元素只有两个的时候返回左、右、左右相加三者中的最大值
            return Math.max(Math.max(a[left], a[right]), a[left] + a[right]);
        }
        // 计算左区间的最大子段和。
        int Lmax = maxSubArr3(a, left, m);
        // 计算右区间的最大子段和
        int Rmax = maxSubArr3(a, m + 1, right);

        int CmaxL = Integer.MIN_VALUE, CmaxR = Integer.MIN_VALUE;
        int temp = 0;
        for (int i = m; i >= 0; i--) {// 左边找一个最大的和(必须包含端点 a[m])
            temp += a[i];
            if (temp > CmaxL) {
                CmaxL = temp;
            }
        }

        temp = 0;
        for (int i = m + 1; i <= right; i++) {// 右边找一个最大的和(必须包含端点 a[m+1])
            temp += a[i];
            if (temp > CmaxR) {
                CmaxR = temp;
            }
        }

        // 返回左区间的和、右区间的和、两者连起来的和中最大的
        return Math.max(Math.max(Lmax, Rmax), CmaxL + CmaxR);

    }



      方法四:动态规划。我们令一个数组sum,sum[i]表示前i个元素能组成的最大和。如果sum[i-1]大于零,则不管a[i]的情况,sum[i-1]都可以向正方向影响a[i],因此可以将a[i]加在sum[i-1]上。如果sum[i-1]小于零,则不管a[i]再大,都会产生负影响,因此我们还不如直接令sum[i]=a[i]。因此状态转移方程就在这里。我们只需在f中扫描一次,找到最大的值就是最大子段的和。

/**
     * 动态规划算法
     * 
     * @param a
     * @return
     */
    private int maxSubArr4(int a[]) {
        int max = Integer.MIN_VALUE;
        int sum[] = new int[a.length];
        sum[0] = a[0];
        for (int i = 1; i < a.length; i++) {
            if (sum[i - 1] > 0) { // 如果第 i 个数后面一个数能构成的最大子段和大于 0
                sum[i] = sum[i - 1] + a[i]; // 大于就将第 i 个数加入其中
            } else {
                sum[i] = a[i]; // 否则第 i 个数自己组成一个最大子序列
            }
            if (sum[i] > max) {// 更新最大值
                max = sum[i];
            }
        }

        return max;
    }

OK。



     



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值