Loading

动态规划记录

我不会大炮但是大家都会,所以打算花一段时间来训练大炮。

下面的题如果是洛谷上的就标上了相应的难度,CF 或者 AT 的就标了评分,颜色代表等级。

如果是阅读理解题面会加上题意简述,引用框内的都是重点(?)然后一般不放代码(嘴巴)以及有可能最后会有总结,以 Summary 开头,以及可能会出言不逊。

最重要是加红的 Hint,是题目中被卡住的部分。

\(\color{green}{\texttt{More vegetable and More vegetable, what should I do?}}\)

CF1566F

\(\color{red}{\bigstar\texttt{ 2600}}\)

给出数轴上若干个点和若干条线段,每次你可以花费 \(1\) 的代价使得一个点向左或者向右移动一个单位,求最小花费使得每条线段都曾包含过一个点。
\(1\le n,m\le 2\times 10^5\),所有坐标在 \([-10^9,10^9]\) 中。

首先可以考虑一波每条线段包含的点一定是左边或者右边离它最近的点。然后暴力做法是枚举每条边对应的点然后求和。可能区间 \(dp\) 吗。不太会设计状态,看 \(hint\)

\(\color{red}{\texttt{Hint1:}}\) 如果一条线段已经包含点了,那么这条线段可以被丢掉。
\(\color{red}{\texttt{Hint2:}}\) 如果一条线段被另一条线段包含,可以只考虑小的那一条。

废话。
但是废话不是没有用,这样一来,数轴上的情况就变成了:若干个点然后若干条线段然后再是若干个点,以此类推,并且每一堆线段按左端点排序后,右端点也是有序的。

这样就有了一个初级的算法,就是对于每一堆线段,预处理出左边的点标记一段前缀的花费和右边的点标记一段后缀的花费,然后扫一遍求最小值。现在的问题是怎么求两堆线段中只有一个点的情况。(如果有多个点可以看成两个点中间有 \(0\) 条线段)

然后考虑这个点的轨迹是什么。想来必然是向左一段然后拐过来向右对吧。

太菜了,还是看了题解。

\(dp_{i,0/1}\) 表示第 \(i\) 个点先向左(右)走的最小代价。
那转移就是:

\[\begin{aligned} dp_{i,0}&=\min_k(dp_{i-1,0}+S_{k}.l-p_{i-1}+2\times(p_i-S_{k+1}.r)) \\&\min_k(dp_{i-1,1}+2\times(S_{k}.l-p_{i-1})+2\times(p_i-S_{k+1}.r)) \end{aligned}\]

\[\begin{aligned} dp_{i,1}&=\min_k(dp_{i-1,0}+S_{k}.l-p_{i-1}+p_i-S_{k+1}.r) \\&\min_k(dp_{i-1,1}+2\times(S_{k}.l-p_{i-1})+p_i-S_{k+1}.r) \end{aligned}\]

其中 \(S\) 表示夹在这两个点中间的线段。

最后注意特判一下开头一坨线段左边有没有点,以及最后一坨线段右边有没有点。

写完了但是 WA on 12。。。

哦删边写挂了,改改就过了。

record

Summary:这题主要是设计状态出问题。以及有一个套路就是说其实当前这一位的贡献可以拆开到下一位去算。遇到这种线性的 dp 然后发现要两边兼顾的时候可以考虑拆贡献。

CF1129C

\(\color{red}{\bigstar\texttt{ 2400}}\)

若任意一个长度在 \([1,4]\) 中的 \(01\) 串(除 \(0011,0101,1110,1111\) 外)都唯一代表一个字母,那么求按顺序在空串 \(s\) 中逐个加入字符的时候,当前的所有子串能表示的不同字符串的个数。

容易想到每次考虑后缀多出来的答案。然后考虑到状压的话最多也就 \(30\) 个状态。那就不妨令 \(dp_{i,j,k}\) 表示到第 \(i\) 位时所有的后缀中以长度为 \(j\) 的二进制表示 \(k\)\(01\) 串结尾。然后加进去一个字符,枚举上一个的状态,以及当前字符是并入前一个的结尾还是另开一个,直接计数就行了。由于感觉转移需要巨大多 if,这里不写了,然后 \(dp\) 第一维是可以压掉的。

