从《Cash》谈一类分治算法的应用

从《Cash》谈一类分治算法的应用

分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同.求出子问题的解,就可得到原问题的解.分治算法非常基础,但是分治的思想却非常重要,本文将从今年NOI的一道动态规划问题Cash开始谈如何利用分治思想来解决一类与维护决策有关的问题:

例一.货币兑换(Cash)

问题描述

小Y最近在一家金券交易所工作.该金券交易所只发行交易两种金券:A纪念券(以下简称 A 券)和 B 纪念券(以下简称 B 券).每个持有金券的顾客都有 一个自己的帐户.金券的数目可以是一个实数.

每天随着市场的起伏波动,两种金券都有自己当时的价值,即每一单位金券 当天可以兑换的人民币数目.我们记录第 K 天中 A 券和 B 券的价值分别为 AK 和 BK(元/单位金券).

为了方便顾客,金券交易所提供了一种非常方便的交易方式:比例交易法. 比例交易法分为两个方面:

A) 卖出金券:顾客提供一个[0,100]内的实数 OP 作为卖出比例,其意 义为:将 OP%的 A 券和 OP%的 B 券以当时的价值兑换为人民币;

B) 买入金券:顾客支付 IP 元人民币,交易所将会兑换给用户总价值为

IP 的金券,并且,满足提供给顾客的 A 券和 B 券的比例在第 K 天恰好为 RateK

例如,假定接下来 3 天内的 Ak、Bk、RateK 的变化分别为:


时间


Ak


Bk


RAtek


第一天


1


1


1


第二天


1


2


2


第三天


2


2


3

假定在第一天时,用户手中有 100 元人民币但是没有任何金券. 用户可以执行以下的操作:


时间


用户操作


人民币(元)


A 券的数量


B 券的数量


开户



100


0


0


第一天


买入 100 元


0


50


50


第二天


卖出 50%


75


25


25


第二天


买入 60 元


15


55


40


第三天


卖出 100%


205


0


0

注意到,同一天内可以进行多次操作.

小 Y 是一个很有经济头脑的员工,通过较长时间的运作和行情测算,

他已经 知道了未来 N 天内的 A 券和 B 券的价值以及 Rate.他还希望

能够计算出来,如 果开始时拥有 S 元钱,那么 N 天后最多能够获得多少元钱.

算法分析

不难确立动态规划的方程:

f [i]表示第i天将所有的钱全部兑换成A, B券,最多可以得到多少A券.很容易可以得到一个O(n2)的算法:

f [1]←S * Rate[1] / (A[1] * Rate[1] + B[1])

AnsS

For i ← 2 to n

For j ← 1 to i-1

x ← f [j] * A[i] + f [j] / Rate[j] * B[i]

If x > Ans

Then Ans ← x

End For

f [i] ← Ans * Rate[i] / (A[i] * Rate[i] + B[i])

End For

Print(Ans)

O(n2)的算法显然无法胜任题目的数据规模.我们来分析对于i的两个决策jk,决策j比决策k优当且仅当:

  (f [j] – f [k]) * A[i] + (f [j] / Rate[j] – f [k] / Rate[k]) * B[i] > 0.

不妨设f [j] < f [k],g[j] = f [j] / Rate[j],那么

(g[j] – g[k]) / (f[j] – f[k]) < -a[i] / b[i].

这样我们就可以用平衡树以f [j]为关键字来维护一个凸线,平衡树维护一个点集(f [j], g[j]),f [j]是单调递增的,相邻两个点的斜率是单调递减的.每次在平衡树中二分查找与-a[i] / b[i]最接近的两点之间的斜率.

这样动态规划的时间复杂度就降低为O(nlog2n),但是维护凸线的平衡树实在不容易在考场中写对L,编程复杂度高,不易调试(我的Splay代码有6k多).这个问题看上去只能用高级数据结构来维护决策的单调性,事实上我们可以利用分治的思想来提出一个编程复杂度比较低的方法:

对于每一个i,它的决策j的范围为1~i-1.我们定义一个Solve过程:

Solve(l, r)表示对于的l ≤ i ≤ r,用l ≤ j ≤ i-1的决策j来更新f [i]的值.这样我们的目标就是Solve(1, n):可以先Solve(1, n/2)后计算出f [1] .. f[n/2],那么1~n/2的每一个数一定是n/2+1~n的每个i的决策,用1~n/2的决策来更新n/2+1~nf[i]值后Solve(n/2+1, n).这恰好体现的是一种分治的思想:

用1~n/2的决策来更新n/2+1~nf[i]值:类似用平衡树的方法,我们可以对1~n/2的所有决策建立一个凸线,对n/2+1~n的所有i按照-a[i] / b[i]从大到小排序,凸线的斜率是单调的,-a[i]/b[i]也是单调的,这样我们就可以通过一遍扫描来计算出对于每一个i在1~n/2里面最优的决策j

