• [算法]一次商品交易利益最大化


    [问题背景]

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

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

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

    首先,最容易的想法是暴力搜索。我们可以枚举买进和卖出的日期,然后计算对应的获利,最后得到一个最大利益。这种方式的时间复杂度是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语言中不包括变参,函数返回值处理略麻烦。

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

  • 相关阅读:
    Combine 框架,从0到1 —— 4.在 Combine 中使用计时器
    Combine 框架,从0到1 —— 4.在 Combine 中使用通知
    Combine 框架,从0到1 —— 3.使用 Subscriber 控制发布速度
    Combine 框架,从0到1 —— 2.通过 ConnectablePublisher 控制何时发布
    使用 Swift Package Manager 集成依赖库
    iOS 高效灵活地配置可复用视图组件的主题
    构建个人博客网站(基于Python Flask)
    Swift dynamic关键字
    Swift @objcMembers
    仅用递归函数操作逆序一个栈(Swift 4)
  • 原文地址:https://www.cnblogs.com/CQBZOIer-zyy/p/5275917.html
Copyright © 2020-2023  润新知