• 一些动规水题


    开学以来的一个半月做了不少动规(水)题,那就写写题解吧。

    1. Vijos 1059 积木城堡

    一开始看到这是浙江省选题顿时吓尿。但其实是大水题(主要体现在数据弱)。

    用背包求出每个城堡能达到的高度,取所有城堡都能达到的最大高度即可。

    具体一点:

    用 f(i, j, k) 表示对于第 i 个城堡,用前 j 个积木能否拼成 k 的高度,则

    f(i, j, k) = f(i, j-1, k) or f(i, j-1, k-h(j))

    边界 f(i, 0, 0) = true

    然后滚动数组滚起来。如果 f(i, j, k) 为 true,就给 hash[k]++,最后扫一遍 Hash 数组找到最大的 k 满足 hash[k]=n,输出。

    2. Vijos 1014 旅行商问题简化版

    先按照点的横坐标排序。

    问题可以转化为两个人同时从第一个点出发走两条不相交(除起点终点外没有共同点)的路径到达最右边的点所走的最短路径。

    用 f(i, j) 表示第一个人走到了点 i 、第二个人走到了点 j 时的最短路径长度(因为 f(i, j)=f(j, i),所以我们假设 i>j),则

    若 j=i-1,f(i, j) = dist(k, i)+min{ f(k, i-1) },1<=k<i-1

    若 j<i-1,f(i, j) = dist(i-1, i)+f(j, i-1)

    3. Vijos 1037搭建双塔

    其实这道题还是不错的,但是数据很弱所以就被我无脑水过去了。

    用 f(i, j, k) 表示能否用前 i 个水晶搭建高分别为 j 和 k 的两座塔,则

    f(i, j, k) = f(i-1, j, k) or f(i-1, j-h(i), k) or f(i-1, j, k-h(i))

    边界条件 f(0, 0, 0) = true

    理论上这样做的最多操作次数可能达到 4 亿。但是由于数据弱,还是被本蒟蒻水过去了。

    更好的做法是用 f(i, j) 表示用前 i 个水晶搭建高度相差为 j 的两座塔中较低的塔最高是多少,则

    f(i, j) = max{ f(i-1, j), f(i-1, j+h(i))+h(i), f(i-1, j-h(i)), f(i-1, h(i)-j)+h(i)-j }

    看起来很不好理解,其实决策就是把第 i 个水晶放在高的塔还是矮的塔。如果放在高塔上,那么高塔与矮塔之前的差为 j-h(i);如果放在矮塔,这里有两种情况:1)矮塔变成了较高的塔,那么之前的差应该是 h(i)-j;2)矮塔依然是矮塔,那么之前的差是 h(i)+j。画个图会更加清楚。

    4. Vijos 1006晴天小猪历险记之Hill

    这是一道看起来不那么水的题目。

    这里我们先把问题看做是从上往下走,然后很快可以写出一个有点问题的动规方程。用 f(i, j) 表示走到第 i 行第 j 列时的最短路径,则

    f(i, j) = min{ f(i, j-1), f(i, j+1), f(i+1, j), f(i+1, j+1) }

    对应四种决策:往左走、往右走、往左下走、往右下走。而边界的时候,比如当 j=i 时意味着在最右边一列,再往右走会走到第 1 列。很明显,这里有个死循环在里面。

    而我的解决方法是边界处特殊处理。从右边界往右走到达左边界,继续往右走会回到右边界。显然,在第 i 行的右边界往右走不会超过 i 步,否则相当于走了一圈,没有任何意义反而增加了路径长度。

    特殊处理并不难,只要枚举从右边界开始往右走最后停在哪个点然后往下走即可。

    左边界也是一样的道理。(题目里没有说清楚的是从右边界往右可以到左边界,从左边界往左也可以到右边界)

    但是这种方法比较另类。比较普遍的做法是某些人所说的「暴力 DP」,其实就是对于一个阶段内的状态并不止计算一次,如果有一个状态的值更新了就会导致另一些状态的值更新,直到所有状态的值都不再更新为止,这样就有效避免了死循环。这里有点 SPFA 的味道,所以 LZW 大神就直接用 SPFA 做了。

    5. NOIP2013DAY2T3 花匠

    后来听说了这道题的 O(N) 算法之后真是一口老血吐在屏幕上。

    明确一下题目的意思是去掉最少的元素使得一个序列成为波浪形上下起伏。为了叙述方便我们称这样的序列为合法序列。

    我们可以用 f(i, 0) 表示以 i 这个元素结尾且 a(i) 小于前一个元素的合法序列的最大长度,用 f(i, 1) 表示以 i 这个元素结尾且a(i) 大于前一个元素的合法序列的最大长度,则

    f(i, 0) = max{ f(j, 1), j<i 且 a(i)<a(j) }

    f(i, 1) = max{ f(j, 0), j<i 且 a(i)>a(j) }

    如果直接暴力找 j,那么复杂度是 O(N^2) ,无法承受。

    然后 LZW 大神用的是线段树来维护最大值。因为常数比较大所以稍慢。

    而我发现这个方程很像 LIS,LIS 维护了一个有序序列+二分查找使得复杂度变为 O(NlogN),那么这道题应该也是可以的。于是我们记 x(i) = min{ a(j) }, f(j, 1)=i,第一个方程就可以变成

    f(i, 0) = k+1, k 是满足条件 x(k)>a(i) 的第一个元素

    那么这个 k 就可以通过二分查找来得到(x 数组的递增性是显然的)。

    同理,可以另设一个 y 数组充当类似的作用。

    但是很关键的一点是,x 数组的更新和 LIS 是不一样的。得到了一个 f(i, 1) 之后可能需要更新许多 x 值,这里的循环次数不好估计。所以我这样也算是碰运气过了?不过事实证明循环次数不会太多。至少从时间上来看远小于线段树的方法。

    至于 O(N) 神算法就是统计「拐点」的个数,因为最优解不过就是包含了所有这些拐点而已。orz。

  • 相关阅读:
    Redis常见数据类型
    MYSQL常见可优化场景
    算术切片
    找数组里没出现的数
    不同路径和(II)
    不同路径和
    最小路径和
    强盗抢房子
    丑数(2)
    判断子序列
  • 原文地址:https://www.cnblogs.com/lsdsjy/p/dp_water.html
Copyright © 2020-2023  润新知