HDU 3507 Print Article

题意

给出了一个费用计算公式,现在要你把一个数列分成若干组,每一组分别套用给出的费用计算公式计算费用。你要设计一种方法使得总费用最小。

思路

本题的DP方程推导相对容易(不过我学艺不精被卡在了这一步),之后我们会发现导出的方程完全无法满足时间限制,这个时候通过对费用公式的变形可以导出一种适用于斜率优化DP的形式。

斜率优化的操作非常复杂,幸运的是已经有人写出了详实的教程:斜率优化DP。这篇教程基本是斜率优化教程中最好理解的了,并且以这个题为例题。建议学习时手动模拟操作过程,以便形象、深入地理解。

代码

实现斜率优化的要点:1.合理正确地推导出斜率的表示形式和所有的转移方程、决策判定条件。2.正确地选择大于小于号。3.单调队列的实现,同时注意单调队列无法使用STL很好地实现,只能数组模拟。4.正确地初始化DP数组(非常重要)。

 

SGU 195 New Year Bonus Grant

题意

某厂采用一种奇怪的方式发奖金,给出一个上下级关系树,大BOSS是1号根节点,别的员工都有自己的上司。对于每个员工,它有三种选择,要么接受上级下发的奖金,要么给所有下级发奖金,要么啥都不做(只有傻子才会这样,所以你可以当成没这种选择),大BOSS一定发奖金。只要一个员工选择收奖金,他就得到1000块。

现在要你设计一种方案,使得所有人收到的奖金的总数额最大。

思路

树DP解法

对于每一个节点,都有两种状态:选择收奖金,选择收奖金。

对于选择收奖金的节点的儿子节点,一定不可以选择收奖金。

对于选择发奖金的节点的儿子节点,可以选择收他的奖金,也可以选择发奖金。

这样就可以建立dp数组dp[a][0/1],a是节点对应的编号,0/1表示该节点选择收/发奖金的状态。利用DFS下潜到叶子节点之后,就可以开始树DP了。

状态转移的方程可以参考dfs()方法。

输出路径的话,只要记录下每一次的决策,DP结束之后反查就可以搞定。

贪心解法

引用自JHC23的Blog。

自底而顶遍历,如果自己的上级没有接受奖金(那么就能够把钱给发自己了),而自己也从不增接受奖金则可以获得奖金,同时标记自己和上级。这样下去就可以得到可得到最大奖金。

代码

树DP解法:

 

HDU 4313 Matrix

题意

给你一颗树,上面有黑点(母体机器人所在位置),有白点(没有潜伏母体机器人的安全节点),并且给了你每一条边的边权(删边(炸毁道路)需要的时间代价)。现在要你以最小代价删掉一些边,使得没有两个黑点同时可以互相连通。输出这个时间花费即可。

思路

树形DP。这个题和Codeforces 461B Appleman and Tree是一个路子的题,都是在一棵树上不允许同时出现两个联通的特定类别的节点。这种问题我们在思考的时候,方向是将当前的大问题分解成同样的小问题,不断分解到最后,然后考虑怎么样根据一些小问题解得出大问题的解,这是DP的思路所在。

这次,我们设状态表示为dp[a][0/1],其中dp[a][0]表示要想让以a为根节点的子树满足题目的要求,而且a所在的联通块里面不可以有黑点,需要的最小代价是多少;dp[a][1]表示要想让以a为根节点的子树满足题目要求,且a所在的联通块里面有且只有一个黑点,需要的最小代价是多少。