但是在样例上挂了。???咕咕咕

发现 \(dp\) 转移写挂了,改了之后还是过不了,稍微分析一下,发现算重了。于是考虑性质,两个不同的 \(01\) 串代表的字母串一定不同,所以只要不同就不会算重。那我们就设 \(dp_{i,j}\) 为前 \(i\) 个字符长为 \(j\) 的后缀构成的 \(01\) 串表示不同字母串的个数。然后每次先更新 \(dp\),然后把前面没有出现过的后缀加到答案里。考虑用一个 \(hash\) 去重。

悲!unordered_map 被卡了。于是换用 \(SAM\),利用其后缀树的性质,直接得到 \(lcp\),所以从新建节点的父亲的长度开始记入答案,复杂度 \(O(m^2)\)

过了!

record

Summary:这题 dp 倒是自己推的,然后主要是最后去重的时候没想到用 SAM 来优化。以后字符串题可以多想想 SAM。

P2396

\(\color{purple}{\texttt{省选/NOI-}}\)

看到厄运数字只有 \(2\) 个,非常开心,很容易想到正难则反。于是我们考虑一个到达厄运数字的方案数。然后容斥一下就完了。暴力枚举的复杂度是 \(O(2^n)\),好像很卡。然后主要是找有多少方案是既可以到达第一个厄运数字,也可以到达第二个厄运数字。

考虑 \(f_i\) 表示用 \(i\) 的子集中的数到达第一个厄运数字的方案数,\(g_i\) 表示用 \(i\) 的子集中的数到达第二个厄运数字的方案数。烦了,看题解了~

???是对的,不过题解没有反过来做,直接正着做,然后令 \(dp_i\) 表示用 \(i\) 的子集中的数赢的方案数。然后预处理一个 \(d_i\) 表示用上 \(i\) 中的所有数字,所构成的数字。然后每次从少一个数的地方转移过来,如果少一个数形成了厄运数字或者当前就是厄运数字就不转移。

P4124

\(\color{purple}{\texttt{省选/NOI-}}\)

简单数位 dp 题?

记录前导 \(0\),是否顶最高位(套路),有没有出现 \(8\)\(4\),以及出现的连续的数字和出现了几次在加上第几位,总共是 \(7\) 维的 dp:)

哦为了好写一点,考虑把连续 \(3\) 个的限制用记录前一个和前一个的前一个是什么,以及最终是否 ok 来记录。

尼玛**,hack:

10000000000 10000000000

沙比,不用考虑前导 \(0\),但是要枚举第一位,去掉第一位前导 \(0\) 的情况。

P2150

\(\color{purple}{\texttt{省选/NOI-}}\)

终于还是来了。

给定一个 \(n\),表示有 \(2\sim n\) 的数,然后两个人都从这些数中选一些数组成两个集合 \(A,B\),求有多少种方案使得集合 \(A\) 中的任意一个元素与 \(B\) 中的任意一个元素都互质。
\(2\le n\le 500\)

感性地说,就是 \(A\) 选了某个数的倍数,那 \(B\) 就不能选这个数的其它任何一个倍数了。这个“某个数”可以看成是质数,这样就是对每个质数打个标记。暴力呢就枚举 \(A\) 的选法,然后像筛一样筛一下 \(B\) 的选法,直接累加。

然后得到启发就是枚举哪些质数是属于 \(A\) 的,然后直接……好像复杂度也差不了多少。

\(\color{red}{\texttt{Hint1:}}\) 一个小于等于 \(500\) 的数最多只有 \(1\) 个大于 \(22\) 的因子。

哦对哦,\(\sqrt{500}=22\)。。。

