算法竞赛中常见trick

本文主体译自 Collection of little techniques 并有所删改

前言略

1.bitset优化空间

考虑 DAG上的可达性 ,给定一个 n 个节点和 m 条边的 DAG,包含 q 次查询,其中查询的形式为 "顶点 v 是否可由顶点 u 到达" ,其中 1n,m,q105 且不允许有类似 O(n2) 的空间复杂度,其中包括开 n 个长度为 n 的 bitset

一个比较经典的解法是让 dp[v] 成为一个 bitset ,当 v 可以到达 u 时令 dp[v][u] = 1 。这个方法在大多数情况是可行的,但是 105 个长度为 105 的 bitset 也会占用大量内存,因此我们可以用 64 位整数来代替 bitset ,并重复该算法 n64 次。在第 k 次执行该算法时,我们令 dp[v] 成为一个 64 位整型用来储存 64k,64k+1...64k+63 是否可以到达 v

2.避免在莫队中出现 log

考虑如下问题:给定一个长度为 n 的序列和 n 次询问,询问区间 mex ,不强制在线。

线段树固然可行,但我们现在要考虑莫队。

在利用莫队维护 mex 的时候,我们同时也需要一个 set<int> 来维护在该区间所有未出现的整数,这样在每次指针后我们都可以用 O(logn) 的复杂度来维护答案,这样莫队总的复杂度就做到了 O(nnlogn) ,但是这样太慢了!

显然复杂度的瓶颈是 set<int> ,我们之所以用它是为了支持以下操作:

  1. 插入一个元素,当前单次复杂度 O(logn) ,需要操作 O(nn)
  2. 删除一个元素,当前单次复杂度 O(logn) ,需要操作 O(nn)
  3. 查询最小值,当前单次复杂度 O(logn) ,需要操作 O(n)

我们注意到前两种操作更频繁一点,但他们的时间复杂度相同,因此我们可以牺牲最后一个操作的复杂度,使得前两种操作更高效。

我们需要这样一个数据结构:

  1. O(1) 插入元素
  2. O(1) 删除元素
  3. O(n) 查询最小值

因此我们可以将值域分块,插入删除显然可以 O(1) 完成,同时我们维护是否每个块中的元素已经全部出现,自小到大找到一个非满的块暴力查询即可。

这是在莫队中至关重要的一件事,确保指针能在 O(1) 复杂度下移动,即使有时需要牺牲查询的复杂度

3.根号优化背包/ “3k trick”

假设有 n 个物品,每个物品都有一个非负数权值 ai ,且 ai=m ,询问是否能从中选出一些物品使得其权值和为 w

我们假设有三个相同权值的物品 a,a,a 。注意到用 a,2a 可以替换掉这三个物品,我们可以重复这个过程直到每个权值至多有两件物品,且有 ai=m ,因此至多只有 O(m) 个物品。因此我们可以直接进行背包,同时物品的数目减少至 O(m) 个,这在大部分情况下是更好的复杂度

这个技巧大多出现在序列 a 能被某些特殊形式划分的时候,例如他们可能代表一个图的分量,请看示例:

问题链接

给定一个含有 n 个字母的字母表以及一个含有 m 个单词的字典
你想用两种颜色给字母染色,使得每个单词中相邻字母都是不同颜色的,并尽量减少两种颜色字母的数量差

我们考虑把整个过程看成二分图,最初的输入会产生一定的分量,我们可以考虑翻转其中的一部分使得两边大小尽量相同。最初我们可以翻转全部的分量,使得左边的分量变小。然后我们可以每次选择翻转一部分分量,使得左侧部分大小增加 ai ,且 ai 为定值。 这样就可以利用上述技巧解决掉整个问题了。

4. 不同种元素的划分方案数

该问题与上一个问题有关,对于长度为 n 的非负整数序列有 ai=m ,我们能得到不同的 ai 的取值至多只有 O(m) 种。

证明:

取出 a 中全部各不相同的值,并将其按升序排序得到 b0,b1,,bk1 ,由于 b 序列中均为非负整数,因此有 bii ,进而可以得到

b0+1+2+...+(k1)=k(k1)2

且有 bm ,所以 k 至多只有 O(m) 种。

5.从背包中删除元素

假设有 n 个物品,第 i 个物品的价值是 wi 。你需要维护一个 dp 数组,其中 dpi 表示得到价值总和恰好为 i 的物品有多少种方式。

向背包中加入物品是个经典问题

// 从大到小转移保证不会被重复选择
for(int i = MAX_Val; i >= val; i--) {
    dp[i] += dp[i - val];
}

要是想撤销刚才的操作,我们只需要将一切都倒过来即可

// 撤销新加入的状态
for(int i = val; i <= MAX_Val; i++) {
    dp[i] -= dp[i - val];
}

需要注意的是 dp 数组的状态与物品加入的顺序无关。实际上,刚才所展示的代码可以撤销掉任意一个价值为 val 的物品,我们可以通过假设该物品是最后加入的来证明其正确性。

如果我们仅仅是为了检验是否有得到价值恰好为 i 的方案,而不关心有多少种方案的时候依然可以用这个技巧的变种。我们依然是计算方案数,并检查方案数是否为 0 ,由于方案数可能会很大,我们可以选取一个大素数作为模数。虽然可能会出现错误,但是如果我们随机选择一个足够大的模数,该方法的成功率是相当可观的。

6.调和级数

有结论

Hn=11+12+13++1n=Θ(logn).

该结论可以用来计算某些算法的复杂度。通常来说这类算法都类似这样:

for(int i = 1; i <= n; i++)
	for(int j = i; j <= n; j += i)
	// 某些操作

对于一个确定的 i 来说,有着不超过 nij 的取值,且他们总的数目同上述结论可以推得

n1+n2++nn=nHn,

因此该算法时间复杂度为 O(nlogn)

本文作者:Jadebo1

本文链接:https://www.cnblogs.com/Jadebo1/p/17609897.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Jadebo1  阅读(364)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.