[USACO08NOV]玩具Toys

算法:三分+贪心

1.读题之后的第一件事是明确目标和问题

    第一步有同学可能会往DP的方向思考(比如蒟蒻我)。因为我们要处理的问题是我需要买多少个玩具,并且每天分别分多少个给n1,多少个给n2才能使得总和最小。
但是,在DP之前一定要仔细想一想,有一个算法也可以处理部分DP问题但是比DP更优秀,那就是贪心

    但是我们要证明贪心是正确的。我们不妨假设c1与c2是商店里玩具的另一种价格且数量有限,sum为1~D天派对所需要的玩具总量,无论如何我们都要从c1,c2,tc这三种价格购买sum个玩具,显然买价格越便宜的越好。

2. 第二步将问题拆分,往简单的方向去思考

    关于拆分的方向,这里是根据算法以及自身经验来的(如果是大佬的话完全可能能够一次性到位吧)因为我们要让总的金额最少,并且要让每天派对拥有足够的玩具,我们必须要选择最便宜的方案。

     为了说明方便,我们这里的所有玩具消毒只按天数付钱。也就是我可以先把所有玩具免费消毒,然后根据取回当天距离消毒当天的天数,划分n1,n2两个档次。如果n1=1,n2=2,一个玩具在第一天送去消毒,在第二天取出来的价格就是c1,但是如果在第三天取出来价格就是c2了(相当于玩具不用立刻在n1天或者n2天取出,可以存放很长一段时间)

c 1 > c 2 c1>c2 c1>c2(接下来的分析是对于每天单独贪心的情况)

如果 c 2 > t c c2>tc c2>tc 也就表明与其送去消毒不如天天买新玩具,这个时候只需要输出 t c ∗ s u m tc*sum tcsum就可以啦

如果 c 1 > t c > c 2 c1>tc>c2 c1>tc>c2或者 t c > c 1 > c 2 tc>c1>c2 tc>c1>c2且c2要快一些那么我们只需要将玩具分给从商店买,和n2天消毒就行

如果 t c > c 1 > c 2 tc>c1>c2 tc>c1>c2 且c2 比较慢,这个时候我们要仔细斟酌一下啦。

     到这里与其考虑每天的分配方式,我们不如从总量分析(因为这里每天分析会变的很麻烦)我们不妨假设我们一共要买x个玩具,那么我们设f(x)表示满足每天需求前提所需要的最小费用,其中x表示购买的的新玩具的个数。g(x)表示在1~D天内把认为需要送去消毒的旧玩具送去消毒花费的费用。从商店购买的费用算为 t c ∗ x tc*x tcx,总共的费用就可以算为: f ( x ) = g ( x ) + t c ∗ x f(x)=g(x)+tc*x f(x)=g(x)+tcx,并且这函数的斜率是单调的。

为什么?!

    如果新玩具很多,那么g(x)就会特别小。因为新玩具已经满足了派对的需求所以不需要在送一些去洗了。并且存在 g ( x − 1 ) − g ( x ) > = g ( x ) − g ( x + 1 ) g(x-1)-g(x)>=g(x)-g(x+1) g(x1)g(x)>=g(x)g(x+1)可以这样来考虑,因为玩具数少需要快洗,花的钱就比较多。并且abs(k)就要大一些,函数图像也要抖一些。但是如果玩具数量多,就会选择慢洗。所以abs(k)就要小一些,函数图像也就要缓一些。
    反向表示带入可得: f ( x ) − f ( x − 1 ) < = f ( x + 1 ) − f ( x ) f(x)-f(x-1)<=f(x+1)-f(x) f(x)f(x1)<=f(x+1)f(x)于是f(x)的斜率也是单调递增的。
那么斜率单调递增的函数会有哪些图像呢:
1 23
    由图可见这种函数是单峰的,所以可以三分。

     如果这里理解不了的同学可以这样意会:如果玩具买多了就会浪费一部分钱,因为多买的部分可以用消毒的费用来代替,如果买少了反而要在消毒上面花更多的钱。所以这个函数就是单峰的!


单峰的函数自然而然会联想到三分,所以这道题到这里已经解决了一半了。

