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

[问题背景]

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

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

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

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

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

原文地址:https://www.cnblogs.com/CQBZOIer-zyy/p/5275917.html