zoukankan      html  css  js  c++  java
  • [算法]一次商品交易利益最大化

    [问题背景]

    已知某个商品一段时间的价格,求在某一天买进,然后在之后的某一天卖出,求最大利益。

    [问题分析](着急看算法的小盆友请直接跳过这一节)

    不难发现,有个很特殊的情况,如果价格一直在涨,显然第一天买进,最后一天卖出能够获得最大利益。所以,当价格有涨有跌的时候,这个问题才有意义。

    首先,最容易的想法是暴力搜索。我们可以枚举买进和卖出的日期,然后计算对应的获利,最后得到一个最大利益。这种方式的时间复杂度是O(n^2)的,如下(c代码):

     1 #include <stdio.h>
     2 #define MaxN 120
     3 #define INF 0x3f
     4 int main()
     5 {
     6     int a[MaxN], n;
     7     int i, j;
     8     int Ans, Delta, st = 0, ed = 0;
     9     scanf("%d", &n);
    10     for(i = 1; i <= n; i++)
    11         scanf("%d", &a[i]);
    12     /*暴力枚举买入日期和卖出日期,然后计算其中的获利*/
    13     Ans = -INF;
    14     for(i = 1; i < n; i++) //显然不能在最后一天买入 
    15         for(j = i + 1; j <= n; j++) //至少在后一天卖出
    16         {
    17             Delta = a[j] - a[i];
    18             if(Ans < Delta)
    19             {
    20                 Ans = Delta;
    21                 st = i;
    22                 ed = j;
    23             }
    24         }
    25     printf("获利:%d元
    在第%d天买入,在第%d天卖出
    ", Ans, st, ed);
    26     return 0;
    27 }
    View Code

    然后,我们觉得这种方法的时间复杂度太高了,于是思考更优的方法。

    有一种尝试,将原来的保存每天的价格的数组改变一下,变成保存当前价格相对于前一天的变化。为了叙述方便,我们将先前的保存每天的价格的数组记为a[],其中a[i]表示第i天的商品价格是多少;将记录价格变化(价格差)的新的数组记为b[],其中b[i]表示第i天的价格相比于第i-1天涨了多少(负数表示降价),也可以理解为b[i]保存的是“如果第i-1天买进第i天卖出所得利益”。于是,我们发现,第i天买进然后第j天卖出,可以分解为:第i天买进、第i+1天卖出、第i+1天买进、第i+2天卖出……第j-1天买进、第j天卖出。

    咋一看觉得把复杂度提高了,因为原本求“第i天买进,第j天卖出”所得利益只需要O(1)的复杂度就可以搞定了,现在还需要做一次累加,变成了O(n)级别的操作了。各位看官先别着急,咱们先分析一下问题转换成了什么,再分析复杂度不迟。

    对a[]的问题是,求两个位置i和j,其中i<j,使得a[j]-a[j]最大;对b[]的问题是,求两个位置i和j,使得a[i]+a[i+1]+……+a[j]最大(这个和式也就是所得利益)。也就是说,现在的问题变成了求连续子数组最大和。我们仍然从暴力搜索开始思考,一个很朴素的想法是,枚举买进的日期,然后计算在之后的每一天卖出所能获得的利益。最后我们同样可以获得最大利益,时间复杂度仍然是O(n^2),如下(c代码):

     1 #include <stdio.h>
     2 #define MaxN 120
     3 #define INF 0x3f
     4 int main()
     5 {
     6     int a[MaxN], n;
     7     int i, j;
     8     int Ans, Sum, st = 0, ed = 0;
     9     scanf("%d", &n);
    10     a[0] = 0;
    11     for(i = 1; i <= n; i++)
    12         scanf("%d", &a[i]);
    13     for(i = n; i >= 1; i--)
    14         a[i] -= a[i - 1]; //这里的a[]就是指的新得到的b[] 
    15     /*暴力枚举买入日期*/
    16     Ans = -INF;
    17     for(i = 1; i < n; i++) //显然不能在最后一天买入 
    18     {
    19         Sum = 0;
    20         for(j = i + 1; j <= n; j++) //至少在后一天卖出
    21         {
    22             Sum += a[j];
    23             if(Ans < Sum)
    24             {
    25                 Ans = Sum;
    26                 st = i;
    27                 ed = j;
    28             }
    29         }
    30     }
    31     printf("获利:%d元
    在第%d天买入,在第%d天卖出
    ", Ans, st, ed);
    32     return 0;
    33 }
    View Code

    现在估计有看官觉得zyy又在装神弄鬼了…… 明明还是O(n^2)的好吗!还弄这么复杂很好玩吗啊摔!w(゚Д゚)w

    ……摸摸,都说了别着急…… 下面上干货。

    我们来分析一下对于数组b[]来说,答案会出现在什么地方。如果对数组b[]一分为二,那么最后求得的答案只能是三种情况:要么是全在左边的连续子数组,要么是全在右边的连续子数组,要么是横跨中点的连续子数组。说到这里就应该有一种灵光一现的感觉了吧~ 是的,这是一种基于分治思想的算法(你觉得是二分,或者是线段树,甚至树状数组那都没问题,不过我们这里说的是算法的思想),下面给出算法的具体实现。

    [算法描述及实现]

    对于一个数组,求下标范围为[left,right]的连续子数组最大和。将数组一分为二,记mid=(left+right)/2(具体实现个人更倾向于mid=left+(right-left)/2的写法,这里不作解释),那么最优解要么是从[left,mid]得到,要么是从[mid+1,right]得到,要么是横跨中点的一个答案。对于前两种情况,是将问题完全转化为一个规模更小的、本质不变的子问题,递归进去实现就可以了,这里我们需要解决的是如何求横跨中点的情况。我们很容易发现,想要mid左右两边选出来的连续的数之和达到最大,让左右两边分别达到最大即可。于是第三种情况:最大和=mid向左侧延伸的最大和+mid向右侧延伸的最大和。这个在同一级别的区间并起来看时间复杂度是O(n)的,二分的复杂度是O(lg n)的,所以总的复杂度是O(n lg n)。

    下面是实现(c++代码):

     1 #include <iostream>
     2 #include <cstdio>
     3 using namespace std;
     4 #define MaxN 120
     5 #define INF 0x3f
     6 int Get_Max_Sum(int *a, int L, int R, int &st, int &ed)
     7 {
     8     int Ret, tmp, mid = L + (R - L) / 2;
     9     int s, e;
    10     int Max1, Max2;
    11     int i;
    12     /*单个点的情况,直接返回*/
    13     if(L == R)
    14     {
    15         Ret = a[L];
    16         st = ed = L;
    17         return Ret;
    18     }
    19     /*从左侧得到答案*/
    20     Ret = Get_Max_Sum(a, L, mid, s, e);
    21     st = s; ed = e;
    22     /*从右侧得到答案*/
    23     tmp = Get_Max_Sum(a, mid + 1, R, s, e);
    24     if(Ret < tmp)
    25     {
    26         Ret = tmp;
    27         st = s; ed = e;
    28     }
    29     /*横跨mid的情况,实际上是指至少包括a[mid]和a[mid+1]*/
    30     //左侧
    31     s = mid;
    32     tmp = 0;
    33     Max1 = a[mid];
    34     for(i = mid; i >= L; i--)
    35     {
    36         tmp += a[i];
    37         if(tmp > Max1)
    38         {
    39             Max1 = tmp;
    40             s = i;
    41         }
    42     }
    43     //右侧
    44     e = mid + 1;
    45     tmp = 0;
    46     Max2 = a[mid + 1];
    47     for(i = mid + 1; i <= R; i++)
    48     {
    49         tmp += a[i];
    50         if(tmp > Max2)
    51         {
    52             Max2 = tmp;
    53             e = i;
    54         }
    55     }
    56     tmp = Max1 + Max2;
    57     if(tmp > Ret)
    58     {
    59         Ret = tmp;
    60         st = s; ed = e;
    61     }
    62     return Ret;
    63 }
    64 int main()
    65 {
    66     int a[MaxN], n;
    67     int i;
    68     int Ans, st = 0, ed = 0;
    69     scanf("%d", &n);
    70     for(i = 1; i <= n; i++)
    71         scanf("%d", &a[i]);
    72     for(i = n; i >= 2; i--)
    73         a[i] -= a[i - 1]; //这里的a[]就是指的新得到的b[]
    74     Ans = Get_Max_Sum(a, 2, n, st, ed);
    75     printf("获利:%d元
    在第%d天买入,在第%d天卖出
    ", Ans, st - 1, ed);
    76     return 0;
    77 }
    View Code

    解释一下,这里写的是c++的代码,因为c语言中不包括变参,函数返回值处理略麻烦。

    目前三个代码没有经过对拍,最后一种方式当然是正确的,但不保证我写得没有错误……挖个坑吧~

  • 相关阅读:
    Java IO输入输出流 FileWriter 字符流
    Java IO输入输出流File 字节流
    Java List集合和Map集合的综合应用
    表单提交中的重复问题(表单令牌验证)
    php中const与define的区别
    阿里云中获取文件及目录列表的方法
    巧用php中的array_filter()函数去掉多维空值
    文件大小格式化函数
    UTC 通用格式时间 转换为 时间戳,并格式化为2017-01-01 12:00:00
    关于匿名函数的使用,购物车中计算销售税的应用
  • 原文地址:https://www.cnblogs.com/CQBZOIer-zyy/p/5275917.html
Copyright © 2011-2022 走看看