3.接下来要完成的任务便是怎么求出这个总花费。

    我们依旧在 t c > c 1 > c 2 tc>c1>c2 tc>c1>c2 且c2 比较慢这情况下面思考。

    那么只考虑对每天进行贪心,就是每天都洗最便宜的衣服,求出来的答案是否是最优的呢?

    很明显不完全正确! 我这里举个例子:D=4,N1=1,N2=3,C1=3,C2=1,Tc=4,D1=3,D2=1,D3=1,D4=4;当我们按照上述方法贪心的时候求出来的结果是27,但是正解是25,原因就是我们在第三天的时候,贪心会选择来自第一天的玩具(因为这个贪心只考虑当前最便宜的玩具不考虑天数)。

    但是如果我们选择跟第一天玩具相同价格的第二天的玩具。虽然对于第三天来说花费是相通的,但是对第四天来说花费就有所不同了(大家可以手工模拟一下这里就不再做详细解释了)。

    所以我们从上面的例子得到了一个结论,在价格相同的情况下我们应该选择消毒时间最近的玩具,这样消毒时间远的玩具价格就会降低。


算法到这里大概就解释清楚了 ~~(吧)~~然后是代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=100000+5;
const int inf=1000000000; 
int d,n1,n2,c1,c2,tc,l,r,t[maxn];
struct code
{int Day,toys;};
deque<code> n,o,m;
//我们这里用了三个双端队列,表示n,o,m。
//n表示从开始消毒的那天到今天少于n1天的玩具
//m表示消毒了至少有n1天但是不满足n2天的玩具
//o表示已经消毒了至少n2天的玩具 
void add(int day,int toy)
{
	code now;
	now.Day=day,now.toys=toy;
	n.push_back(now);
}
void newch (int x)
{
	while (n.size()&&x-n.front().Day>=n1)//这里从队首开始讨论,因为队首都是早先被压入的,时间比较久 
	m.push_back(n.front()),n.pop_front();
	while (m.size()&&x-m.front().Day>=n2)
	o.push_back(m.front()),m.pop_front();
}
int Find(int x)
{
	while (n.size()) n.pop_front();//记得清零! 
	while (o.size()) o.pop_front();
	while (m.size()) m.pop_front();
	int money=(tc-c2)*x;
	//因为在下面的讨论里面我们要把所有的玩具当成消毒过的。
	//但这里是直接购买的玩具,所以要减去c2 
	add(-200000,x); //设成-200000是为了方便讨论(不清楚的可以手工模拟一下) 
	for (int i=1;i<=d;i++)
	{
		int rest=t[i];
		newch(i);//在这里进行新旧交替的操作,相当于题解中我们按照天数堆对消毒进行付款,而不明确的分类 
		while (rest&&o.size())//先选择便宜的 
		{
			if (o.back().toys>rest)//这里从back弹出就是相同价格选择清洗时间比较近的(下同) 
			o.back().toys-=rest,money+=rest*c2,rest=0;
			else 
			rest-=o.back().toys,money+=c2*o.back().toys,o.pop_back();
		}
		while (rest&&m.size())
		{
			
			if (m.back().toys>rest)
			m.back().toys-=rest,money+=rest*c1,rest=0;
			else 
			rest-=m.back().toys,money+=c1*m.back().toys,m.pop_back();
		}
		if (rest) return inf;//inf就表示玩具数量不足以满足派对的需求 
		add(i,t[i]);
	}
	return money;
}
int three()
{
	 r=r+1;
	while (r-l>2)//当l和r足够接近的时候就可以手工跳出了 
	{
		int x=(2*l+r)/3,y=(2*r+l)/3;
		int a=Find(x);
		if (a!=inf&&a<=Find(y)) r=y; 
		else l=x;
	}
	int ans=Find(l);
	for (int i=l+1;i<=r;i++)
		ans=min(ans,Find(i));
	return ans;
}
int main()
{
	scanf("%d%d%d%d%d%d",&d,&n1,&n2,&c1,&c2,&tc);
	if (n1>n2)//保证n1是快洗 
	swap(n1,n2),swap(c1,c2);
	if (c1<c2) c2=c1;//如果快的价格反而要低一些为什么不选择快的呢
	//因为后面的讨论是根据慢的比较低这个条件来的,所以这里需要赋值。 
	for (int i=1;i<=d;i++) scanf("%d",&t[i]),r+=t[i];
	//为什么二分的上限是玩具的总数,因为我最多把所有玩具买下来啊 
	printf("%d",three()); 
}

参考资料:官方标准程序(如下)

#include <stdio.h>
#include <stdlib.h>
#include <algorithm>

using namespace std;

#define MAX (100005)
#define INF (1000000000)

