问题的简短描述:
给出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