技巧与错误(4)

14.错误 

    昨天模拟考,在提交的前一分钟发现自己的并查集忘了写路径压缩了,顿时一身冷汗……引以为戒!

15.错误

    模拟考第二题,DP的时候发现数组的开反了,f[205][205][405]开成了f[205][405][205];在写以自己优化他人的DP的时候因为不习惯,和以前自己被他人优化的DP的下标搞混了;看题目样例的时候其概率输入的都是整数,就以为都是整数,忘了看题目的说明其为实数,看错题已经不是一次两次了,一定要看清楚题目要求,数据范围与类型,要对题目理解透彻,不要想当然。

16.技巧 贪心

    贪心是个需要智商的东西,出难了之后就真的很艰难了……只能秉承多看多练多总结的原则,小心谨慎对待了。总结一下自己的体会:

    (1)不浪费原则。有些题目的代价并非着眼于具体的数值,而是诸如以其中最大最小值为代价,以装数据的容器数量计算等,遇到这种计价规则,一个思路就是不浪费原则,其中有一道经典的问题就是乘船问题,说的是有一群人要乘船,船的载重量都是一样的,且每艘船最多只能装2个人,要求每个人都要坐上船,求最少需要的船的数量。这道题的解决方法就是典型的不浪费原则,从最轻的人开始,他是一定要上船的,上完了之后,剩下的空间尽量避免浪费,那么就装上能够和他一起乘船的最重的人就行了。

    (2)数学表达式分析。有些题目的决策与数据描述能够较为方便地使用数学表达式来表示的,把它写出来,经过分析与变换,说不定就能找到解法,这种方法不仅仅适用于贪心,在其余类型的题目上也有很大帮助。

    典型例题1.排队打水问题,n个人去打水,每个人都有固定的打水时间,要求给出一个合适的排队顺序,使所有人的等待时间加起来最小。

    我们以ti表示第i个人打水所需的时间,那么答案就为t1+(t1+t2)+(t1+t2+t3)……+(t1+t2+t3……+tn)=nt1+(n-1)t2+(n-2)t3+……+tn,由于前面的系数是一定的,ti本身也是一定的,那么为了使和尽量小,我们只需要将时间最少的放到系数最大的上面,次小的放到次大的上面,依此类推就行了。

    典型例题2.糖果传递问题,n个人围成一圈,每个人手上有固定的糖果数,一个人可以向左向右传递他的一部分糖果,每个糖果的传递代价为1,现在,若要使每个人手上都有一样的糖果数,那么求最少需要多大的代价才能达到这个目标。

    初看这道题,也是觉得无从下手,然而,让我们冷静地用数学表达式将其表示出来:每个人的糖果数是已知的,所以平均数能够算出来为m,然后我们用xi表示第i个人给了第i-1个人多少个糖果,当x1为正时,拿出,为负则表示收入,那么,我们可以列出方程:

    s1-x1+x2=m则x2=x1-(s1-m);

    s2-x2+x3=m则x3=x2-(s2-m)=x1-(s1+s2-m-m);

    s3-x3+x4=m则x4=x1-(s1+s2+s3-m-m-m),到这里我们可以看出,xi=x1-(s1+s2+s3……+si-m*i),由于我们最终的答案是|x1|+|x2|+|x3|……+|xn|,而xi又可以由x1来表示,所以最终问题转化成了求一个点使其到其余点的距离最小,即寻找中位数!问题也就迎刃而解了。

    典型例题3.CQOI2009中位数,给出一个n的排列,求在此排列中有多少个连续的奇数长度的子序列其中位数为指定的b。

    看起来以为很难,其实不然,分析一下,当一个序列符合条件时,其在原始序列中满足b的左边的比它大的数加上右边比它大的数等于其左边比它小的数加上其右边比它小的数,写成式子就是 lmax+rmax=lmin+rmin,由于这个式子将左边和右边的数向联系了起来,不好用,我们把式子移项,则lmax-lmin=rmin-rmax,这样就一下子看出了解法:两次扫描求出lmin,lmax和rmin,rmax,其间用hash表记录下lmax-lmin的值出现的个数以及rmin-rmax的值出现的个数,然后对于每个左边的差值的个数,直接乘右边的差值的个数,ans+=,搞定。

    (3)等价转换,有些贪心的题目可以像DP一样通过子结构分析缩小其问题规模,从而快速求解。比如NOIP2002均分纸牌,与上面一道糖果传递所不同的是,这道题是一条链(代价的计算方式倒是无所谓)。我们首先求出平均数m之后,将每堆牌的数量减去m,这样,一堆牌的数若为正,则表示其牌多了,为负则少了,那么我们从左向右,假如第一堆牌的数量为0,那么明显它不影响本问题的答案,可以直接去掉,如果其不为0,那么对于它自己的代价是一定有的,所以ans++,并且把它的值加给下一个数,因为其若为正,给下一个数代表移走了自己这堆上的数,而下一堆就多了这么多要处理,为负数同理,这样问题就由n堆牌的问题转化为了n-1堆牌的问题,通过这样不断缩小,最终就得到了答案。

    (4)分类讨论,例如过河问题,总共只有1艘船,这艘船最多载两个人,一次运载的时间等于两个人当中所用时间更多的那一个,求最少的运输时间。这个看起来像小学奥数一样的题就需要用到分类讨论的思想,首先很容易想到其中一种贪心方法,那就是每次都用最轻的人去送最重的人,这样运送的总时间就为除最轻的人以外的人的时间总和加上(n-2)*t1,有没有比这更好的方案呢?不好说,我们发现这种方法的弊端就是每个最大值都用了一遍,有没有办法避免呢?当然有,把最大的次大的一起运,就可以覆盖掉次大的所需的运送时间,那么怎么回来呢?我们可以先让t1,t2在对岸等待,在tn,t(n-1)过去之后,由t1接应把船运回去。这样,我们就有两个决策,看起来像什么?动态规划!!我们记f[i]表示运送完前i个人所需的最小的时间(i已经按照时间排序),那么f[i]=min(f[i-1]+t[1]+t[i],f[i-2]+t[i]+2*t[2]+t[1]),即贪心优化下的动态规划。

    (5)关于区间的经典贪心问题。

        给出n个区间,要求选择最多的两两不相交的区间,我们将其按照右端点排序之后,对于每个线段,能选则选,这里运用到了等价转换缩小问题规模的思想,对于第一个线段,若不选它,而选了i,那么若第一个与i不冲突,则多选一个答案更优,若冲突,因为其右端点小于等于i的右端点,对之后答案的选择的限制更小,按照这种思路不断缩小问题规模就行了。

        给出n个区间,要求选择最少的点,使每个区间里都至少有一个点,同上面一道题的思路类似,将其按照结束位置排序之后,对于每个区间,若其已经满足要求,则跳过,不然就选其最后一个点,因为在排序之后,结束位置的递增保证了当前所选的点一定不会超过剩余区间的右端点,那么就尽量往大的选就OK了。

        给出一条指定的线段和一些区间,要求选择最少的区间覆盖完这条线段,解决方法是将线段按照左端点排序之后,对于左端点小于等于线段的左端点的线段,选择其右端点最大的,这是显然的,因为指定线段的左端点只能由这些线段来覆盖,而剩下的线段当然是越短越好(选择大区间并不会改变其位置)。

    (6)流水调度问题。n个作业要在两台机器M1M2组成的流水线上完成加工。每个作业i都必须先花时间aiM1上加工,然后花时间biM2上加工。确定n个作业的加工顺序,使得从作业1在机器M1上加工开始到作业n在机器M2上加工为止总时间最少。

    说来惭愧,自己第一次看到这个题的数据范围就蒙了,想不出除了暴力枚举以外的方法——动规近似于暴力枚举,直到现在也只能死用Johnson算法,而没有太多的感触。

    Johnson算法:N1a<b的作业集合,N2a>=b的作业集合,将N1的作业按a非减序排序,N2中的作业按照b非增序排序,则N1作业接N2作业构成最优顺序。

    带罚款(奖励)和期限的任务调度问题。n个任务,每个任务都需要1个时间单位执行,任务i的截止时间di(1<=di<=n)表示要求任务i在时间di结束时必须完成,误时惩罚wi表示若任务i未在时间di结束之前完成,将导致wi的罚款。确定所有任务的执行顺序,使得惩罚最小。我们的直观思想是尽量完成罚款较大的,所以我们先将罚款按照从大到小的顺序排序,对于一个任务,如果能够完成,则将其安排在能够完成的尽量后面的位置,不然则直接扔到最后。证明很简单,如果此种方案不是最优的,那么说明在调度过程中,一定至少有一个应当被安排的任务因为前面的任务占据了它的位置而没有得到安排,那么若我们将其与前面已经安排的任务中的某一个进行交换,因为这“某一个”处在它所能承受的最后一个位置,所以交换了之后就一定损失掉了,而交换过去的这一个虽然免去了罚款,但却没有前面的赚,所以还是赔了,而在交换之后,该占的位置还是被占了,所以在位置占领上不会与全面有什么区别,所以一定不会得到更优的答案。这道题的实现方法比较具有代表性,一种是使用堆,从最后一天开始,顺序加入截止时间超过i的任务,加入完之后,取堆顶,这种方法和前面我们的从大罚款到小罚款的安排的算法是一样的,还要更加直观一些,即反正这个位置只有这些人能够使用,那么肯定要给一个最不亏的,而没有没被选到的人因为其截止时间靠后,还可以同接下来加入的任务一起参选,这种处理思路非常值得借鉴。至于领完一种,就是使用并查集,fa[i]表示当前不超过i的区间当中最后一个可以放的,在运行的时候,对于一个任务i,pos=Get(i),if(pos>0)fa[pos]=pos-1,也是比较巧妙的思路,且实现更为简单,速度更快。

  (7)反悔机制。很多时候,我们无法贪心直接得到正确答案的时候,就要想,能否使用数据结构或者其他的什么类似的东西,来构建一个反悔机制,使其能够在一个特殊的,足以动摇已经做出的最优决策序列的选项出现的时候,及时将决策调整到最新的最优决策!

     比如说USACO 2013 FEB中的买牛,总共有m块钱,n头给定价格ai的牛,还有已经持有的k张优惠券,每头牛在使用优惠券之后价格都会降到bi,求最多能买多少头牛。给人的感觉这道题是可以使用DP的,我们使用f[i][j]代表买i头牛且持有j张优惠券的最小花费,对于一个新的牛l,f[i][j]=min(f[i][j],f[i-1][j]+a[l],f[i-1][j-1]+b[l]),其最坏的时间复杂度是n^3,然而n<=50000,这样做基本得不到什么分,而且像这种转移为O(1)的动规,也不能在转移上做出优化。所以我们需要另辟蹊径,贪心!我们首先将使用了优惠券的牛按照价格从小到大排序,并按顺序选择k头牛——如果选不到k头牛是再好不过的了,说明我们的钱连我们用完优惠券都不够,那么当前的选择一定是最优的,直接输出就OK了。这样的处理,使我们能够保证当前k步的选择一定是最优的,然后对于剩下的牛,就有一个问题了,有可能剩下的牛不打折的价格都超级贵,而打折之后价格降低很多但是却高于已经选择的牛一点点——不然它也不会被剩下了,而已选的使用了优惠券的牛虽然打折后价格比较低,但是有可能其原价根本就不高,这样的话如果我们直接买没有被打折的最便宜的那头牛就会血亏,就不是最优决策了!怎么办呢?我们遇到这种情况,先别急着全盘放弃自己已经想好的贪心,而应该去想,是否有一个可行的方法去现场补救!像这道题就是有的,对于新来的那头牛,我们假设其是使用了优惠券的——优惠券哪里来呢?从已经使用优惠券购买的牛那里,退回来!我们将一个使用了优惠券的牛变为未使用优惠券的牛,这样价格就会增加,其等价于我们花费了一定的费用购买优惠券!这个一定的费用是多少呢?当然是取原始价格与打折价格的差值最小的那一个,最便宜嘛,然后我们将这头牛用在使用了优惠券最便宜的那头牛身上,就得到了其中的一个选项,另一个选项就是,购买不打折的价格最低的那头牛。实现也不难,一个直观而不一定最好的实现就是用堆维护一个原价与优惠券的差值,一个用了优惠价,一个原价,每次比较是原价最小还是优惠价加上买优惠券最小。

    再比如说这样一道题,给你n天的股票的价格,每天你可以最多买入一股,最多卖出一股,要求在n天后不能持有股,求最多能赚多少钱,这道题下意识一看,也是能够用动规来做的,我们用f[i]代表持有i股最少花费多少钱,那么对于新的一天k,f[i]=min(f[i],f[i-1]+a[k]),f[i]=min(f[i],f[i+1]-a[k]),最后的答案为-f[0]。然而这样做,时间复杂度也是n^2。思路又回到了贪心,对于新来的一股,我们先将其扔入一个小根堆,代表当前最低的价格是多少,这一股,我们当然是能赚则赚,若目前的堆堆顶价格小于它,则按照其价格卖出,ans+=差值,出堆顶,但是要额外进入一个当前价格,为什么呢?反悔!比如说股票的价格分别是1 3 4,那么第一天1进堆,第二天3来了,鼠目寸光地赚了2块钱,然后按照我们的算法,堆里还剩两个3,,这两个数的含义是不同的,一个3代表模拟已经买入所持有的虚拟股,另一个3代表的是1的反悔残骸,因为1被错误地在第2天卖出了,只赚了3-1块钱,但是我们留下了痕迹,即操作的痕迹,我们根据这个操作的痕迹,在一个贪心原则下,反悔了,即下一次4来了,它选择了堆里的3,赚了4-3块钱,相当于4-3+3-1=3,总共赚了3块钱,等价于其撤销了由3赚1的选择,选择了由4赚1,成功反悔!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值