现在面临的问题是如何对于一段区间[l, r]维护出它的凸线:由于f []值是临时计算出来的,我们只需要递归的时候利用归并排序将每一段按照f []值从小到大排序,凸线可以临时用一个栈O(n)计算得出.下面给一个分治算法的流程:

由于-a[i] / b[i]是已知的,不像f [i]是临时计算得出的.因此可以利用归并排序预处理,计算每一段[l, r]按照-a[i] / b[i]排序后的i

Procedure Solve(l, r)

If l = r

  Then更新ans,利用已经计算好的l的最优决策k,计算f [l]值,Exit

Mid ← (l + r) / 2

Solve(l, mid -1)

对[l, mid-1]这一段扫描一遍计算出决策的凸线,由于[mid+1 .. r]这一段以

-a[i] / b[i]的排序在预处理已经完成,因此只需要扫描一遍更新[mid + 1 .. r]

的最优决策.

Solve(mid+1, r)

利用[l, mid-1]已排好序的f []值和[mid+1, r]已排好序的f []值归并排序将

[l, r]这一段按f[]值排序. 

End Procedure

至此,问题已经基本解决.时间复杂度为T(n) = 2T(n/2) + O(n),因此算法的时间复杂度为O(nlog2n),NOI2007的测试数据最慢的测试点0.2s,是一个相当优秀的算法.

我们比较一下分治算法和用平衡树维护决策的方法:时间复杂度均为O(nlog2n),空间复杂度平衡树为O(n),分治为O(nlog2n) (预处理-a[i]/b[i]需要O(nlog2n)的空间).但是编程复杂度却差别非常大,分治算法实现起来相当简单,对于考场来说无疑是一个非常好的方法.

在编程复杂度非常高的情况下,动态规划维护决策与分治思想很好的结合起来从而降低了编程复杂度,无疑是“柳暗花明又一村”.这种分治思想主要体现在将不断变化的决策转化成一个不变的决策集合,将在线转化为离线.下面我们再来看一个例题:

例二.Mokia

问题描述

有一个W * W的棋盘,每个格子内有一个数,初始的时候全部为0.现在要求维护两种操作:

1) Add:将格子(x, y)内的数加上A

2) Query:询问矩阵(x0, y0, x1, y1)内所有格子的数的和.

数据规模:操作1) ≤ 160000,操作2) ≤ 10000,.

算法分析

这个问题是IOI 2000 Mobile的加强版:Mobile中W≤1000,就可以利用二树状数组在O(log22n)的时间复杂度内维护出操作1)和操作2).这个问题中W很大,开二维树状数组O(W2)的空间显然吃不消,考虑使用动态空间的线段树,最多可能达到操作次数 * (log2W)2个节点,也相当大了.考虑使用分治思想来解决问题:

将操作1)和操作2)按顺序看成是一个个事件,假设共有Tot个事件,Tot≤170000.类似例题一,我们定义Solve(l, r)表示对于每一个Query操作的事件i, 将l ..i-1的Add操作的所有属于i的矩形范围内的数值累加进来.目标是Solve(1, n).

假设计算Solve(L, R),递归Solve(L, Mid),Solve(Mid + 1, r)后,对L .. Mid的所有Add操作的数值累加到Mid + 1 .. R的所有匹配的Query操作的矩形中.

后面这个问题等价于:平面中有p个点,q个矩形,每个点有一个权值,求每个矩形内的点的权值之和.这个问题只需要对所有的点以及矩形的左右边界进行排序,用一维树状数组或线段树在O((p+q)log2W)的时间复杂度即可维护得出.

因此问题的总的时间复杂度为O(Tot*log2Tot*log2W),不会高于二维线段树的O(Tot*log2W*log2W)的时间复杂度.

上述这个算法无论是编程复杂度还是空间复杂度都比使用二维线段树优秀,分治思想又一次得到了很好的应用.在这个问题中,利用分治思想我们将一个在线维护的问题转化成一个离线问题,将二维线段树解决的问题降维用一维线段树来解决,使得问题变得更加简单.

总结

 【例题一】是一个数据结构维护动态规划决策的问题,【例题二】为一个数据结构维护数据信息的问题.我们巧妙地使用分治思想,将在线维护的问题转化为离线问题,将变化的数据转化为不变的数据,使得问题解决更加的简单.

时间: 2024-07-29 20:27:11

从《Cash》谈一类分治算法的应用的相关文章

【BZOJ】1492: [NOI2007]货币兑换Cash(cdq分治)

http://www.lydsy.com/JudgeOnline/problem.php?id=1492 蒟蒻来学学cdq神算法啊.. 详见论文 陈丹琦<从<Cash>谈一类分治算法的应用> orz 此题表示被坑精度.....导致没1a...开小号交了几发....................坑. 蒟蒻就说说自己的理解吧.. 首先这题神dp...(表示完全看不出来) 首先我们要最大化钱,那么可以将问题转化为最大化A券!(或B券)!!!!这点太神了,一定要记住这些!! 设d[i]表

[BZOJ 1492][NOI2007]货币兑换Cash(CDQ分治+斜率优化Dp)

