比赛链接:https://vjudge.net/contest/748191。
A - 互不侵犯
最最最最最最最板子的状压 DP。
大概就是一行一行去弄。然后的话,DP 维护三个量,当前遍历到第 \(i\) 行,目前总共放了 \(j\) 个国王,以及这一行的放置国王情况为 \(st\)(用二进制状压表示)。判断的时候,有一个这一行的情况 \(now\) 和上一行的情况 \(lst\),判断起来是很简单的,首先要判断这一行左左右右会不会侵犯到,那么直接左移右移然后与一下判断即可(代码片段:(now&(now>>1))||(now&(now<<1))
),然后还得判断这两行的关系,左移右移以及不移都要判(代码片段:(now&lst)||(now&(lst<<1))||(now&(lst>>1))
。转移就完全没难度了吧。
最后的答案就是 \(\sum dp_{n,k,st}\),\(st\) 从 \(0 \sim 2^n-1\)(表示最后一行的状态)。
B - 关灯问题 II
这题的状态设计是很简单的,\(dp_{st}\) 表示最少要按多少下开关才能让灯的开关状态为 \(st\)。显然 \(st\) 是使用了二进制压缩的。初始化所有 \(dp\) 均为极大值,只有 \(dp_{2^n-1} = 0\),因为最开始灯就是这个状态。
转移方程就是 \(dp_{now} = dp_{lst} + 1\)。这不难,但难点在于转移的顺序。可能会有想法依次枚举这些开关然后转移,但是这样行不通,因为开关按下的顺序会影响到灯的变化。
那就按照 DFS 的一个顺序来跑好了,如果可以从 \(lst\) 转移到 \(now\) 的话,那就继续跑 DFS(now)
好啦。
DFS 里的大致框架就是,首先枚举 \(m\) 个开关,依次尝试使用,根据原本状态 \(lst\) 求出新状态 \(now\),如果 \(dp_{now} > dp_{lst} + 1\)(说明这次转移有效),那么更新 \(dp_{now}\) 值并继续就好啦。
至于如何求出新状态,这个直接枚举一下 \(a\) 中的情况,然后挨个判断就 OK 了。
C - 炮兵阵地
与 A 题类似,不同的是,其不仅会攻击自己这一行和上一行,还会攻击到上一行的上一行,因此在存储 \(dp\) 数组的时候得再加一维。判断的方法与 A 题相似,只需根据题目的不同要求稍微改动即可。故不多赘述。
注意这里需要求 \(\text{popcount}\) 出来累加答案,可以考虑手写一个函数来计算。还有,注意山脉的位置不能布置炮兵部队。
整体的细节比 A 要多。
D - 砝码称重
注意到 \(m\) 只有 \(4\) 啊,那完全可以 DFS 枚举一下你要删去哪些砝码。显然也可以状态压缩找出情况,对吧。
那么就只需要考虑在给定砝码的具体情况的时候,该如何计算出这些砝码能称出多少种重量了。
其实上很简单,用一个 bool
类型的 DP 数组,定义 \(dp_x\) 表示是否可以称出 \(x\) 的重量。然后枚举每个砝码(注意要跳过被删去的砝码哦,不然删去这个砝码就没有意义啦),并倒着枚举 \(x\) 表示重量。转移?那就很显然了——dp[x]|=dp[x-a[i]]
,其中 \(i\) 当然是表示现在枚举到的砝码的编号,而 \(a_i\) 就是它的重量啦!
然后就结束了,整个来说算是一个比较暴力的题目。
E - Genshin Impact Startup Forbidden III
状压的题做得多了,好像还没怎么做过这种四进制压缩的题目诶,怪不得想不出来啊。
刚也说了,跟普通状压的区别就在于,普通状压是二进制压缩对吧,但这里是四进制压缩!
定义状态毫无难度对吧,\(dp_{st}\) 表示当前 \(k\) 个格子打捞鱼的情况,情况是指每个格子剩余的还没有被打捞的鱼的数量。所以初始化所有 \(dp_i = \infty\),只有 \(dp_{Ed} = 0\)。哦你问我 \(Ed\) 是什么?\(Ed\) 表示最初的一个状态!即,所有鱼都没被打捞走一条的四进制表示状态。
转移也很简单,首先倒着枚举 \(st\),记着先判断一下 \(st\) 是否合法(就是判断有没有出现,某个格子显示还剩 \(x\) 条鱼没有被打捞,但是这个格子实际上只有 \(y < x\) 条鱼的这种情况),如果不合法,那就跳过!合法?那就继续转移呗。
其实很简单,由于 \(k\) 过小,真正可以炸到鱼的安放炸弹的位置并不多,最多 \(5 \times k\) 个,我们可以把这些位置存下来,转移的时候用来遍历。嗯,很好,接下来遍历具体要在哪安放炸弹,然后根据鱼的位置情况以及剩余数量,依着 \(st\) 算出新的状态 \(now\)。那么转移就是 \(dp_{now} = \min( dp_{now} , dp_{st} + 1 )\) 了。
然后就结束了,唯一的难点在于想到四进制压缩。
F - 软件补丁问题
这题……
其实显然可以状态定义,\(dp_{st}\) 表示最少要使用多久的补丁去修复,才能使这些问题的是否存在情况为 \(st\)。
转移是简单的,只要根据输入算出对应的 \(B_1,B_2,F_1,F_2\),然后一一去算就 OK 了。考验位运算的基本功,这里就不多说了。
问题在于这东西也不能按顺序对吧,跟 B 一样,所以考虑 DFS,但是会 TLE!没办法,我们只好拿出终极武器——最短路。
是的哦,可以用最短路的思想来解决这些问题,不过不用真的建边,只要在遍历到某个状态的时候考虑其所有可以转移出去的情况就行啦。简单来说,就是遍历每个补丁然后去转移!当然,这里是扩散型。
我这里用的是 Dijkstra 的方法。
加上这个优化之后就能 AC 此题了。
G - A Simple Task
定义状态,\(dp_{st,i}\) 表示当前遍历的路径中包含 \(st\) 这些点(用二进制压缩),并且最后一步落脚在 \(i\) 节点的情况下,环的一个数量情况。
为了防止重复计算呢,我们这里强制定义一个环的点集中,编号最小的那个点为这个环的起点。也就是 \(st\) 的 \(\text{lowbit}\),没错。
最开始的初始化,应该是 \(dp_{2^i,i} = 1\),其他 $dp_i = 0 $ 吧。为了方便操作,我这里将下标从 \(1 \sim n\) 偏移成了 \(0 \sim n-1\)。
转移的时候,首先枚举 \(st\) 和 \(i\)。为了节省时间可以先判断一下 \(dp_{st,i}\) 有没有值,如果没有值那就没有转移的必要了。
然后继续枚举点 \(j\) 并要保证存在一条 \(i \to j\) 的边。还得判断一下,\(j\) 不能比 \(st\) 的 \(\text{lowbit}\) 还小,毕竟你不能中途再自定义一个起点啊,不能破坏秩序嘛。
那么接下来就是重要的环节了。判断一下,如果点 \(j\) 存在于 \(st\) 这个集合中,并且 \(j\) 还等于 \(st\) 的 \(\text{lowbit}\) 的话,那么恭喜啊,你找到了一个环!这个时候不要犹豫,\(ans\) 赶紧加上 \(dp_{st,i}\)。诶为啥不是 \(dp_{st,j}\)?因为有值的是 \(dp_{st,i}\) 啊,而 \(j\) 是起点你不可能把它作为终点去怎么样吧!否则 \(j\) 就不存在于 \(st\) 中咯,这个时候就是简单转移了,dp[st|(1<<j)][j]+=dp[st][i]
即可,没有难度哒。
但是最后求出的 \(ans\) 就是答案吗?错!由于是无向图,这种统计方案会把一条单独的边也视作一个环,所以首先要 \(ans-m\);而且,还是因为这是一个无向图,一个环它会顺时针跑一次,逆时针再跑一次,所以一个环会算两次,还得 \(\div 2\)。所以到头来,答案其实上是 \(( ans - m ) \ div 2\) 才对哦!