【做题笔记】dp,但是国庆限定版
Day 1
方块消除
看到这个数据范围就可以猜测正解是 \(O(n^4)\) 的 dp,与这个差不多相符合的可以想到区间 dp。然后大胆猜测一下就是区间 dp,令 \(dp[i][j]\) 表示消除掉 \([i,j]\) 后的最大价值,这个显然可以从长度更短的区间转移过来。所以此题我们可以从区间 dp 的方向思考。
为什么不能用二维状态当普通区间 dp 做? 假如按照普通区间 dp 做,可以造出任意一组需要三个合并消除之后的数据卡掉。这是因为这种 dp 具有后效性,我们不一定要全部消除 \([i,j]\) 这一段的方格,可以取一些保留下来,等到两边的区间都被消掉之后再将它与其他颜色相同的方格连续起来消掉。
所以就有一种很难想到的状态设计,令 \(dp[i][j][k]\) 表示消除 \([i,j]\) 这一段块的区间,但是 \(j\) 是和后面的 \(k\) 个颜色相同的方格(注意不是块)连起来一起消掉的。
为什么不能一个块一个块做? 因为有可能最优解需要将一个块中的任意多个方格取出来先消掉,之后再消这个块中的其他块。
\(col[i]\) 表示块 \(i\) 的颜色,\(v[i]\) 表示块 \(i\) 的大小。设 \(sum[i]\) 表示在 \(i\) 后面有多少个与它颜色相同的方格。边界就是 \(dp[i][i][k]=(v[j]+k)^2(\;(k\le sum[i])\)。
在初始化的时候,为什么不加上由于要让后面 \(k\) 个块落到 \(j\) 旁边而消掉的区间的价值? 所以我们补充一下状态的设计,这个状态仅考虑 \(k\) 个颜色相同的方格已经和 \(j\) 相连了它们的贡献 + \([i,j-1]\) 这一段的贡献,也就是不考虑让 \(k\) 个方格连续过来的贡献。
对于 \(dp[i][j][k]\) 它有两种可能的转移:
- 单纯消掉 \([j,k]\)。
- 找到 \([i,j]\) 中一个与 \(j\) 颜色相同的块 \(l\),将它与 \(j\) 和 \(k\) 一起消掉。
对于转移 \(1\),我们可以枚举 \(k\;(k\le sum[j])\),\(dp[i][j][k]=dp[i][j-1][0]+(v[j]+k)^2\)
为什么 \(j-1\) 就不能接后面的东西了? 如果 \(j-1\) 接的方格 \(x\) 在最后一个 \(k\) 的前面,那么要么 \(col[x]=col[k]=col[j]\) 导致 \(x\) 被保留下来了,但这显然不可能,因为 \(col[x]=col[j-1],col[x]=col[j]\) 但 \(col[j-1]\ne col[j]\)。所以 \(x\) 不会被保留下来,它早就会被消掉。如果 \(j-1\) 接的方格 \(x\) 在最后一个 \(k\) 的后面,考虑将 \([j,k]\) 消掉之后再消 \([j-1,x]\),那么这实际上是一个更大区间的转移 \(2\),我们这时候并不需要讨论。
对于转移 \(2\),我们可以先枚举与 \(j\) 颜色相同的 \(k\),再枚举后面要接的方格数 \(l\),那么 \(dp[i][j][l]=max(dp[i][k][v_{[j]}+l]+dp[k+1][j-1][0])\)。也就是先将 \([k+1][j-1]\) 这一段区间消掉,让 \(k\) 和 \(j\) 连到一起,再将 \([i,k]\) 这一段且 \(k\) 是和后面 \(v[j]+l\) 个方格连在一起消掉的。
为什么 \(k,j\) 又可以当成一整块做了? 注意状态是指消掉 \([i,j]\) 这一段块的整个区间的最大价值,那么无论是 \(k\) 还是 \(j\) 最后肯定都会被消掉。
最后的答案就是 \(dp[1][n][0]\)。
这道题是一种具有后效性的区间 dp,类似的题还有 它的双倍经验: 方块消除 Blocks ,祖玛。
Recovering BST
看到二叉搜索树,可以想到它有一个很好的性质:中序遍历单调不降。然后想到了 加分二叉树 这题,就可以考虑区间 dp 了(
考虑瞎套那道题,设 \(dp[i][j]\) 表示 \([i,j]\) 这一段是否能变成一颗树。但是此题与根节点还有关系,这样的状态并不能知道根节点是谁,所以设 \(dp[i][j][k]\) 表示 \([i,j]\) 这一段且以 \(k\) 为根节点是否能变成一棵树。
然后看一眼数据范围,\(700\times700\times700\),空间直接爆炸。考虑优化。
一个常规的三维优化方式是将第三维变成只有几种取值的维度(参考类型:关路灯)。考虑是否有这种可爱的性质能让第三维变成只有很少的取值。
幸运的是,我们确实有这种性质!当 \([l,r]\) 这段区间变成一棵树的时候,当它成为下一个区间的右子树的时候,它的根节点是 \(l-1\),当它成为下一个区间的左子树的时候,它的根节点是 \(r+1\)。这里只需要证明出两种情况中的任意一个,另外一个就能类似地推出来,所以这里只证明成为右子树的情况。
若 \([l,r]\) 这段为右子树,那么根据二叉搜索树的性质,它的根节点一定比这一段任意数小,也就是比 \(l\) 小,所以根节点取值范围是 \([1,l-1]\)。我们假设根节点是 \(l-x\;(1<x<l)\),那么 \((l-x,l)\) 这一段也必定要放在 \(l-x\) 的右子树中,但现在右子树的形状已经确定为 \([l,r]\) 了。所以假设不成立,\(x=1\)。
为什么不能把 \([l,r]\) 这段区间中间的某个值提出来当根节点? 这样的策略是可行的,但是我们发现在这种树上你要把 \(i/j\) 提出来也是可行的。我们只需要判断是否可行即可,而这种可行的话 \(i/j\) 为根节点也可行,所以没必要那么麻烦。
因此,我们设 \(dp[i][j][0/1]\) 表示 \([i,j]\) 这段区间且 \(i/j\) 为根节点是否能成为一颗树。状态转移的时候可以枚举断点 \(k\) 表示是从 \([i,k]\) 和 \([k+1,j]\) 这两棵树合并起来得到的。显然首先要满足 \(dp[i][k][0]=dp[k+1][j][1]=true\)。接着讨论是 \([i,k]\) 接到 \([k+1,j]\) 这颗树上还是 \([k+1,j]\) 接到 \([i,k]\) 这棵树上。根据二叉搜索树的大小关系,\(k+1\) 一定是最左边的左儿子,\(k\) 一定是最右边的右儿子,接过来的话一定是 \(i\) 变成 \(k+1\) 的左儿子或者 \(j\) 变成 \(k\) 的右儿子。判断一下这两种情况的 \(\gcd\) 是否为 \(1\) 即可。
为什么不能是 \(dp[i][k][1]\) 什么的转移过来?\(k\) 和 \(k+1\) 怎么你了? 这实际上跟之前那个问题是一样的,只要其中一种可行,那么最后也可行,你硬要判断一下也可以过,但实测会慢一点。
最后输出的时候直接判断 \(dp[1][n][0]=true\) 或者 \(dp[1][n][1]=true\) 就行了。
为什么其他题解都说最后答案要枚举一下根节点? 我不知道啊,但 实测 这样是可行的。并且理论上来说树的形态不会影响答案是否可行,那么最后也没必要枚举根节点了。也有可能是数据太弱吧/kk。
Dreamoon and Strings
这题难点大概是想到 dp 吧,毕竟看题面怎么都是一道字符串匹配题(。但发现用字符串算法好像没有什么很好的方法解决这道题(至少我不会),再加上可以想到 编辑距离 那题,想到这种删删减减字符串的题还可以用 dp 写,就大胆猜测一下是 dp。
设 \(dp[i][j]\) 表示 \(s\) 中的前 \(i\) 个删掉 \(j\) 个字符后包含的 \(p\) 的个数的最大值。首先无论如何都可以写出这样一个转移方程,\(dp[i][j]=max(dp[i-1][j],dp[i-1][j-1])\),意思就是当前这个字符要不要删。如果 \(s[i]=p[m]\)(\(m\) 为 \(p\) 的长度),那么就表示加上这一位后可能会贡献一个新的 \(p\)。但是要从哪一个状态转移过来呢?我们可以处理一下 \(f[i]\) 表示以 \(i\) 为结尾恰好产生一个子串的最大起点位置。这个可以 \(O(n^2)\) 预处理出来。那么这样就可以转移了,\(dp[i][j]=max(dp[f_{[i-1]}][j-(i-f_{[i]}+1-m)])\)。转移的过程中判断一下数组可能越位的情况就行,注意 \(i-f_{[i]}+1-m\ge0\)
答案输出显然是 \(dp[n][i](0\le i\le n)\)。
Day 2
Roman and Numbers
一眼看去会感觉这是数位 dp,但是显然这不是常规的数位 dp,它并没有上下界限制,而是数字个数限制。所以我们暂且抛弃常规数位 dp 的思考方式,然后发现对应这么小的数据范围,还有状压 dp 可以思考。
我们考虑将 \(n\) 中每一位数字选没选压缩成二进制,接着很套路的,我们设出状态 \(dp[i][j]\) 表示数位选择状态为 \(i\) 且此时这个对 \(m\) 取模的结果是 \(j\)。初始化 \(dp[0][0]=1\),这个也很显然。
由于知道一个取模后的数很难推取模前的数,但是知道一个取模前的数一定能推出取模后的数,所以我们考虑枚举 \(dp[i][]\) 来更新后面的。也就是 \(dp[i|(1<<(j-1))][(10k+a_{[j]})%m]+=dp[i][k]\)。
这样有个问题就是假设现在还有两个 \(8\) 没取,我们更新的时候会认为 \(xxxx8\) 和 \(xxxx8\) 是两种方案。所以我们考虑记录一下这一次访问到了哪些数字,只有第一次访问到的数字才会被更新方案就行了。对于前导零,我们特判一下第一位的时候不访问到 \(0\) 就行了。
难点大概是状压 dp,毕竟这个题面真的很容易想到数位 dp。
Bicolorings
首先对于这种题要对 \(2\) 这种常数十分敏感,比如 Maze 2D 这道题,常数 \(2\) 就给了它十分好的性质,可用于线段树维护。一般这种题把常数转化成 \(1\) 都会更简单,这题也不例外。
考虑常数为 \(1\) 的情况,我们发现只有前一位颜色与它不同的时候连通块个数才会增加,那么从一行变成两行,前面一列的状态依旧很小,可以枚举,那是不是可以按照一行的思路写下去呢?
所以我们设 \(dp[i][j][0/1/2/3]\) 表示到第 \(i\) 列为止,这一列状态为 \(00,01,10,11\) 且连通块个数为 \(j\) 个的方案数,看看前一列的哪些状态会对它有贡献,加上即可。注意初始化不是简单的 \(dp[0][0/1/2/3]=1\) 了,而是要手动枚举一下第一列的状态。
Party
看到数据范围是爆搜过不了,但状压 dp 可以过的范围,所以可以想到状压。我们设 \(dp[i]\) 表示互相认识的人的状态是 \(i\) 的最小操作次数。比如现在 \(2,3,4\) 都互相认识,但 \(1\) 只是和 \(2\) 认识,那么二进制下的状态应该是 \(0111\)。
设 \(a[i]\) 表示第 \(i\) 个人和哪些人认识的二进制状态,那么初始化 \(dp[a_{[i]}]=1\),这是因为这群人只需要 \(i\) 介绍一下就可以互相认识了。其余的都初始化为 \(\inf\)。
状态转移时,我们先枚举现在的状态,再枚举介绍人,首先介绍人和现在状态中的所有人都是朋友,然后再将他认识的朋友们都介绍进来成为新的状态,也就是 \(dp[i|a[j]]=min(dp[i]+1)\)。
为什么一定要跟状态中的所有人都互相认识?就介绍给几个人不行吗? 注意状态,我们是以一个类似于上帝视角的全局视角看有哪些人互相认识,假设现在所有互相认识的人的集合为 \(A\) 且 \(P\in A\),\(i\) 认识 \(P\) 中的所有人,并想把他的朋友集合 \(B\) 介绍给 \(P\) 认识,显然 \(B\) 和 \(P\) 是不认识的。那么有两种可能,一种是 \(B\) 与 \(CuP\) 的人互相认识,另一种是 \(B\) 和 \(CuP\) 互相不认识。互相不认识的时候显然转移不了,互相认识的时候能转移,但由于最后是要求所有人互相认识的情况,这种情况等效于先将 \(i\) 介绍进来,再将 \(B\) 介绍进来。
不需要把 \(i\) 和他每一个认识的人的状态初始化为 \(0\) 吗? 你说的对,但是状态转移时枚举的是把一个人认识的所有朋友进来,这是其一。其二,如果是这样初始化,打个比方,\(i\) 认识 \(a,b,c\),那么状态转移不好的话很有可能就 \(i\) 先把 \(b\) 介绍给 \(a\),再把 \(c\) 介绍给 \(a,b\),这样答案是错的。
模拟过程的话认识的人不是会变吗?为什么这里的都不变? 假设 \(x\) 新认识了一大堆朋友,但 \(y\) 还是只认识聚会开始前的朋友,那么把 \(x\) 的一大堆朋友介绍给 \(y\) 就相当于把 \(y\) 的集合并入 \(x\) 的集合。又假设 \(x\) 新认识了一大堆朋友,\(y\) 也新认识了一大堆朋友,但是 \(x\) 和 \(y\) 还不认识,那么把他们俩互相介绍的效果等价于把 \(y\) 的集合先拆分为最开始的几个集合,再一个一个合并到 \(x\) 中,操作次数是不变的。
Day 3
Cashback
看到 \(\lfloor\frac{k}{c}\rfloor\) 这个东西,其实就可以感性想一个结论,每次能取 \(c\) 的倍数就取 \(c\) 的倍数。然后我们具体思考每次应该取多大的块长。
假设现在有一个总长度为 \(2c\) 的序列且序列长下面这模样
我们有两种分块情况,方案 \(A\) 是拿两个长度为 \(c\) 的序列,方案 \(B\) 是拿一个长度为 \(2c\) 的序列。若 \(m1<m2<m3\) 那么方案 \(A\) 会使最后的答案减去 \(m1,m3\),方案 \(B\) 会使最后的答案减去 \(m1,m2\),显然方案 \(A\) 更优。若 \(m1<m3<m2\) 那么方案 \(A\) 会使最后的答案减去 \(m1,m3\),方案 \(B\) 会使最后的答案减去 \(m1,m3\)。再分类讨论也没必要了,但我们可以发现,方案 \(A\) 总是不会劣于方案 \(B\) 的。感性理解一下,如果取大的区间减去的一定是区间内前 \(\frac{k}{c}\) 小的数,但如果是取多个小的区间还有可能取到前 \(\frac{k}{c}\) 小后面的数。
最后的结论就是每次都恰好分块长为 \(c\) 的块是最优的。
这个东西就可以用滑动窗口处理。设 \(dp[i]\) 表示到 \(i\) 为止这样分块能减去的最大值,那么 \(dp[i]=max(dp[i-1],dp[i-c]+a[q_{[fro]}])\)。
答案就用总和\(-dp[n]\) 即可。
Discrete Centrifugal Jumps
观察题面发现对于每一个限制 \(i\) 都最多由一个转移过来,同时最多转移到一个去。并且转移和被转移的数只有可能是 \(i\) 右边第一个比它大的,第一个比它小的和在 \(i\) 左边第一个比它大的,第一个比它小的。这个显然可以用单调栈正着扫一遍再反着扫一遍就可以维护了。
注意由什么转移过来和能转移到哪去都要维护。
Non-equal Neighbours
先不管数据范围,瞎设状态。不难设出一个状态 \(dp[i][j]\) 表示填到前 \(i\) 个且第 \(i\) 个填的是 \(j\) 的方案数。那么 \(dp[i][j]=\sum_{k=1}^{a[i-1]} dp[i-1][k]-dp[i-1][j]\),用一个 \(sum[i]\) 表示第 \(i\) 个的总方案数,方程就可以改写成 \(dp[i][j]=sum[i-1]-dp[i-1][j]\)。再滚掉一维就是 \(dp[j]=sum[i-1]-dp[j]\)。
也就是之前的 \(dp[j]\) 乘上一个 \(-1\) 再加上 \(sum[i-1]\) 就是现在这个点的答案了。从总体上考虑,就是 \([1,a[i]]\) 的值都乘上一个 \(-1\),然后再都加上一个 \(sum[i-1]\),接着把后面的都赋值为 \(0\) 就行。这就是一个区间修改区间查询的题了,可以用线段树维护。空间炸了就动态开点,大概是只有 \(\log n\) 左右的点。
\(O(n)\) 的容斥做法太神秘了,我目前还不会/kk。
Day 4
The Hard Work of Paparazzi
这种有时间的不难联想到 瑰丽华尔兹,所以可以设出一个状态 \(dp[i][j][k]\) 表示第 \(k\) 秒在 \((i,j)\) 最多可以拍到几个名人,第三维可以滚掉,但时间滚不掉。所以这道题实际上和瑰丽华尔兹没有什么关系。
考虑名人的出现顺序是按照 \(t\) 从小到大给出的,这提示我们可以根据这个设出状态,\(dp[i]\) 表示到第 \(i\) 个人出现时能拍到的名人数量,那么 \(dp[i]=max(dp[j]+1)\;(j<i)\)。时间复杂度 \(O(n^2)\)。
接着观察数据范围 \(r\le 500\),但 \(n\le 10^5\)。这么小的 \(r\),我们发现对于 \((i,j)\) 这个点,所有点与它的距离都不会超过 \(2r=1000\)。换句话说,从任意一点到 \((i,j)\) 的时间花费不会超过 \(2r\)。
所以我们可以优化这个转移,\(i-2r\) 前的肯定都可以转移过来,只需要枚举 \([i-2r+1,i-1]\) 这一段的能不能转移即可。
本题比较坑的一点就是看到那个 \(dp[i]=max(dp[j]+1)\;(j<i)\),你会很套路的陷入单调性优化这种怪圈。
Divan and Kostomuksha (easy version)
看到这种重排问题,我们可以基本排除 \(dp[i]\) 表示前 \(i\) 个数之类的线性 dp 状态了。观察答案序列的 \(\gcd\) 有哪些性质。
- \(\gcd\)单调不升
- 前一个 \(\gcd\) 是后一个 \(\gcd\) 的倍数
我们发现最后的值仅仅与前缀和 \(\gcd\) 有关,所以考虑设计状态 \(dp[i]\) 表示以 \(i\) 及 \(i\) 的倍数做 \(\gcd\) 的最大值。\(i\) 有多少的贡献又要看最多有多少个数的 \(\gcd=i\),所以设计一个辅助数组 \(cnt[i]\) 表示数组中有多少个数含有 \(i\) 这个因数。于是 \(dp[i]=max(cnt[i]\times i,dp[j]+(cnt[i]-cnt[j])\times i)\)。\(cnt[i]-cnt[j]\) 表示的是有多少个数是 \(i\) 的倍数但不是 \(j\) 的倍数。
枚举因数直接 \(O(n\sqrt(n))\) 枚举,转移是 \(O(v\log v)\) 的。这可以通过简单版本。
考虑进一步优化,我们发现 \(dp[k]\ge dp[l]\;(k|l)\),这是因为 \(l\) 是 \(k\) 的倍数,在转移 \(k\) 的时候也会遍历到 \(l\),根据转移式发现 \(dp[k]\) 至少等于 \(dp[l]\)。因此枚举到一个数 \(k\) 就没有必要枚举它的倍数了,转移时直接转移质数倍即可。
Leftmost Ball
仔细思考一下题意,可以将题意转化为有题意转化成有 \(n\) 个白球,\(n\) 种彩球,每种彩球有 \(k-1\) 个。但这个转化不一定有用,可以先丢在一旁。
接着观察样例发现合法数据需要满足任意时刻彩球种类数要 \(\le\) 白球个数,因此之前那个转化可能是有用的。
试图直接搞出状态,令 \(dp[i][j]\) 表示前 \(i\) 个球中有 \(j\) 个白球,发现无法转移。改一下状态,令 \(dp[i][j]\) 表示前 \(i\) 个球中有 \(j\) 种彩球,此时可以转移,但需要用 \(i-j(k-1)\) 计算出白球个数判断是否合法,所以第一维实际上是没用的,我们将状态改为 \(dp[i][j]\) 表示有 \(i\) 个白球,\(j\) 种彩球的方案数。
如果这一个放白球,\(dp[i][j]=dp[i-1][j]\)。
如果这一个放彩球,那么这一次可以放 \((n-(j-1))\) 种颜色的球。为了防止重复,我们令前面的颜色都连在一起,接下来还有 \(nk-(j-1)(k-1)-i\) 个空可以填。但是由于我们钦定已经放过的颜色都连在一起了,所以第一个这种颜色的球也不能与前面产生空隙,需要插在最左边的空隙中。所以实际上的可填的空只有 \(nk-(j-1)(k-1)-i-1\) 个空。
为什么要一次填一车彩球?不是一个一个吗? 这个有两个原因,首先根据我们的状态设定的是放了 \(j\) 种彩球的方案数,如果一个一个放,我们还需要知道这一种之前已经放了多少个,显然会炸。其次放一个一个呀会重复计算方案,如果先放 \(X\) 再放 \(O\),或者先放 \(O\) 再放 \(X\),\(XO\) 会被计算两遍
为什么填的第一个这个颜色的球要放在第一个空位? 依旧是重复的问题。假设不放在第一个空位,那么 \(XO\) 还是会被计算两遍(感性理解)。
所以状态转移方程就是 \(dp[i][j]=dp[i-1][j]+dp[i][j-1]\times (n-j+1)\times (nk-(j-1)(k-1)-i-1)\)。答案输出 \(dp[n][n]\) 即可。
END