Description 小Y最近在一家金券交易所工作.该金券交易所只发行交易两种金券:A纪念券(以下简称A券)和 B纪念券(以下 简称B券).每个持有金券的顾客都有一个自己的帐户.金券的数目可以是一个实数.每天随着市场的起伏波动, 两种金券都有自己当时的价值,即每一单位金券当天可以兑换的人民币数目.我们记录第 K 天中 A券 和 B券 的 价值分别为 AK 和 BK(元/单位金券).为了方便顾客,金券交易所提供了一种非常方便的交易方式:比例交易法 .比例交易法分为两个方面:(a)卖出金券:顾客提

再回首--分治算法

谈起分治算法,首先从字面意思理解:就是将一个问题划分成多个较小的问题的算法.其实正应题目的意思.其基本设计思想就是:将一个难以直接解决的大问题分解成一些规模较小的相同问题以便各个击破,分而治之. 设计步骤:1)分解:分解成若干子问题 2)求解:求解个子问题 3)合并:将子解合并成原问题的解. 在自考的时候,我们遇到的二路归并算法就属于一种分治法.当然,要学会算法,就要找到其核心,抓住其核心了,我们也就明白算法是怎么回事了.下面我们通过二路归并算法找到其核心. 例子:给出一列数:4,2,8,3.利

算法浅谈——分治算法与归并、快速排序(附代码和动图演示)

在之前的文章当中,我们通过海盗分金币问题详细讲解了递归方法. 我们可以认为在递归的过程当中,我们通过函数自己调用自己,将大问题转化成了小问题,因此简化了编码以及建模.今天这篇文章呢,就正式和大家聊一聊将大问题简化成小问题的分治算法的经典使用场景--排序. 排序算法 排序算法有很多,很多博文都有总结,号称有十大经典的排序算法.我们信手拈来就可以说上来很多,比如插入排序.选择排序.桶排序.希尔排序.快速排序.归并排序等等.老实讲这么多排序算法,但我们实际工作中并不会用到那么多,凡是高级语言都有自带的

从决策树学习谈到贝叶斯分类算法、EM、HMM

从决策树学习谈到贝叶斯分类算法.EM.HMM 引言 近期在面试中,除了基础 &  算法 & 项目之外,经常被问到或被要求介绍和描写叙述下自己所知道的几种分类或聚类算法(当然,这全然不代表你将来的面试中会遇到此类问题,仅仅是由于我的简历上写了句:熟悉常见的聚类 & 分类算法而已),而我向来恨对一个东西仅仅知其皮毛而不得深入,故写一个有关数据挖掘十大算法的系列文章以作为自己备试之用,甚至以备将来经常回想思考.行文杂乱,但侥幸若能对读者起到一点帮助,则幸甚至哉. 本文借鉴和參考了两本书,

从决策树学习谈到贝叶斯分类算法、EM、HMM --别人的,拷来看看

从决策树学习谈到贝叶斯分类算法.EM.HMM 引言 最近在面试中,除了基础 &  算法 & 项目之外,经常被问到或被要求介绍和描述下自己所知道的几种分类或聚类算法(当然,这完全不代表你将来的面试中会遇到此类问题,只是因为我的简历上写了句:熟悉常见的聚类 & 分类算法而已),而我向来恨对一个东西只知其皮毛而不得深入,故写一个有关数据挖掘十大算法的系列文章以作为自己备试之用,甚至以备将来常常回顾思考.行文杂乱,但侥幸若能对读者起到一点帮助,则幸甚至哉. 本文借鉴和参考了两本书,一本是T

一类分治问题

有一类关于区间最大值和最小值之类的问题,利用单调性,可以采用分治算法解决. SPOJ22343 Norma 题意,给定一个数列,定义区间的代价为区间最大值.区间最小值.区间长度的成绩,求所有区间的代价和. 既然是分治,我们肯定要处理一个数列跨过中点的答案. 假设当前数列的中点为mid,我们从mid往前扫,扫到了i. 然后根据单调性,我们越往左扫,最大值单调不降,最小值单调不增. 那么我们可以在右边维护一个指针,表示满足最大值的区间的最靠右的端点. 假设有这么一种情况,那么我们可以把区间拆成mid

分治算法(Divide and Conquer)

分治算法 在计算机科学中,分治法是建基于多项分支递归的一种很重要的算法范式.字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并. 分治法所能解决的问题一般具有以下几个特征: 问题的规模缩小到一定的程度就可以容易地解决 问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质 利用该问题分解出的子问题的解可以合并为该问题的解 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问

分治算法

分治算法即将一个问题划分成多个子问题求解,最后的结果就是几个子问题的合集,通常图形类的算法,尤其是2的几次方数组问题可以优先考虑. 汉诺塔和二分搜索都是分治算法的思想,个人觉得最好体现分治算法的demo是棋盘覆盖问题,代码如下: #include <stdio.h> #include <stdlib.h> #define SIZE 4 static int title = 1; //title表示L型骨牌的编号 static int board[SIZE][SIZE]; /** *