营业额统计问题的三种巧妙解法

问题的简短描述:

       给出n天的营业额,该天的最小波动值=min{|该天以前某一天的营业额-该天的营业额|} 。第一天的最小波动值为第一天的营业额。求n天的最小波动值之和。n<=32767,营业额a<= 1000000。详见:http://new.tyvj.cn/Problem_Show.aspx?id=1185

样例输入:

6 5 1 2 5 4 6

样例输出:

12

样例解释:

5+|1-5|+|2-1|+|5-5|+|4-5|+|6-5|=5+4+1+0+1+1=12

 

巧解一:

       A[i]表示第i天的营业额。到了第i天时,设前面与A[i]最接近的营业额为m。考察样例可以知道,m出现多少次并不重要。例如在样例的第5天,前面的日子里和第5天的营业额最接近的为营业额4,而营业额4出现了两次。这启发我们,在第5天的时候,只用知道前面都出现过哪些营业额即可,并不用记录他们出现的次数。于是巧妙的算法诞生了——

        int s=0;

        while(!A[x-s] && !A[x+s]) s++;//第i天的营业额为x,往两头找。

        ans+=s;

        A[x]=true;

       这里的A[i]表示为营业额i是否出现过的标志。

       可还是一个一个查找啊?!算法不会快。评测系统答道:我给你的算法跪了,恭喜你,你的程序运行时间为0ms,请接受自动生成的祝贺[1]。

       想想对于这样的算法,最强悍的数据是怎样的。可以用二分思想得到这样的数据。每次选择中间的营业额。

      

加法的次数=2(len+len/2+len/4+len/8+len/16……len/2^k)=2len(2-1/2k)=4len,而len为营业额可能的最大值,即len=1000000。故算法复杂度为O(4len)。

#include<cstdio>
#include<cstring>
#include<math.h>
#include<stdlib.h>
#include<algorithm>
using namespace std;
const int maxn=2000000;

bool A[maxn*2];//tyvj上的数据有负数,故所有数都往右移maxn个单位
 
int main()
{
#ifndef ONLINE_JUDGE
  freopen("in.txt","r",stdin);
#endif
	int i,j,x;
	int n;
	scanf("%d",&n);
	scanf("%d",&x);
	int ans=x;
	memset(A,false,sizeof(A));
	A[x+maxn]=true;
	for(i=2;i<=n;i++)
	{
		scanf("%d",&x);
		x+=maxn;
		int s=0;
		while(!A[x-s] && !A[x+s]) s++;
		ans+=s;	
		A[x]=true;
	}
	printf("%d\n",ans);
  return 0;
}


巧解二:

       设计一个离线算法,怎么样?现在得到了n天的营业额,那第n天的最小波动值为多少?只用知道谁跟A[n]最靠近!也即给A数组排序,然后比较A[n]两头谁跟它更靠近。再看看未知数。所要求的是波动值之和。为什么要乖乖地从第1天开始然后把每天的最小波动值相加呢?可以考虑从最后一天算最小波动值。说不定,那样更神奇地得到最小波动值之和呢。

       那怎么求第n-1天的最小波动值呢?发现第n天的营业额在干扰。这意味着要把第n天的营业额从数组中删掉。从数组中删除元素很慢,要移动的数据很多,怎么办?双向链表从天而降!像这样:

void link(Node *x,Node *y)

{

    x->right=y;

    y->left=x;

}

 

void MakeList()

{

    N[1].left=NULL;

    N[n].right=NULL;

    for(int i=1;i<n;i++)

      link(&N[i],&N[i+1]);

}

       再定义一个结构体:

struct Node

{

    Node *left,*right;

    int v;

    int p;//原来的位置 

}N[maxn];

       类似后缀数组中的概念,定义一个名次数组rank[i],表示原来数组中的第i个数在排序数组的位置。之后求最小波动值就很容易了:

       for(i=n;i>=2;i--)

       {

              Node*p=&N[rank[i]];

              if(p->left!=NULL&& p->right!=NULL)

              {

                     ans+=min(p->v-p->left->v,p->right->v- p->v);

                     link(p->left,p->right);

              }elseif(p->left==NULL)

              {

                     ans+=p->right->v-p->v;

                     p->right->left=NULL;

              }else

              {

                     ans+=p->v-p->left->v;

                     p->left->right=NULL;

              }

       }

后面计算最小波动值那一节时间复杂度为O(n),排序为O(nlogn)。故时间复杂度为O(nlogn)

#include<cstdio>
#include<cstring>
#include<math.h>
#include<stdlib.h>
#include<algorithm>
using namespace std;
const int maxn=32768;
int rank[maxn],n;
struct Node
{
	Node *left,*right;
	int v;
	int p;//原来的位置 
}N[maxn];

bool cmp(Node a,Node b)
{
	return a.v<b.v;
}

void link(Node *x,Node *y)
{
	x->right=y;
	y->left=x;
}

void MakeList()
{
	N[1].left=NULL;
	N[n].right=NULL;
	for(int i=1;i<n;i++)
	  link(&N[i],&N[i+1]);
}

int main()
{
#ifndef ONLINE_JUDGE
  freopen("in.txt","r",stdin);
#endif
	int i,j;
	scanf("%d",&n);
	for(i=1;i<=n;i++)  
	{
		scanf("%d",&N[i].v);
		N[i].p=i;
	}
	sort(N+1,N+1+n,cmp);
	for(i=1;i<=n;i++) rank[N[i].p]=i;
	int ans=N[rank[1]].v;
	MakeList();
	for(i=n;i>=2;i--)
	{
		Node *p=&N[rank[i]];
		if(p->left!=NULL && p->right!=NULL)
		{
			ans+=min(p->v- p->left->v,p->right->v- p->v);
			link(p->left,p->right);
		}else if(p->left==NULL)
		{
			ans+=p->right->v-p->v;
			p->right->left=NULL;
		}else
		{
			ans+=p->v-p->left->v;
			p->left->right=NULL;
		}
	}
	printf("%d\n",ans);
  return 0;
}


