例题3 来源: USACO 4-3-1
【问题描述】
“逢低吸纳”是炒股的一条成功秘诀。如果你想成为一个成功的投资者,就要遵守这条秘诀:
"逢低吸纳,越低越买"
这句话的意思是:每次你购买股票时的股价一定要比你上次购买时的股价低.按照这个规则购买股票的次数越多越好,看看你最多能按这个规则买几次。
给定连续的N天中每天的股价。你可以在任何一天购买一次股票,但是购买时的股价一定要比你上次购买时的股价低。写一个程序,求出最多能买几次股票。
以下面这个表为例, 某几天的股价是:
天数 1 2 3 4 5 6 7 8 9 10 11 12
股价 68 69 54 64 68 64 70 67 78 62 98 87
这个例子中, 聪明的投资者(按上面的定义),如果每次买股票时的股价都比上一次买时低,那么他最多能买4次股票。一种买法如下(可能有其他的买法):
天数 2 5 6 10
股价 69 68 64 62
【输入文件】buylow.in
第1行: N (1 <= N <= 5000), 表示能买股票的天数。
第2行以下: N个正整数 (可能分多行) ,第i个正整数表示第i天的股价. 这些正整数大小不会超过longint(pascal)/long(c++).
【输出文件】buylow.out
只有一行,输出两个整数:
能够买进股票的天数长度 达到这个值的股票购买方案数量
在计算解的数量的时候,如果两个解所组成的字符串相同,那么这样的两个解被认为是相同的(只能算做一个解)。因此,两个不同的购买方案可能产生同一个字符串,这样只能计算一次。
【输入样例】
12
68 69 54 64 68 64 70 67 78 62 98 87
【输出样例】
4 2
【问题分析】
从题目描述就可以看出这是最长下降子序列问题,于是求解第一问不是难事,以天数为阶段,设计状态opt[i] 表示前i天中要买第i天的股票所能得到的最大购买次数。
状态转移方程:
opt[i]=max(opt[j])+1 (a[i]>=a[j],0=<j<i) {a[i]存第i天股价}
最大的opt[i]就是最终的解。
第二问呢,稍麻烦一点。
从问题的边界出发考虑问题,当第一问求得一个最优解opt[i]=X时,
在1到N中如果还有另外一个opt[j]=x那么选取J的这个方案肯定是成立的。
是不是统计所有的opt[j]=x 的个数就是解了呢?
显然没那么简单,因为选取J这天的股票构成的方案不见得=1,看下面一个例子:
5 6 4 3 1 2
方案一:5431
方案二:5432
方案三:6431
方案四:6432
x=4
也就是所虽然opt[5]=X 和opt[6]=X但个数只有两个,而实际上应该有四个方案,但在仔细观察发现:
构成opt[5]=x 的这个方案中 opt[j]=x-1的方案数有两个(5 4 3 和6 4 3)
opt[j]=x-2的有一个,opt[j]=x-3 的有一个……
显然这是满足递归定义的设计函数F(i)表示前I张中用到第i张股票的方案数。
递推式:
1 (i=0)
F(i)=
Sum(F(j)) (0<=j<=n,a[j]>a[i],opt[j]=opt[i]-1) {sum 代表求和}
答案=sum(F(j)) {0<j<=n,opt[j]=x}
复杂度:
求解第一问时间复杂度是O(N2),求解第二问如果用递推或递归+记忆化时间复杂度仍为O(N2)但要是用赤裸裸的递归那就复杂多了……,因为那样造成了好多不必要的计算。
你认为这样做就解决了这道题目了么?还没有,还要注意一些东西:
(1)如果有两个方案中出现序列一样,视为一个方案,要需要加一个数组next用next[i] 记录和第i个数情况一样(即:opt[i]=opt[j] 且 a[i]=a[j])可看做一个方案的最近的位置。
递推时j只要走到next[i]即可。
(2)为了方便操作可以将a[n+1]赋值为-maxlongint这样可以认为第n+1个一定可以买,答案就是sum(F(n+1))。
(3)看数据规模显然要用高精度。
注:USACO上最后一个点错了。我在程序中做了处理。
【源代码】#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef struct BIGNUM *bignum_t;
struct BIGNUM
{
int val;
bignum_t next;
};
int num[5000]; //存储给定的数据
int len[5000]; //保存最大下降序列值
int nlen; //数据数量
bignum_t cnt[5000];
bignum_t get_big(void)
{
static bignum_t block;
static int size = 0;
if (size == 0)
{
block = (bignum_t)malloc(sizeof(*block)*128);
size = 128;
}
size--;
return block++;
}
/*初始化高精度数*/
void init_big(bignum_t *num, int val)
{
*num = get_big();
/* initialize */
(*num)->val = val;
(*num)->next = NULL;
}
void add(bignum_t a, bignum_t b)
{
int c; /* carry */
c = 0;
while (b || c)
{
a->val += c;
if (b) a->val += b->val;
/* if a->val is too large, we need to carry */
c = (a->val / 1000000); //4字节int最大值为2147483647
a->val = (a->val % 1000000);
if (b) b = b->next;
if (!a->next && (b || c))
{ /* allocate if we need to */
a->next = get_big();
a = a->next;
a->val = 0;
a->next = NULL;
}else
a = a->next;
}
}
void out_num(FILE *f, bignum_t v)
{
if (v->next)
{
out_num(f, v->next);
fprintf (f, "%06i", v->val);
}
else
fprintf (f, "%i", v->val);
}
int main(int argc, char **argv)
{
FILE *fout, *fin;
int lv, lv2;
int c;
int max;
int l;
bignum_t ans;
if ((fin = fopen("in.txt", "r")) == NULL)
{
perror("fopen fin"); //perror(str)打印str和一个相应的错误消息
exit(1);
}
if ((fout = fopen("out.txt", "w")) == NULL)
{
perror ("fopen fout");
exit(1);
}
fscanf (fin, "%d", &nlen);
for (lv = 0; lv < nlen; lv++)
fscanf (fin, "%d", &num[lv]);
/* 用DP计算最大长度*/
for (lv = 0; lv < nlen; lv++)
{
max = 1;
for (lv2 = lv-1; lv2 >= 0; lv2--)
if (num[lv2] > num[lv] && len[lv2]+1 > max)
max = len[lv2]+1;
len[lv] = max;
}
for (lv = 0; lv < nlen; lv++)
{
if (len[lv] == 1)
init_big(&cnt[lv], 1);
else
{
init_big(&cnt[lv], 0);
l = -1;
max = len[lv]-1;
for (lv2 = lv-1; lv2 >= 0; lv2--)
if (len[lv2] == max && num[lv2] > num[lv] && num[lv2] != l)
add(cnt[lv], cnt[lv2]);
l = num[lv2];
}
}
/* 找最长串*/
max = 0;
for (lv = 0; lv < nlen; lv++)
if (len[lv] > max) max = len[lv];
init_big(&ans, 0);
l = -1;
for (lv = nlen-1; lv >= 0; lv--)
if (len[lv] == max && num[lv] != l)
{
add(ans, cnt[lv]);
l = num[lv];
}
/* output answer */
fprintf (fout, "%i ", max);
out_num(fout, ans);
fprintf (fout, "\n");
return 0;
}