专栏文章
0609
个人记录参与者 1已保存评论 0
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 0 条
- 当前快照
- 1 份
- 快照标识符
- @mip44qel
- 此快照首次捕获于
- 2025/12/03 05:51 3 个月前
- 此快照最后确认于
- 2025/12/03 05:51 3 个月前
在 github 阅读 以查看图片
NFLS T1
你有一个序列 ,长度为 。
有 组查询。每组查询包含五个整数 。
对于每组查询,你需要构造一个新序列 ,其中:
即:
&表示按位与 (AND)|表示按位或 (OR)^或⊕表示按位异或 (XOR)popcnt(x)表示整数 的二进制表示中 1 的个数(即 popcount)
你需要输出序列 中的第 小的元素。
不重要,值域和询问次数都
题解
首先立刻注意到异或的 popcount 是假的,可以拆成与和或。变成:
询问没有很多,每次询问我们可以枚举与和或的 popcount。于是考虑这个问题:
给定数组 ,多次询问,每次询问给定 ,要求你求出原数组里有多少 满足:
只要能解决这个,每次询问我们枚举 之后对权值排序,取前 个即可。
朴素做法是遍历 一个一个算,我们有一个高明的多的 DP。
我们数量不多,我们可以处理出答案数组 。考虑一位一位地处理这个东西。
表示考虑了前 个二进制位,前 为原数组值、后 位为输入值,与为 ,或为 的原数组值数量。
相当抽象,先看初始化,有 。
即,最开始,数组里的值就是自己原本的值,呈待处理。
我们要解决的问题是,一开始给你 ,多次询问给出 ,需要求出满足 的 的数量
于是这个 DP 的起点就是 ,终点就是 ,我们在过程中把 维护进状态。
对于手头上已有的 (要求 这一位为 ),若枚举到了第 位,直接放代码感觉更直观
CPPg[v][AND][OR]+=f[v][AND][OR];
g[v][AND][OR+1]+=f[v|(1<<i)][AND][OR];
g[v|(1<<i)][AND][OR+1]+=f[v][AND][OR];
g[v|(1<<i)][AND+1][OR+1]+=f[v|(1<<i)][AND][OR];
就是在枚举这一位的情况。
不妨来考虑一些更本质的东西。为什么这么做能节约复杂度,或者说,为什么要这样做?这样做的动机是什么?
先只关注 AND 的情况。考虑这样的图:

这描述了一个暴力的过程,对于每个 ,枚举所有可能的 ,一个一个 bit 地判断这一位与是否为 (当然代码里不会体现这一点),记录 popcount,最后穿过去之后变为 ,这个三元组的权值 会被累加给 。
权值是什么呢,容易发现其实已经隐含了一步映射,我们把 个 映射到了更小的值域上,并给每个值赋权了(即在 中的出现次数)
我们发现枚举 耗费了大量时间复杂度 ,但最后我们却不关心 的值,这实在太浪费了!
我们希望避免这种浪费,暂时没有什么思路。来仔细推敲一下这根红色运算线。随着 bit 的推进,我们会发现,我们不再关心这个 bit 之前的 的具体数值,我们只需要保留这个 bit 前面 的数值——毕竟我们最后统计是真的会用到 的。
能不能据此改进呢?哪怕一点点也可以。容易想到这个:

AND 的值不在此画出,实际上是另外一个维度,我们也暂时不太关心它
这样确实减少了状态,具有相同的 的状态被合并,而它们的 我们则不再加以区分。注意到 和 或 一定无交,直接把其中二者合并,我们的状态数还是 的
但这并没有完全解决我们的问题。你会发现让我们的状态数维持在 级别的罪魁祸首是 和 ——我们不得不同时记录它们二者,只有这样才能正确地在后面计算 AND。
然后你会立刻发现这蠢到家了——如果两个状态的 、 和 都是一样的,那么它们目前的值显然完全一样, 的影响完全不会体现,因为它在后面才会被考虑!
那我们自然会想去丢掉这个 ,会发现很简单,真的直接把它丢掉就行了。我们完全没必要分配 个起点,只需要分配 个起点,在每个 Bit 考虑这一位的 是 还是 ,就可以避免记录 了。
当然,对于一个状态,就需要转移到两个状态,代表枚举的 是 还是 。这是非常合理的。

然后你会发现我们做完了,复杂度对了,和之前描述的做法一模一样。核心在于,对于任何一个 bit,它前面的 和它后面的 我们都不关心,于是同 bit 状态数变成了 。
如果多维护一个 OR 也是简单的,加个维度多讨论一下就行了,反正这都是省不掉的维度,不用考虑优化,是小问题
多画图讨论一下是极好的啊(赞赏)
相关推荐
评论
共 0 条评论,欢迎与作者交流。
正在加载评论...