考虑前面得到的做法,可以用状压来写,就是令 \(dp[a][b]\) 表示 \(A\) 中选的质因子集合为 \(a\)\(B\) 中选的质因子集合为 \(b\) 的方案数。

然后发现这样的 dp 在 \(500\) 的情况下是开不下的,于是我们考虑运用 \(\color{red}{\texttt{Hint1}}\) 的内容。假如说,所有质因子都是小于 \(22\) 的,那么状压是可以开下的,否则,只要记录一下这个大于 \(22\) 的质因子放在哪边,是 \(A\) 还是 \(B\),然后直接转移就可以了。

关于具体实现还是看了题解。

首先对数排序,按照最大因子排。这样最大因子相同的就在一起了~

然后你搞一个 \(f_1[i][j][k]\) 表示到第 \(i\) 个数了,然后最大的质因子是给 \(A\) 集合的并且 \(A\) 中还有质因子集合为 \(j\)\(B\) 中质因子集合为 \(k\)。同样定义一个 \(f_2[i][j][k]\),然后第一维可以滚动,然后就是你最后用上面那个 \(dp[][]\) 来合并这两个数组,就是 \(dp[i][j]=f_1[i][j]+f_2[i][j]-dp[i][j]\),就是要减去两边都不选这个最大因子的情况。这个东西一段一段做就可以了。

Summary:这题的难点应该就是想到这个 Hint。其实之前也做过不少这样的题,但是这样的 trick 真的不太好想,以后如果一道题的暴力看起来比较有前途,我们可以考虑对更大的数据它的瓶颈在哪里,比如说这题,瓶颈就是因子太多了,然后就会去想因子能不能少一点呢?发现说大因子最多只有 \(1\) 个,然后小小分讨一下就可以了。
以及这题还有一个值得注意的点就是定义 \(f_1\)\(f_2\),这样的做法使得实现简单了不少,那为什么会想到呢?因为一段中的数显然只能归给一个集合,然后如果混在一起就非常麻烦,于是就把 dp 拆开,保证两边互不影响,然后在合在一起。

P2481

\(\color{purple}{\texttt{省选/NOI-}}\)

给出一个 \(n\),求有多少个 \(n\) 位的十进制数能被 \(p\) 整除,并且每一位从左往右递增。\(1\le n\le 10^{18},1\le p\le 500\)