int T[MAX];
int queue[MAX], num[MAX];
int sn,sm,so,en,em,eo;
/* These variables control the queue and point to the start 
 * and end of new, middle, and old toys, respectively. 
 * New toys are ones washed less than N1 days ago, old toys are 
 * washed at least N2 days ago, and middle toys are all the rest.
 * We essentially keep three queues in a single array, but the manner
 * in which we add/remove elements guarantees that we never have to
 * worry about memory overlap.
 */

int D,N1,N2,C1,C2,Tc;

inline void add_new(int x,int q){
  queue[en]=x;
  num[en++]=q;
}

void flush_new(int x){
  while(sn!=en && x-queue[sn] >= N1) { num[em]=num[sn]; queue[em++]
= queue[sn++]; }
}

void flush_mid(int x){
  while(sm!=em && x-queue[sm] >= N2) { num[eo]=num[sm]; queue[eo++]
= queue[sm++]; }
}

int f(int t){ //find the minimum cost given that we use t toys
  sn=sm=so=en=em=eo=0;
  int r = (Tc-C2)*t;
  /*
   * In the following algorithm, we pay even to wash the initial toys, so 
   * we have to subtract this out of our initial cost estimate for 
   * purchasing the toys. This is valid as long as we use all of the toys 
   * we purchase (which is why we start the ternary search at tsum+1 
   * instead of 5000001).
   */
  add_new(-200000,t);
  for(int d=0;d<D;d++){
    flush_new(d); flush_mid(d); //move any toys toys whose status changes 
      //from being new to middle or middle to old to the appropriate queue
    int i = T[d];
    while(i > 0){ //we deal with the toys in batches to make 
      //the runtime of this function O(N) instead of O(sum ti)
      if(so!=eo){ //if there are any old toys
        if(num[eo-1] > i){ //if this batch has more toys than we need, we can stop here
          r += C2*i;
          num[eo-1]-=i;
          break;
        }
        else { //otherwise, use all toys in this batch
          r += C2*num[eo-1];
          i -= num[eo-1];
          eo--;
        }
      }
      else if(sm!=em){ //else if there are any middle toys
        if(num[em-1] > i){
          r += C1*i;
          num[em-1]-=i;
          break;
        }
        else {
          r += C1*num[em-1];
          i -= num[em-1];
          em--;
        }
      }
      else return INF; //if there are no available toys, 
        //we can't find a solution with this many toys
    }
    add_new(d,T[d]); //put the toys we used today back into the queue of toys
  }
  return r;
}

int ternary_search(int s,int e){
  while(1){
    if(e-s <= 2){ //when e-s is small enough that our ternary search 
      //can get stuck, just handle the end manually
      int m = f(s);
      for(int i=s+1;i<e;i++) m = min(m,f(i));
      return m;
    }
    int x = s+(e-s)/3, y = s+2*(e-s)/3; //sample values 1/3 and 2/3 
      //of the way through the remaining interval
    int a = f(x); //f(x) = -1 if x was too few toys to have enough toys each day
    if(a!= INF && a <= f(y)) e=y;
    else s=x;
  }
}

int main(){
 scanf("%d%d%d%d%d%d",&D,&N1,&N2,&C1,&C2,&Tc);
  if(N1 > N2){ //set N2 to be greater than N1
    N1 ^= N2; N2 ^= N1; N1 ^= N2;
    C1 ^= C2; C2 ^= C1; C1 ^= C2;
  }
  if(C1 < C2){ //if faster way is cheaper
    C2 = C1; //then set its cost to that of the slower way, 
      //since we could always just use the slower way instead
  }
  int tsum = 0;
  for(int i=0;i<D;i++) { scanf("%d",&T[i]); tsum += T[i]; }
  printf("%d\n",ternary_search(0,tsum+1));
  return 0;
}

    因为本蒟蒻太菜了,所以一来想到的是Dp(可能DP做多了,都忘了老本行贪心了),然后搞了个五维的出来,并且还不能优化(那么问题来了,为什么会有状态压缩的标签呢),总之就是我太菜了第一次没做出来。

    于是本蒟蒻就很不要脸的去网上找题解了(emm),可能是年代太早了,网络上的题解相当的少,而且相当的简洁。可能一般做到这里的人,都已经学会网络流了(餐巾问题呢)所以本蒟蒻看不懂呢qwq。花了一天的时间终于把这道题A掉了,然而为了造福后面解这道题的人,这篇题解就写的特别的啰嗦(害怕有人看不懂emm)

    另外是关于函数的部分,其实之前有很多博客都证明了单峰函数,但是函数的含义并没有详细的给出。这里进行了比较详细的说明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值