DP 状态设计专训

会挑一些题来记录,会有一些锐评。

8.10

A.麻将

首先很明显的考虑是,从 \(1\)\(K\) 来考虑每一个数填放多少,可以设计状态为 \((i,j,k,0/1)\) 表示考虑到第 \(i\) 个数,目前从 \(i-2\) 开始的面子有 \(j\) 个,从 \(i-1\) 开始的面子有 \(k\) 个,目前是否有对子。

显然可以有形如这样的转移:

\[(i+1,k,k',0) \leftarrow (i,j,k,0) \\ (i+1,k,k',1) \leftarrow (i,j,k,0) \]

其中应该满足 \(k'+ \Delta \le 4\)

但是,对于这个合法的胡牌,会计算重复:\(1,1,2,2,3,3,4,4\),原因在于对子的划分会有很多种。

这个东西是一个经典的 Trick,考虑将原状态看作一个 \(\text{NFA}\),而我们能够不重不漏的去统计的状态机是 \(\text{DFA}\)。在 \(\text{NFA}\)\(\text{DFA}\) 可以使用类似于状压的思路:我们存储 \(\text{NFA}\) 中目前路径上所有能够到达的点的集合,这样可以保证我们的合法路径一定在新的自动机上的路径上唯一。由于这道题目对于最终状态有限制,且状态较为有限(\(\text{NFA}\) 状态只有 \(18\) 个左右),所以可以进行如此转化,最终的 \(\text{DFA}\) 状态约有 \(140\) 个,完全是可以通过的。

B.Code With No Forces

这个题目很好想吧,考虑发现我们最主要的约束是时间和空间,而其他是不重要的。考虑设置 \(5\) 个状态,前 \(4\) 个为是否匹配时间和空间,最后一个看成失配状态表示已经匹配或者已经错误。

由于 \(m\) 很小,于是可以 \(5^m\) 进行一个状压,就做完了。

C.Netrpeljivost

这个题目也比较神奇吧,主要的观察在于找到分界点两侧能够取的值。

很明显需要从左往右确定这个东西,我们可以考虑已经确定了第 \(i\) 个数是多少后,来考虑第 \(i + 1\) 个数能够取到什么值?考虑到一个分界点 \(x\)\(x\) 两侧的点一定来自两个相邻且不同的,大小为 \(\operatorname{lowbit}(x)\) 的块,那么这样就非常简单了,设 \((i,j)\) 表示目前是 \(i\) 这个分界点,然后最后一个数填的是 \(j\) 的方案数,转移是简单的。

8.11

A.广播站

很板的 \(dp\) 吧,考虑设 \(dp_{i,j}\) 表示目前在考虑 \(i\) 的子树,然后多出来了(就是可以从 \(i\) 向上覆盖)\(j\) 的方案数,注意此时的 \(j\) 可以取负数,表示目前子树还没有被覆盖完,转移是朴素的,我们可以直接合并两个子树,并且将 \(j\) 加起来,这样就是转移。

B.RGB Sequence

伸臂题目,真正的状态定义神题。

可以定义如下状态:\((i,j,k)\) 表示在 \(i\) 往前第一个和 \(a_i\) 不同的下标为 \(j\),再往前的一个和 \(a_i,a_j\) 颜色不同的下标为 \(a_k\),显然很好转移,这道题目的约束实际上是在转移过程中设置了一些非法的状态,我们直接根据闭区间的约束设置非法的状态为 \(0\) 就行了。

8.13

A.Phryctoria

和题解不太一样的做法,其实很自然。

\(dp_{i,j}\) 表示缩写现在已经能够匹配到 \(i\),然后现在 \(T\) 同步被匹配到 \(j\) 的最短缩写长度,转移是好想的,考虑预处理 \(trans_{i,l,r}\) 表示从 \(i\) 往后,能够匹配到 \(s[l,r]\) 的最近的位置。

显然转移为 \(dp_{i,j} \rightarrow dp_{k, trans_{j, i + 1, k}} + (k - i)\),答案为 \(dp_{n,m+1}\)

B.Random Dungeon

概率神题

目前我的水平只支持我感性理解做法。首先考虑发现你停下来的时候一定是你抽到了前 \(k\) 大的东西,或者说,我们可以设置一个阈值 \(B\),当某一次抽出来的 \(x\) 大于 \(B\) 的时候就停下,否则就继续。显然这道题目就要在这里做文章。设 \(dp_i\) 表示考虑前 \(i\) 大的 \(a\) 的时候的最优的情况时的期望,按照刚刚的结论。

显然有转移:\(dp_i = -C + \frac{1}{i} \sum\limits_{j=1}^{i} \max\{a_j, dp_{i-1}\}\)

意思是你考虑要么在 \(a_j\) 处停下,取到 \(a_j\),要么继续抽,这样的方案应该就是 \(dp_{i-1}\)。为什么是 \(dp_{i-1}\) 呢?

考虑我们的阈值 \(B\),我们将 \(a\) 中的数分成两类,一类是大于 \(B\) 的值,这些东西对答案的贡献只会是 \(a_i\),而剩下的东西只会贡献 \(-C\),既然你并不会取到 \(a_i\),那么我们其他的值取到的是 \(a_i\) 还是 \(a_{i-1}\) 这样是不重要的。然后答案应该就是 \(dp_n\)

C.Zacuscă

很简单的题目,考虑状压,设 \(dp_{msk}\) 表示目前已经取用了 \(msk\) 的最小代价,当我们加入一个数 \(x\),对整体的新贡献为 \(cst_{msk, x}\),显然可以 \(O(2^n \times n)\) 预处理。然后这样就能通过了?这个时候你的空间复杂度为 \(O(2^n\times n)\),也就是说需要优化。

有个 Trick :可以将 \(msk\) 按照前 \(6\) 位和后 \(6\) 位分开,这样时间复杂度不变,空间复杂度大大优化,可以通过。

D.Matrix Fraud

首先发现转置不会影响原来的要求。

考虑设 \(dp_{i, l, r}\) 表示在第 \(i\) 行,第 \(i\) 行的填 \(1\) 的区间为 \([l,r]\) 的最小代价。

显然转移为:\(dp_{i,l,r} = \min\limits_{l'\le l \And r'\le r} dp_{i-1,l',r'} + cst(l,r)\)

但是这样的时间复杂度是 \(O(nm^2)\)。由于转置对答案是没有影响的,所以可以将时间复杂度变成 \(O(nm\times \min(n,m))=O(nm\sqrt{nm})\),可以通过。

E.Random Walk to Millionaire

看到平方的期望,考虑降维。设状态 \((u, a, b, c)\) 表示 \(P(X)=a, E(X)=b, E(X^2)=c\) 且当前在 \(u\)

考虑一条边 \(u\rightarrow v\),分情况讨论:

  • \(C_v=0\):则转移为 \((v, a_v, b_v, c_v) \leftarrow (v, a_v, b_v, c_v) + \frac{1}{deg_u}(a_u, a_u + b_u, a_u + 2b_u+c_u^2)\) 这里的加法是对位加法。

  • \(C_v=1\):则转移为 \((v, a_v, b_v, c_v) \leftarrow (v, a_v, b_v, c_v) + \frac{1}{deg_u}(a_u, b_u, c_u)\)

然后转移即可。

F.Team Building

朴素的考虑是,设 \(dp_{i,msk,j}\) 表示考虑到了第 \(i\) 个人,目前队员选择了 \(msk\) 这些位置,然后目前选择了 \(j\) 个观众的最大n总和。这样的时间复杂度为 \(O(2^pkn)\) 显然无法通过。

考虑优化,可以预先将 \(a_i\) 从大到小排序,如果我们不选择一个人作为队员,而且观众没有满,那么我们必定选择其为观众,这样就能通过了。

8.15

A.[NOIP2021] 数列

很简单的题目。

显然从小到大考虑加入数,但是需要考虑进位,所以需要新开一维。设 \(dp_{i,j,k,l}\) 表示目前考虑到 \(i\) 这个数,然后目前已经填入了 \(j\) 个数,目前进上来了 \(k\) 个,目前前面有 \(l\)\(1\) 的方案数。

转移很好写。

B.Funny Card Game

遇到众数题目的基本套路,钦定众数,若不为真正的众数,则转移不优,考虑这道题目能否这样做?

\(f_{i,j}\) 表示考虑到了 \(i\) 这个点,强制要求第 \(j\) 段在 \(i\) 这里截断的最大收益,而 \(g_{i,j}\) 表示不强制要求 \(i\)\(j\) 的末尾的最大收益(可以看做是 \(f\) 的一个前缀最大值)。

转移显然为 \(f_{i,k} = \max \{g_{i - 1, k - 1} + 1, f_{pre, k} + 1\}\),这里的 \(pre\)\(a_i\) 上一次出现的下标。

\(g_{i,j} = \max \{g_{i - 1, j}, f_{i,j}\}\)

显然可以发现,你的转移若转移时 \(a_i\not = Mode\),则你的转移不优秀,那么这样的转移一定是正确的。

8.17

A. Web Delivery

感觉上和 8.13 的 D 题很像,发现对于 \(r\times c\) 是有限制的,也就是说,我们若能做到 \(r\times c \times 2^{\min(r,c)}\) 那么这个东西是能够通过的。

  • \(r<c\):考虑设 \(dp_{i,msk}\) 表示现在在 \(i\) 这个商店,目前已经采购了 \(msk\) 这些化学品,那么转移的时候我们假设 \(nmsk\) 是我们在当前位置采购的化学品,则有转移 \(dp_{i,msk} = \min\{dp_{i-1,msk-nmsk}+cst_{i,nmsk} + d_i,dp_{i,msk}\}\)。这个转移可以通过先做完全背包,再和上一层进行合并。
  • \(r>c\):那么就对商店做状压,设 \(dp_{i,msk}\) 表示现在考虑 \(i\) 这个化学品,目前我们其实已经采购了 \(msk\) 这些商店。

两者拼到一起就是正解。

现在对这个东西还是很敏感了,就是如果题目里面有特殊性质是两个互相约束的条件中的一个很小,那么一定要往这个方面去想。

B.Suffixes may Contain Prefixes

看题目感觉就是类似于 kmp 自动机的东西。

完全可以考虑将一个串的长度改成前缀个数,由于你的这个前缀一定是被匹配的,所以可以进一步转化成跳 Fail 指针的次数。

\(dp_{i,j}\) 表示目前拼到的长度为 \(i\),且目前的 nxt 值为 \(j\) 的最大价值。

这时候的转移就非常简单了,\(dp_{i + 1,trans[j][c]} \leftarrow dp_{i,j} + sum[trans[j][c]]\)

通过预处理 \(sum\) 可以做到 \(O(n^2)\)

D.最大前缀和

重点在于刻画?

考虑一个前缀 \(1\sim i\) 如何成为一个最大的一个前缀的?首先要求 \(i+1\sim n\) 这一段的前缀和都小于 \(0\),其次要求将 \(1\sim i\) 的数组取出,所有的后缀和都大于 \(0\)

那么我们肯定是对着这两个东西计数。由于 \(n\) 只有 \(20\),显然考虑状压,设 \(f_{msk}\) 表示将 \(msk\) 这些数重排满足后缀和全部大于 \(0\) 的排列数,而 \(g_{msk}\) 表示将 \(msk\) 重排,要求前缀和都小于 \(0\)

则转移为 \(f_{msk} = [sum_{msk} \ge 0] \times \sum\limits_{i\in msk} f_{msk\setminus\{i\}}\)\(g_{msk} = [sum_{msk} < 0] \times \sum\limits_{i\in msk} g_{msk\setminus\{i\}}\)

但是这样直接做会有一个问题:就是若最大前缀和小于 \(0\),那么你会把这个东西算成 \(0\),这个东西也很好解决,你可以花 \(O(n)\) 的时间复杂度去枚举第一个数放什么,这样就能完美解决上面这个问题。

答案为 $\sum f_{msk} \times sum_{msk} \times g_{U\setminus msk} $。

E.波浪

插入 dp 典题。

\(dp_{i,j,k,cur}\) 为目前从 \(1\) 填到了 \(i\),然后构成了 \(j\) 个联通块,现在填了 \(k\) 个边界 (\(1\) 号或者 \(n\))号,目前的绝对值之和的部分的和为 \(cur\) 的方案数。

转移写成了代码形式,不然太难打了:

// open on 2 side
if (c != 2)
    trans(dp[cur][j + 1][nxtk][c + 1], dp[lst][j][k][c] * (T(1)) * (2 - c) / i);
// open between
if (j + 1 <= i)
    trans(dp[cur][j + 1][nxtk][c], dp[lst][j][k][c] * (j + 1 - c) / i);
// keep on 2 side
if (c != 2)
    trans(dp[cur][j][nxtk][c + 1], dp[lst][j][k][c] * (T(1)) * (2 - c) / i);
// keep between
trans(dp[cur][j][nxtk][c], dp[lst][j][k][c] * (2 * j - c) / i);
// merge
if (j != 0)
    trans(dp[cur][j - 1][nxtk][c], dp[lst][j][k][c] * (j - 1) / i);

注释写的很清楚了,不过多赘述,注意精细化实现转移,不然会非常难以调试。

8.19

A.ZUMA

典题,但是还是记录一下。

倘若使用传统的 \(dp_{l,r}\),显然会有问题,因为你完全会因为未来无法被估计而无法转移。所以考虑新开一维状态表示未来信息,例如这道题目中,我们设 \(dp_{l,r,k}\) 表示考虑 \([l,r]\) 这段区间,未来会刚好有 \(k\) 个和 \(a_r\) 同色的东西在 \(r\) 的右边。

则有转移:\(dp_{l,r,k} \leftarrow dp_{l,r - 1, k + 1} + cst(k + 1)\),且有 \(dp_{l,r,k}\leftarrow dp_{l,mid-1,k +1}+dp_{mid+1,r,k}+cst(k+2)\) (要求 \(a_{mid}=a_r\))。

C.Lego Wall

同样的套路,我们又见面了?

观察到题目中有 \(r\le 700\)\(c\le 700\),考虑分开做。

  • \(r<c\):高度较小?考虑设 \(dp_{i,j}\) 表示考虑到第 \(i\) 条缝,目前在 \(i\) 条缝中有 \(j\)\(2\times 1\) 的块,这个的转移很简单。
  • \(r>c\):宽度较小?考虑设 \(dp_i\) 表示宽度为 \(i\) 的时候的答案,显然有容斥:\(dp_i = \sum\limits_{j=1}^{j} dp_j \times fib_{i-j}^r\)

然后就根据情况套用不同的 dp 即可。

D.Permutation

纯粹的性质观察 + 猜结论题目。

考虑到 \(10^18\) 对于 \(n\) 的大小还是太小了,猜测需要用到的状态不会太多。

我们有一个非常简单的 \(dp_{i,j}\) 表示目前有 \(i\) 个数,然后目前有 \(j\) 个逆序对的方案数。(给一个好玩的东西:Link

然后我们发现我们完全可以只存储 \(dp_{i,j} \le 10^18\) 的状态,而这样的状态刚好是一段前缀,这个东西可以很好的使用 vector 进行存储以及预处理,然后就做完了。

8.21

A.Not Another Constructive!

很唐诗的题目。

\(dp_{i,n,na,nac}\) 表示目前有 \(n\) 个 N,\(na\) 个 NA,\(nac\) 个 NAC 的合法性,这个东西显然可以使用 bitset 优化。

然后构造一下方案就行了。

D.懒惰排序

先咕咕咕,补了再写吧。

Summary

都是一些锐评。

首先是题目,我觉得里面不缺乏一些好的题目,有的很新奇,有的很典,但是都有收获吧。

其实 \(dp\) 就是一个观察性质的东西,当你发现可以通过性质对某些东西进行刻画,归类的时候,完全可以考虑使用 \(dp\)。而当刻画不完全时,可以考虑添加状态或者使用容斥。

觉得做不出来的问题还是在于刻画这一步,只能说多见识,多学习吧。

posted @ 2026-08-17 21:07  To_string  阅读(13)  评论(0)    收藏  举报