在写莫队的时候,常用到这样的四个循环(以下代码摘自 OI Wiki 普通莫队算法):
while (l < a[i].l) del(c[l++]);
while (l > a[i].l) add(c[--l]);
while (r < a[i].r) add(c[++r]);
while (r > a[i].r) del(c[r--]);
在有些题目中,我们需要保证 [l,r] 在移动端点的过程中始终是合法区间,即 l≤r+1 恒成立(l=r+1 表示空区间)。此时,每个元素被加入的次数都是非负整数。
因为考虑莫队区间的移动过程,从开始到这时,相当于加入了 [1,r] 的元素又再删除了 [1,l−1] 的元素,因此:
上面我们证明了,如果某时刻出现 l>r+1 的情况,那么会存在一个元素的加入次数是负数。这在某些题目会造成问题,例如我们如果用一个 set
维护区间中的所有数,就会出现“需要删除 set
中不存在的元素”的问题。
综上,很多时候保持 l≤r+1 始终成立是必要的。
然而,上面这种典型的写法不能保证 l≤r+1 恒成立。令 l′ 表示 a[i].l
,r′ 表示 a[i].r
,那么当 l<r<l′<r′ 时,上面的写法会先将左端点右移到 l’,可能造成 l>r+1。
这四个循环一共有 4!=24 种排列顺序,我枚举了第一个循环是操作左端点的这 12 种排列(另外 12 种是对称的),并写出了正确性和反例。
循环顺序 | 正确性 | 反例 |
---|---|---|
l--,l++,r--,r++ | 错误 | l<r<l′<r′ |
l--,l++,r++,r-- | 错误 | l<r<l′<r′ |
l--,r--,l++,r++ | 错误 | l<r<l′<r′ |
l--,r--,r++,l++ | 正确 | |
l--,r++,l++,r-- | 正确 | |
l--,r++,r--,l++ | 正确 | |
l++,l--,r--,r++ | 错误 | l<r<l′<r′ |
l++,l--,r++,r-- | 错误 | l<r<l′<r′ |
l++,r++,l--,r-- | 错误 | l<r<l′<r′ |
l++,r++,r--,l-- | 错误 | l<r<l′<r′ |
l++,r--,l--,r++ | 错误 | l<r<l′<r′ |
l++,r--,r++,l-- | 错误 | l<r<l′<r′ |
因此,24 种排列中只有 6 种是正确的,其中有 2 种的证明较繁琐,这里只给出其中 4 种的证明。
这 4 种正确写法的共同特点是,前两步先扩大区间(l--
或 r++
),后两步再缩小区间(l++
或 r--
)。这样写,前两步由于是扩大区间,因此区间一定不会变非法;执行完前两步后,l≤l′≤r′≤r 一定成立,此时执行后两步只会把区间缩小到 [l′,r′],不会造成区间不合法。
翻阅此题的题解,许多题解的写法是前两种(先操作左端点,再操作右端点)或后六种(第一步就是缩小区间),这些写法虽然在此题可能不会造成很大问题,但由于此题有莫队模板的性质,可能会造成新学莫队的 OIer 此后写莫队时出错。
而 OI Wiki 上更是用粗体标出“注意:由于 ++l
和 --r
的存在,下面代码中的移动区间的 4 个 for 循环的位置很关键,不能改变它们之间的位置关系”,我认为这个错误比较严重。如果我的这些讨论没有错误,我将到 OI Wiki 上对此段代码进行修改。
本人水平有限,以上内容如有错误,敬请指正。