巧解三:

       用SBT也可以来解决该问题。但是同样可以做得更好,不用常规操作“求前驱”、“求后继”(中规中矩地求了它们,然后与当天的营业额作差,得到最小波动值。),而是定义一个求最小波动值的函数:

int findClose(int u,int v)//在以u为根的子树,找到min(abs(key-v)),返回min值

{

    if(!u) return INF;//返回-INF,不行。返回INF值,表示取不到这样的min,和能取到区分开来。

    if(T[u].v<v)

    {

        int t=findClose(T[u].right,v);

        return min(v-T[u].v,t);

    }else if(T[u].v>v)

    {

        int t=findClose(T[u].left,v);

        return min(T[u].v-v,t);

    }else return 0;

}

对于SBT,还可以写成,因为很多操作都是对称的,可以用指针下标为0和1的来表示左儿子和右儿子,同理,用0,1来表示左旋和右旋操作。

       如:

void rot(Tnode *&u,bool d)//分两种情况,d为左儿子,则!d为右儿子。d为右儿子,则!d为左儿子。

{

       Tnode*p=u->c[d];

       u->c[d]=p->c[!d];

       p->c[!d]=u;

       p->CalSize();

       u->CalSize();

       u=p;

}

又像这样:

void maintain(Tnode *&u,bool d)

{

       if(u==Null)return;

       Tnode*&p=u->c[d];//不能写成Tnode *p=u->c[d],否则不能改变u->c[d]的值。

       if(p->c[d]->size>u->c[!d]->size)//case1以及case3,这两种情况对称

              rot(u,d);

  elseif(p->c[!d]->size >u->c[!d]->size)//case 2以及case4

  {

     rot(p,!d);

     rot(u,d);

  }else return;

 maintain(u->c[0],0);

 maintain(u->c[1],1);

  maintain(u,0);

  maintain(u,1);

}

      insert一次的时间复杂度为O(logn)共n次,所以总的时间复杂度为O(nlogn)。

#include<cstdio>
#include<cstring>
#include<math.h>
#include<stdlib.h>
#include<algorithm>
#include<ctime>
#include<iostream>
#define INF 1<<30
using namespace std;
const int maxn=32768;

struct Tnode
{
	Tnode *c[2];
	int v,size;
	void CalSize()
	{
		size=c[0]->size+c[1]->size+1;
	}
	Tnode (int _v,int _size,Tnode *_c)
	{
		v=_v;size=_size;
		c[0]=c[1]=_c;
	}
	Tnode (){}
}node[maxn],TNull(0,0,0),*Null=&TNull;

int cnt=0;
Tnode *NewNode()
{
	Tnode *u=&node[cnt++];
	u->c[0]=u->c[1]=Null;
	u->size=1;
	return u;
}

void rot(Tnode *&u,bool d)
{
	Tnode *p=u->c[d];
	u->c[d]=p->c[!d];
	p->c[!d]=u;
	p->CalSize();
	u->CalSize();
	u=p;
}

void maintain(Tnode *&u,bool d)
{
	if(u==Null) return;
	Tnode *&p=u->c[d];
	if(p->c[d]->size>u->c[!d]->size)
		rot(u,d);
  else if(p->c[!d]->size >u->c[!d]->size)
  {
  	rot(p,!d);
  	rot(u,d);
  }else return;
  maintain(u->c[0],0);
  maintain(u->c[1],1);
  maintain(u,0);
  maintain(u,1);
}

void insert(Tnode*&u,int v)
{
	if(u==Null)
	{
		u=NewNode();
		u->v=v;
		return;
	}
	if(v==u->v) return;
	bool d=v>u->v;
	insert(u->c[d],v);
	maintain(u,d);
	u->CalSize();
}

int findClose(Tnode *u,int v)
{
	if(u==Null) return INF;
	if(u->v==v) return 0;
	int d=v>u->v;
	int p=findClose(u->c[d],v);
	return min(p,abs(u->v-v));
}

void printTree(Tnode *u)
{
	if(u==Null) return;
	printf("%d\n",u->v);
	for(int i=0;i<1;i++)
	  printTree(u->c[i]);
}

int main()
{
#ifndef ONLINE_JUDGE
  freopen("in.txt","r",stdin);
#endif
	int i,j;
	int n;
	scanf("%d",&n);
	int x;
	scanf("%d",&x);
	int ans=x;
	Tnode *root=Null;
	insert(root,x);
	Null->c[0]=Null->c[1]=Null;//necessary
	//printf("%d\n",NULL);
	//printf("%d\n",root->v);
	//printTree(root);
	for(i=1;i<n;i++)
	{
		scanf("%d",&x);
		ans+=findClose(root,x);
		//printf("%d\n",ans);
		insert(root,x);
		//printTree(root);
	}
	printf("%d\n",ans);
	//rintTree(root);
	//printf("ok\n");
	//printf("%.2lf\n",(double)clock()/CLOCKS_PER_SEC);
  return 0;
}

/*
*/


注释:

       [1]我很喜欢usaco。它的自动祝贺一定让很多oiers乐了。

 

参考:

1、陈启峰《Size Balanced Tree》

2、刘汝佳《基础数据结构》

3、WJMZBMR的SBT c++ code。http://www.nocow.cn/index.php/Code:SBT_C%2B%2B

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值