又一题数位 dp 板子么(((

考虑记录当前模 \(p\) 的余数和上一位,然后……做完了。

?好像看错题了,是 \(10^{18}\) 位!这样复杂度显然是不能接受的。

那显然是要把 \(n\)\(\log\) 一下。于是我们考虑一个分治。这个分治也很简单,我们考虑记录 \(i\) 位的第一位是 \(j\) 最后一位是 \(k\) 的合法数量。所以不需要像套路的记忆化,直接分治就可以了?哦还有是 \(p\) 的倍数的情况。所以还是记忆化一下左边是 \(l\),右边以 \(r\) 结尾并且整个的模 \(p\) 余数是 \(rem\) 和长度为 \(i\) 的方案数就好了。搞一个 map 存一下,由于是分治,不会超过 \(\log\) 个,应该可以吧……

无奈之下看了题解,发现自己的想法和题解完全不一样。自己还是 naive 了啊。

\(\color{red}{\texttt{Hint:}}\) 每一位顺次递增相当于拆成若干个每一位都是 \(1\) 的数相加。

这个转化太神必了。(谁想得到啊

这样就变成了求有多少种每一位都是 \(1\) 的数相加,是 \(p\) 的倍数。然后就是全部由 \(1\) 构成的数长度增加的时候模 \(p\) 的余数有周期(鸽巢原理)。

然后我们记一个 \(cnt_i\) 表示模 \(p\)\(i\) 的全为 \(1\) 的数(长度不超过 \(n\))有多少个。这东西可以预处理。然后开始大炮。

\(dp[i][j][k]\) 表示考虑模 \(p\)\(i\) 的全 \(1\) 数,此前已经放过 \(j\) 个全 \(1\) 数,此前模 \(p\) 余数为 \(k\) 的方案数。然后进行转移。注意我们最后为了保证 \(n\) 位,需要加上 \(n\) 位的全 \(1\) 数。所以 \(j\) 上限是 \(8\),最终答案是 \(dp[p-1][0\sim 8][0]\),以及最初 \(dp[0][0][NP]=1\),其中 \(NP\)\(n\) 位全 \(1\) 的数。

然后转移就是

\[dp[i][j][k]=\sum_{l=0}^jdp[i-1][j-l][(k-i\times l)\%p]\times\dbinom{cnt_i+l-1}{l} \]

关于上面那个组合数:是从 \(cnt_i\) 个元素中可重复地选出 \(l\) 个。我们可以掏出 \(l\) 个小球,然后用隔板分成 \(cnt_i\) 段,每段就表示当前元素选多少个。然后由于可以空,就放进 \(cnt_i\) 个球,所以就是 \(\binom{cnt_i+l-1}{cnt_i-1}=\binom{cnt_i+l-1}{l}\)。这样 \(l\) 是很小的,所以直接算就可以了。以及——

999911659 是质数!!!!OHHHHHHHHHHHHH

我无敌了~

调了一个下午。。。(用滚动可以避免 \(0\) 减到负数的情况)

Summary: 最重要的就是不要陷入思维定区,之前是被数位 dp 框定住了,事实上完全是另一种思路。
调题的过程中也发现了在草稿本上画数轴标下标是很有效的解决边界细节的手段。
dp 转移可以用刷板法有的时候会更方便一点,以及滚动可以避免负下标的情况等等。

CF149D

\(\color{grey}{\texttt{这题 CF 上没有评分}}\)

括号序,经过 CSP 的洗礼之后,这就是一个区间大炮拍脸上了。这个还是比较简单的(?)令 \(dp[l][r][lc][rc]\) 表示区间 \([l,r]\) 左端点染 \(lc\) 有端点染 \(rc\) 的方案数。

然后如果左右两边是配对的,那就同时往里边缩,不然就找到与左边配对的点分开,然后枚举中间相邻两个的颜色,并对应递归做下去,记忆化一下就可以了。(?)

吓死了,小地方手滑了。。。

UVA12991

\(\color{RoyalBlue}{\texttt{提高+/省选-}}\)

感觉一直不太会这种距离和最小之类的东西。先嘴巴一下?令 \(dp[l][r]\) 表示区间 \([l,r]\) 内的最小距离和。。。但是,完全不会。。。

那瞄一眼题解。。。

\(\color{red}{\texttt{Hint:}}\) 可以把整栋大楼的状态看成是一段乒乓一段游泳然后再一段乒乓……

然后我们考虑设 \(dp[i][j]\) 为考虑到第 \(i\) 层楼,并且最后一段是泳池(\(j=1\))或者是乒乓(\(j=0\))的最小距离。

这样的话,我们枚举一下从哪里转移过来,设为 \(k\),那么有:

\[dp[i][j]=\min_kdp[k][j\operatorname{xor}1]+psum[j\operatorname{xor}1][k+1][mid]+ssum[j\operatorname{xor}1][mid+1][i] \]

其中 \(psum[i][j][k]\) 表示区间 \([j,k]\) 中喜欢游泳的(\(i=1\))或者乒乓的(\(i=0\))跑到 \(j\) 的前一位的距离和。同理定义 \(ssum[i][j][k]\) 表示都跑到 \(k\) 的后一位。然后这个东西可以预处理。用一个二维数组存一下就可以了。

……这都能写挂……

P4059

\(\color{RoyalBlue}{\texttt{提高+/省选-}}\)

给出两个字符串(由 \(\texttt{A,T,G,C}\) 组成),现要求在里面加入空格使得它们等长,并且要求相似度最大。相似度由两部分构成:

  1. 如果在某个位置上两个串的字符都不是空格,那么相似度加上 \(d(x,y)\)
  2. 对于两个字符串的任意一段极长的长度为 \(k\) 的连续空格,相似度减去 \(A+B(k-1)\)

什么东西,停课了还一天到晚学生物

又是这种诡异的题,关键是长度好像还不确定。但是不要紧,你考虑一个 \(dp[i][j][k_1][k_2]\) 第一个串加到了 \(i\),第二个串加到了 \(j\),并且第一个串结尾是不是空格,第二个串结尾是不是空格。然后转移的时候不能两个串都是空格,因为这样必然不优,然后暴力转移就可以了。

不对啊,这样不能保证最后长度相同?看题解了。

什么鬼啊,题解 EI 的博客更奇怪(其实是一样的,把我的状态后两位压起来了),这是怎么保证最后长度相同的呢(((

哦事实上是转移的时候就保证是两位同时减一来得到上一个状态的,不过有的时候是减 \(0\) 从而前两个下标不减。。。

还是用题解的方法方便。/xyx

P1941

\(\color{LimeGreen}{\texttt{普及+/提高}}\)

这题大家都做过了吧……

看了看数据范围,大概是 \(O(nm)\) 的大炮。

先来点暴力想法,令 \(dp[i][j]\) 表示到达 \((i,j)\) 的最小点击数。

\(\texttt{A}\color{red}{\texttt{xDea}}\) 说可能可以过,那先写写看~

居然有 90 分,不要急,我们考虑优化这个东西。

\(\texttt{A}\color{red}{\texttt{xDea}}\) 说可以把向上看成是先向右然后再向上,然后再从下往上刷一遍就可以了。

非常高妙啊~

Summary: 这题最高明的地方就是像背包一样先从前一个状态转移过来,然后从下到上刷一遍,这样可以使状态合在一起更新。以后碰到最大最小的问题,可以考虑用这个优化 dp 复杂度。

P3354

\(\color{purple}{\texttt{省选/NOI-}}\)

给出一棵树,要求在其中选出 \(k\) 个节点,使得每个节点到它的最近的被标记的祖先的距离乘上它的点权之和最小(如果没有就到根以上)。求这个最小权值和。
\(2\le n\le 100\)\(1\le k\le \min(n,50)\)

看到数据范围非常小,考虑大力跑 \(dp\)。我们令 \(dp[i][j]\) 表示在以 \(i\) 为根的子树中放 \(j\) 个标记的最小权值。这样我们就考虑当前的根选不选,然后以此来更新当前的 \(dp\)。但是这样感觉不好更新,因为如果选了,那么前面的权值都不能延续下来。

那我们就设,\(dp[i][j][k]\) 表示在以 \(i\) 为根的子树中,进行了 \(j\) 个点的标记,然后……还是 G 了。看题解。

这题状态是确实难设:令 \(dp[i][j][k]\) 表示节点 \(i\) 的祖先中最近的被标记的点是 \(j\) 并且 \(i\) 及子树中有 \(k\) 个被标记的点的最小权值。这样的话,我们在大法师里转移的时候用栈记录其祖先。然后转移就可以了。

具体地,我们枚举它儿子中标记的点的个数。这样一来,我们需要一个数组 \(g[i][j][k]\) 表示强制选当前节点,并且子树中选了 \(k\) 个,祖先中最近的标记点是 \(j\) 的最小代价。

Summary: 遇到奇怪的 dp,你的状态如果不能很好地转移的话,那就考虑缺少什么东西,一般来说,需要加上能计算当前节点贡献的信息,比如说本题的第二维,就是用来计算当前根节点的答案的。

P3478

\(\color{LimeGreen}{\texttt{普及+/提高}}\)

经典换根了,记录 \(dp[i]\) 表示以 \(i\) 为根的子树中的深度之和,\(siz[i]\) 表示以 \(i\) 为根的子树大小,以及 \(up[i]\) 表示不在 \(i\) 子树内的所有节点到 \(i\) 的距离之和,这样的话转移就是:

\[up[x]=(up[fa]+n-siz[fa])+(dp[fa]-dp[x]-siz[x]+siz[fa]-siz[x]) \]

诶哟没开 long long 寄了一发。

P3047

\(\color{RoyalBlue}{\texttt{提高+/省选-}}\)

easy!令 \(dp[i][k]\) 表示以 \(i\) 为根的子树中与 \(i\) 距离为 \(k\) 的点权和。然后考虑换根,让 \(up[i][k]\) 表示不在 \(i\) 为根的子树中的与 \(i\) 距离为 \(k\) 的点权和。

直接转移就可以了,转移比上面那题还好想一点。

CF708C

\(\color{grey}{\texttt{这题 CF 上没有评分}}\)

据说以前是有的,并且据说我以前在模拟赛场切过??

首先考虑对每个点都要判断过去,所以需要一个换根。然后考虑对于一个根节点,它的子树在什么情况下是可以的。

首先,一个节点不可能有超过 \(1\) 个大小超过 \(\dfrac{n}{2}\) 的子树,所以我们只要对那个超过 \(\dfrac{n}{2}\) 的子树进行考虑就可以了。然后我们有一次机会把这个子树断成两个,使得最终这两个子树的大小都不超过 \(\dfrac{n}{2}\)

考虑怎么断比较牛逼。你显然知道断一个点出去显然不优。你显然知道如果断得只剩下一个点肯定不优。于是你考虑断那条边能使得得到的两个联通块之差最小。所以如果只是求一个根可不可以,我们需要求出其最大的子树,设根为 \(x\),然后在里面找到一个 \(s\),使得 \(|2siz[s]-siz[x]|\) 最小。这样最优策略就是找到这个不合法的子树中小于等于 \(\dfrac{n}{2}\) 的最大子树。所以我们考虑一个 \(dp[x]\) 表示以 \(x\) 为根的子树中满足小于等于 \(\dfrac{n}{2}\) 的最大子树。所以:

我们记录 \(dp[i]\) 表示 \(i\) 子树中的满足小于等于 \(\dfrac{n}{2}\) 的最大子树,\(up[i]\) 表示除了 \(i\) 子树中的,满足沿 \(i\) 向外且大小小于等于 \(\dfrac{n}{2}\) 的最大子树大小,\(siz[i]\) 表示子树大小。然后记录每个节点的最大和次大儿子(\(dp\)),然后直接跑裸的换根就可以了。

P6419

\(\color{purple}{\texttt{省选/NOI-}}\)

给一棵树,树上有若干个点是被标记的,现在从起点 \(i\) 开始,按任意顺序遍历所有被标记的点(不用回起点),对于每个 \(i\in[1,n]\),求路程之和最小。

感觉比较诡异啊,首先有一个贪心就是,肯定是把离当前节点最远的点留到最后。这样的话,令 \(dp_{i,0/1}\) 表示在确定根的情况下,遍历完子树中的所有点然后回不回到根的最短长度和。然后我们就可以求出在固定根的情况下的答案了。接下来我们考虑如何在换根的时候转移这个信息。容易想到,我们在跑的同时记录一下每个节点向上遍历所有的标记点的最小距离,以及回不回到这个节点,这样我们可以直接转移就好了。

哦还有就是你要记录一个点子树中有没有标记的点,否则就不用更新它的 \(dp\) 值。这个东西记录一下子树中有多少个被标记的点就可以了。

发现写起来有点恶心,所以看了眼题解,题解是直接套路维护最长和次长链。额确实哦,这样就不用维护不回到当前节点的信息了,就是回到当前节点再减去最长链的长度就可以了。

Summary: 维护这种树上换根之类的东西的时候,通常不止一种理解方式,换换角度来思考往往能使得代码更好写。

P3647

\(\color{purple}{\texttt{省选/NOI-}}\)

给定一种构造一棵双色边的树的方式:每次向树种加入一个点,然后连边为红色或者向一条红色边中插入一个点,两边都变成蓝色。给出树的最终形态,求蓝色边长度和最大是多少。

假如说我们已经确定了起初的时候那个点是那个点,假设它为根,那么蓝边肯定是形如 \(fa\to x\to son\) 的样子,也就是深度递增的由两条边构成的链。于是这个东西可以 \(dp\)。我们令 \(dp_{x,0/1}\) 表示 \(x\) 节点是不是这个两条蓝边的中点。那么有转移:

\[dp_{x,0}=\max_i\{\max(dp_{s_i,0},dp_{s_i,1}+w_i)\} \]

\[dp_{x,1}=sum_x-\max_i\{\max(dp_{s_i,0},dp_{s_i,1}+w_i)+dp_{s_i,0}+w_i\} \]

其中 \(sum_x=\sum\max(dp_{s,0},dp_{s,1}+w)\)

那我们考虑换根。记 \(up_{x,0/1}\) 表示沿 \(x\) 子树向外的所有点中,\(x\) 不是/是两条蓝线的中点的最大蓝色边权和。考虑更新它需要什么信息。

好的,手推出来 \(16\) 种分类。还要记录各种次大值。打算咕掉。

P4381

\(\color{purple}{\texttt{省选/NOI-}}\)

给一棵基环树森林,求直径和。

我也不清楚为什么是这个题意……不知道是我语文有问题还是翻译的人有问题……

那反正知道题意之后就好办了。我们对每棵基环树都跑一次直径,具体地,我们对于环上的每个点,求出子树中的最长链,然后相当于给一个环,求环上两个点,权值加上他们之间的距离最大是多少。这东西用单调队列瞎几把优化一下就可以了。

嗯……好难写啊,不过可以有简单的写法,就是如果是有向图构成的基环树一定是内向的,环上的点一定能转一圈,所以只要建有向图就可以了。

写完了。

P3233

\(\color{purple}{\texttt{省选/NOI-}}\)

每次询问给你一些关键点,然后每个节点是属于离它最近的关键点中编号最小的那个。求每个关键点包含几个点。

看起来是一道非常牛逼的虚树题。虚树套路就是先看暴力 \(dp\) 怎么写。暴力是个换根 \(dp\),首先可以求出每个点到它子树中最近的关键点。然后换根一下就知道每个节点到它最近的关键点了,然后统计一遍就可以了。

然后考虑缩略到只剩关键点之后怎么办。这样两点之间的信息应该是它们之间的路径长度。但是你发现此时好像并没有办法那么方便地进行 \(dp\)。事实上并不困难。首先我们可以求出虚树中每个点属于哪个关键点。然后考虑对边进行分讨。如果边的两端都属于同一个关键点,那么这条边上的点一定也属于那个关键点。否则,我们可以通过倍增求出边上那个点是分界线,然后两头分别加贡献就可以了。这样复杂度是每次 \(O(m\log n)\),就可以过了。

发现自己忽略了好多点的贡献,然后这样就需要大力重构了。。。于是我不争气地打开了题解,发现其实也差不多,感觉唯一可以借鉴的是 \(\texttt{C}\color{red}{\texttt{3H5ClO}}\) 大佬的做法,感觉非常不一样。

他把这个属于的过程变成一个自上往下的染色的过程,这样每次就是之前的颜色个数减去当前染的,然后当前的颜色个数加上当前染的就可以了。然后还有一点就是我的换根写烦了,由于这题距离的特性,不需要记录次大值……所以 \(\texttt{C}\color{red}{\texttt{3H5ClO}}\) 用寥寥几行解决了换根。然后他在做后面的事情的时候,用染色的思想更好写了,对于每条边,二分或者倍增一下到哪里为止是和下面那个节点同一个归属的,然后直接搞答案就好了。。。

My Solution

UVA10559

\(\color{purple}{\texttt{省选/NOI-}}\)

一道咕了很久的区间 dp。dp 都是这样,你信息不够就加维。发现这题 \(dp[l][r]\) 显然是不够的。于是——

\(dp[l][r][len]\) 表示区间 \([l,r]\) 消除时右端点的右边有 \(len\) 个与右端点同色的块与区间右端点一起消除的最大得分。这样最后的结果就是 \(dp[1][n][0]\)。然后考虑怎么转移。

完蛋真的不会转移了。这题的转移是和状态一样考虑的,就是考虑末端的那一坨同色的块是否保留。

Summary: 不会设状态就加维,不会转移就手模,但都是废话。不行就试试感性理解和假贪心。

UVA1628

\(\color{purple}{\texttt{省选/NOI-}}\)

你给 \(n\) 个人送外卖,在第 \(t\) 时刻送到第 \(i\) 个人会获得 \(\max(0,e_i-t)\) 的收益,求最大收益。你从位置 \(0\) 出发。

很显然是区间 \(dp\)。和关路灯那题有点像,考虑设计状态为 \(dp[l][r][0/1]\) 表示给区间 \([l,r]\) 内的人送外卖,并最终停在最左边/最右边的最大收益。但是我们发现这题有个不一样的地方就是会对 \(0\)\(\max\),也就是对每个人送餐必然不亏。那就有问题了,我们似乎还需要一维状态来记录当前的时间才能很好地算贡献了。看了题解,果然,直接加一维表示 时间?必不可能,时间最大那么大,开不下吧! 接下来需要给多少人送餐。为什么这就够了?我们考虑一个贪心,我们放弃一些人不给它们送餐必然是在两端的,如果在中间蹦出来一个人说我不给他送餐是不是神经质,反正是顺路的啊。所以知道最终要给多少人送餐,就知道当前的预贡献,可以和关路灯一样处理了。

每次暴力枚举给那个人送餐。当然 \(dp\) 的意义也稍有变动,表示 \([l,r]\) 区间内送完之后还要给 \(cnt\) 个人送餐,并且当前停在左/右边。

Summary: 这种题做了好多次了。像这种在序列上跑来跑去的题,一般就是记录区间和在这个区间搞完之后是在左端点还是右端点。如果不够算答案就加维。然后这个答案的算法就是预算,比如这题,就是每次预算后面的点的罚时,然后当前只要算当前的答案就可以了。

P1156

\(\color{RoyalBlue}{\texttt{提高+/省选-}}\)

你有 \(n\) 个垃圾,在 \(t_i\) 的时候落下,可以用它来延长时限或者增加高度,如果高度大于等于给定的高度,就得救了,如果时限小于当前时间,就寄了。求会不会寄,如果会寄,求什么时候寄。

看起来就是很大炮很背包的题。但是感觉相关的值有点多。先来一波大胆设状态:\(dp[i][h]\) 表示到第 \(i\) 个垃圾堆的高度是 \(h\) 的最大剩余时间是多少。然后我们考虑转移。那当前的垃圾可以堆上去也可以用来延长时间。那我们就有:

\[dp[i][h]=\max\{dp[i-1][h-H_i]-T_{i}+T_{i-1},dp[i-1][h]-T_{i}+T_{i-1}+F_i\} \]

然后每次你在 \(dp[i]\) 判断一下大于等于 \(D\) 的是否大于 \(0\)。然后如果没有,就重新算一遍,就摆烂,每个垃圾都吃掉,算最大存活时间。

然后转移的时候要注意,如果在当前时刻之前剩余时间就小于等于 \(0\) 了,那就不能转移。以及你注意到每次算完之后 \(dp\) 应当向下刷一次最大值,因为如果 \(dp[i][h]\) 有最多剩余 \(t\) 的时间,那么 \(dp[i][h'](h'<h)\) 应当至少有 \(t\) 的剩余。

CF730J

第一问直接暴力贪心是对的。第二问的话相当于希望最小化选定 \(k\) 个瓶子后瓶子容量大于总的水量的前提下,原先在这些瓶子里的水总量最多。那我们设 \(dp[i][j]\) 为选了 \(i\) 个瓶子,当前总容量为 \(j\) 的最大原水总量。

posted @ 2022-04-07 22:09  ZCETHAN  阅读(40)  评论(0编辑  收藏  举报