设好了状态,我们开始想DP方程:

  • 我们当前检查的点标号是a,我们去逐一检查他的孩子节点,标号是b_{i},如果a是一个黑点:
    • dp[a][1] = \sum\min \left\{\begin{matrix}dp[b_{i}][0]\\ dp[b_{i}][0]+cost(a \rightarrow b_{i})\\ dp[b_{i}][1]+cost(a \rightarrow b_{i})\end{matrix}\right.
      因为当前节点是黑点,我们有三种操作可以选:

      • 当前讨论的孩子部分,如果他没有与任何黑点连着,我们可以把这个部分连入当前讨论的根部分,这就是dp[b_{i}][0],即孩子的代价就是根的代价,因为没有切断就没有增加代价。
      • 把根部分和当前讨论的孩子部分中间的边切断,这样的话无论孩子部分到底有没有和黑点连接,都没有关系,只要把孩子的代价加上切断代价就是这样做的代价了,也就是 dp[b_{i}][0]+cost(a \rightarrow b_{i})(孩子部分没有和黑点相连的情况下切断)和dp[b_{i}][1]+cost(a \rightarrow b_{i})(孩子部分有和黑点相连的情况下切断)。
      • 比较这三种选择,取最优就是局部最优解了。
    • dp[a][0]=\inf
      黑点不可能不和黑点相连,所以这里用inf屏蔽掉这种选择。
  • 如果a是一个白点:
    • dp[a][1] = \min \left\{\begin{matrix}dp[a][0]+dp[b_{i}][1]\\ dp[a][1]+dp[b_{i}][0]\\ dp[a][1]+dp[b_{i}][1]+cost(a\rightarrow b_{i})\\ dp[a][1]+dp[b_{i}][0]+cost(a\rightarrow b_{i})\end{matrix}\right.
      因为当前节点是白色,我们想要让他变成连着黑色的情况,这样的话有四种操作可以选:

      • 假如截止目前和根部连接的所有孩子都不包含黑点,我们可以让现在的这个孩子部分以含有黑点的形态和根部连接,使根部变成有黑点的情况。这就是dp[a][0]+dp[b_{i}][1]
      • 假如当前的根部已经连有黑点,为了满足要求,新联入的部分不可以再包含黑点,这就是dp[a][1]+dp[b_{i}][0].
      • 假如孩子部分被切断,这样的话就无所谓孩子长成啥样了,只要算一下代价就可以了,也就是 dp[b_{i}][0]+cost(a \rightarrow b_{i})(孩子部分没有和黑点相连的情况下切断)和dp[b_{i}][1]+cost(a \rightarrow b_{i})(孩子部分有和黑点相连的情况下切断)。
      • 这四种选择里面选最优的就好了。
    • dp[a][0] = \sum\min \left\{\begin{matrix}dp[b_{i}][0]\\ dp[b_{i}][0]+cost(a \rightarrow b_{i})\\ dp[b_{i}][1]+cost(a \rightarrow b_{i})\end{matrix}\right.
      当前节点是白色,我们想要让她不要根任何黑色连上,这样的话有3种选择:

      • 当前讨论的孩子部分没有和黑色连着,我们可以放心的把这部分连到根部,这就是dp[b_{i}][0]
      • 切断了和孩子部分之间的连接,也就是 dp[b_{i}][0]+cost(a \rightarrow b_{i})(孩子部分没有和黑点相连的情况下切断)和dp[b_{i}][1]+cost(a \rightarrow b_{i})(孩子部分有和黑点相连的情况下切断)。(其实我已经复制了这一段两遍。。。)
    • 注意这两个dp[a][0]dp[a][1]要同时处理,因为这里面有一个从“刚才”dp[a][0]到“现在”dp[a][1]的转移。

方程想好之后,DFS深入叶子部分,在返回的时候处理好dp数组就可以了,最后我们要在dp[0][0]dp[0][1]之间取个最小的,因为你所制定的根在最优解的时候连不连黑点并不知道。

代码

 

Codeforces 461B Appleman and Tree

题意

给你一颗树,上面有黑色节点,也有白色节点,现在你要做的事情是,在这棵树上选出几条边,去掉,这样的话这棵树就会变成好多小树,这些小树一定要含且只含一个黑色点。问你有多少种不同的方法删边来满足要求。

思路

树形DP。往DP方面去想是因为原始的大问题可以进行剖分,即如果我们想要知道将整棵树按要求切割的方案个数,我们可以去考虑它的子树按要求切割的方案个数,对于子树也是同样地进行剖分。确定了将问题剖分的方向之后,就是确定状态了。

我们发现,要想从一颗树的子树的分割情况得到整棵树的分割情况,我们不仅要知道子树符合要求的分割情况,也要知道子树不符合要求的分割情况,这样才能覆盖全部情况。因此我们设定状态为dp[a][0/1],其中dp[a][0]存值为将以a为根节点的子树进行分割,而且与当前的子树根节点相连的节点中没有黑色节点方法个数;dp[a][1]存值为将以a为根节点的子树进行分割,而且与当前的子树根节点相连的节点中有且仅有一个黑色节点方法个数。

这里一定要清楚,0/1表示的是与根节点联通的节点中是不是有黑色的节点,已经断开的地方,我们不用关心。

接下来讨论状态的转移:

  • 如果当前节点已经是叶子节点,节点编号是a
    • 如果是黑色节点:
      • dp[a][0] = 0 因为黑色节点为根部的子树不可能不含有黑色节点。
      • dp[a][1] = 1 因为只有黑色节点一个点,只有一种分割方式就是把节点自己当作一颗树。
    • 如果是白色节点:
      • dp[a][0] = 1 因为白色叶子节点的子树中只有他自己一个白色节点,不可能含有黑色节点了,分割方式也只有一种就是把自己当成子树。
      • dp[a][1] = 0 因为白色叶子节点的子树中不可能含有黑色节点。
  • 如果当前节点不是叶子节点,节点编号是a,它的孩子节点们的编号为b_{i}:
    • 如果是白色节点:
      •  dp[a][0] = \prod( dp[b_{i}][0]+dp[b_{i}][1]),我们逐一检查每一个孩子节点,我们想得到的结果是当前节点直接相连的联通块里面不存在黑色节点,那么:
        • 如果当前检查的孩子节点是黑色的,我们考虑切开还是不切开同这个孩子节点连接的边:
          • 不切开的话,要求连接的孩子部分一定不可以有黑色节点,这显然是不可能的,不过黑色节点的dp[a][0] = 0始终是0,所以没关系。
          • 切开的话,就要求切开之后的孩子部分自己可以满足要求,这样的话有dp[b_{i}][1]种方法。
        • 如果当前检查的孩子节点是白色的,我们考虑切开/不切开同孩子连接的边:
          • 切开的话,孩子部分自己要满足要求,这样的话有dp[b_{i}][1]种方法。
          • 不切开的话,孩子部分与根节点直接相连的部分一定不可以包含黑色节点,这样的话有dp[b_{i}][0]种方法。
      •  dp[a][1] =dp[a][1]*(dp[b_{i}][0]+dp[b_{i}][1])+dp[a][0]*dp[b_{i}][1] ,这个方程的计算必须和上面的dp[a][0]同步进行。这个有一点不好懂,其实可以这么想:
        • dp[a][1]*(dp[b_{i}][0]+dp[b_{i}][1]) 这一部分,我们要想的过程是将一个已经连接好了黑点的根部,讨论是不是要和当前的孩子相连的过程:
        • 如果当前检查的节点是黑色节点,我们还是考虑切开/不切开:
          • 不切开的话不满足要求,不过黑色节点的dp[a][0] = 0始终是0,所以没关系。
          • 切开的话,连接上去的孩子部分一定要满足要求,有且只有一个黑点,所以有dp[b_{i}][1]种方法。
        • 如果当前检查的是白色节点,切开/不切开:
          • 不切开的话,还是不满足要求,但是如果这个时候在根节点上已经有带黑点的联通块连上去了,这个时候只要令连接上去的部分没有黑点就可以了,所以有dp[b_{i}][0]种方法。
          • 切开的话,孩子部分要自己保持成立,有且只有一个黑点,所以有dp[b_{i}][1]种方法。
        • dp[a][0]*dp[b_{i}][1] 这一部分,想法是当前依然不成立的根部,和带有一个黑点的孩子部分连接。
    • 如果是黑色节点:
      • dp[a][0] = 0 因为黑色节点为根部的子树不可能不含有黑色节点
      • dp[a][1] = \prod (dp[b_{i}][0]+dp[b_{i}][1]) 我们逐一检查每一个孩子节点,既然我们想得到的结果是当前节点直接相连的联通块里面只有一个黑色节点,那么:
        • 如果当前检查的孩子节点是黑色的,我们考虑切开还是不切开同这个孩子节点连接的边:
          • 切开的话,没有问题,因为当前考虑的子树的根节点已经是黑的了,而且还要让让切开之后切出来的孩子的那一部分保持成立,要想做到这一点的话有dp[b_{i}][1]种方法。
          • 不切开的话,显然是不行的,因为这样就有了两个黑色节点,不过黑色节点的dp[a][0] = 0始终是0,所以加进去也没关系。
        • 如果当前检查的孩子节点是白色的,我们考虑切开/不切开同孩子连接的边:
          • 切开的话,还要保证切除的孩子部分也成立,因此孩子部分有dp[b_{i}][1]种方法。
          • 不切开的话,因为当前节点是黑色的,链接孩子节点之后,连接上的部分不可以含有黑色节点,孩子节点能做到这样的方法有dp[b_{i}][0]种。

写了一堆废话其实就是为了理清思路,思维能力太弱只能这样了。DP的思路搞定之后,具体的操作就非常简单,利用DFS深入叶子,在返回的过程中进行对孩子的考察和对自身的更新,最后在根节点上读取答案。

代码

 

15多校2-9/HDU 5308 I Wanna Become A 24-Point Master

更新

根据出题人的回复,问题应该已经修复了,本文代码中的PATCH部分可以忽略。关于原来出现的问题的证明也已经失效,敬请留意。

题意

给出一个数字n,问你如果给你n个n,你要怎么样才能利用加减乘除运算给算成24呢?注意,没个数字只能使用一次,所有的数字必须都要用到。你要按照题里面介绍的特别的表示方法把你的解法表示出来。除法是可以出分数的,因为是SPJ,应该可以过。

思路

我个人不太认同出题人题解里面给出的手算4~15的所有情况,对于较大的数再机智得直接消除凑24,因为手算的太多了。

我的思路是对于比较小的情况,即n <= 27的时候,可以直接利用dfs找出答案。因为题目给出的表示方法其实意外地适合dfs的搜索过程,我们不断的枚举下一个操作,即从当前没用过的数里面选出两个,然后尝试加减乘除,每一次尝试就是改变当前数组尾部的值,同时做好标记进入下一层。只要搜索成功,我们就可以在dfs的退出过程中将路径保存下来,开一个stack,把这些个结果压进去, 输出的时候弹出来,顺序就正好是正序了。

对于比较大的情况,事情就变得非常简单,假如说现在的n = k,那么只要找出24个相同的数k,加到一起,就是24 * k了,然后再额外拿一个k,做一下商,就得到了24,对于多出来的数,我们可以找两个k做一下差得0,然后用得到的0去乘上下一个多余的数,最后剩下一个0,和刚才的24加一下,多余的数也消耗掉了。

注意点

做这个题的时候发生了两处失误,首先是dfs的时候,范围设置的不正确,两层的循环都应该是从1开始的,原来外层从1开始而内层却是从i+1开始(i是外层的循环变量),这样显然丢解了,而且搜索速度也拖慢了。

其次是题目中要求每个数只可以用一次,结果我偷懒把一个0跟好多哥数字去乘了,这样肯定就错了。

然后dfs应该采取一些必要的优化,比如说跳过重复的区段(剪枝),如果不做的话可能超时。如果实在没办法,打表也可以,反正才24种情况,并不会被拒绝提交。

然而还是没过

折腾了一下午后发现很有可能这个题的评测器在实现上有漏洞,通过排(feng)除(kuang)法(jiao),发现有3组数据没有通过评测器,但是确实是正确的。

如果数据确实不符合题意,请与我跟进,谢谢。

代码

为了过,最后没有办法特殊对待给上面三组数据打了补丁。

标准答案里边很豪放地用了分数,正常来讲评测器应该是接受分数的,但不知道为什么不接受我这个调换位置保证不出现分数的答案了,真的很怪。我自己写了一个检查我自己答案的评测器,没有提供对分数的支持,但是评测器会在发现出现分数的时候报错,因为我的答案是已经调换顺序保证不出分数的了,此外也检查了每个数字的使用情况,保证和题意一致。

从1开始测了1个GB的数据,没发现问题,如果评测器有问题,请跟进,谢谢。

如果你还是不相信我

下面是官方题解:

QQ20150723-1@2x

 

 

虽然很蛋疼,写这么多其实就是为了让这么个玩意别再坑更多的人了。

Scroll to top