两者的本质均基于单调性,寻找题目中具有单调性的函数关系,然后施展二分或者倍增。二分答案可以用来解决分数规划问题,三分法可以求解单峰/谷函数。同时,二分上界不确定的内容的最佳方式是倍增,通过先倍增到上界,再倍增答案来解决。

二分

一种二分写法

通过 l <= r 之类的方法的二分笔者认为有些过于诡异,这里给出我自己的二分实现方式(我记得当时是在 B 站学的,但是出自哪个视频忘了)。

Luogu P2249
1
2
3
4
5
6
7
8
int L = 0, R = n + 1;
while (L + 1 != R) {
int mid = L + R >> 1;
if (a[mid] >= x) R = mid;
else L = mid;
}
if (a[R] == x) cout << R << " ";
else cout << "-1 ";

01 分数规划

用来求一个分式的极值,也就求一组 wi={0,1}w_i=\{0,1\},最大化或者最小化:

ai×wibi×wi\frac{\sum a_i\times w_i}{\sum b_i\times w_i}

我们一般使用二分答案来解决这个问题。以最大值为例:

ai×wibi×wi>midai×wimid×bi×wi>0wi(aimid×bi)>0\begin{aligned} &\frac{\sum a_i\times w_i}{\sum b_i\times w_i}>mid\\ \Longrightarrow& \sum a_i\times w_i - mid\times \sum b_i\times w_i > 0\\ \Longrightarrow& \sum w_i(a_i-mid\times b_i)>0 \end{aligned}

于是排序即可确定 wiw_i 的值。有些时候会限制 bi×wi\sum b_i\times w_i 的最小值之类的,例题,这时使用背包求解即可。

三分法

三分法可以用于求解单峰函数的极值点。对于常规的三分写法不再赘述,但值得注意的是,对于定义域为整数的实际问题,它们的答案可能真的是单峰的,但此时由于其不满足函数的连续性,会存在一大段平的区间,然后三分就炸了(鬼知道极值点在哪,你需要 O(n)O(n) 扫过去)。

不要错误的将某些函数视作可以三分,更不要将单峰函数视作凸函数而对导函数二分。单峰的证明是比较困难的,而且有时对于非单峰的问题也比较难找到反例(廊桥分配),要根据实际情况判断。

Luogu P1883

画图可知,两个单谷函数(一次函数的谷点视作在无穷远处)取 max\max 后,得到的新函数仍然是单谷函数,因此直接三分即可。

但很阴的是,这题要求极值而不是极值点,精度会被 bb 吃掉,因此 eps 要取小。

1
2
3
4
5
6
7
double L = 0, R = 1000;
while (L + eps < R) {
double Lmid = (L + R) / 2;
double Rmid = (Lmid + R) / 2;
if (calcF(Lmid) > calcF(Rmid)) L = Lmid;
else R = Rmid;
}

wqs 二分

O(logn)O(\log n)

倍增

ST 表

ST 表使用倍增结构来实现,支持在末尾插入一个数。大概长这样:

1
2
3
f[0][n] = a[n]; // The new N
for (int j = 1; 1 << j <= n; ++j)
f[j][n - (1 << j) + 1] = max(f[j - 1][n - (1 << j) + 1], f[j - 1][n - (1 << j - 1) + 1]);

在开头插入亦同理。

Problemset

** [CF1764G] Doremy’s Perfect DS Class

G1G2G3。给定一个 1n1\sim n 的排列 ppn1024n \le 1024,注意 210=10242^{10}=1024),每次你可以询问 l,r,kl,r,k,交互库会返回 plk,pl+1k,,prk\left\lfloor\cfrac{p_l}k\right\rfloor,\left\lfloor\cfrac{p_{l+1}}k\right\rfloor,\cdots,\left\lfloor\cfrac{p_r}k\right\rfloor 中不同数的个数,需要找到 11 的位置。交互次数分别限制在 30,25,2030,25,20 次。

询问能告诉我们什么?好奇怪啊,不知道。尝试从给定的 kk 值开始分析。k=1k=1 没什么意义,然后尝试从特殊的,比如 k=2,nk=2,n 开始分析。k=nk=n 比较好说,只有 nn 可以被记入答案,可以根据此找出 nn 的位置。k=2k=2 则可以将数分为两组,在 nn 为奇数时只有 11 是单独一组,nn 为偶数时只有 1,n1,n 是单独一组。

从别的地方再想一想,都要求 log\log 级别的询问,不难想到二分。设 solve(l, r) 代表答案在 [l,r][l,r] 的位置中,我们需要确定 11[l,mid][l,mid] 还是 [mid+1,r][mid+1,r] 里。咦,感觉不太对,不是严格的子问题!但是我们只需要寻找答案在哪里,因此只需要分别答案在 [1,mid][1,mid] 还是 [mid+1,n][mid+1,n] 就好了。

选择从 k=2k=2 入手,x,yx,y 分为一组仅当它们除以二下取整后的值相等。我们可以求出两个区间中在自己区间内没有匹配的数的数量,然后这个数量大的,答案就在那里(因为剩下的每有一个都是成对的)。

nn 是偶数怎么办呢?我们只需要找到 nn 就行,不难发现 k=nk=n 可以很好的完成这个任务。当两个区间的值相等时,说明 1,n1,n 各占一个,我们令 k=nk=n,询问其中一个,看 nn 是否在其中。找到 nn 的位置之后发现之后的递归不会受到影响(如果 pn>midp_n> mid,我们会递归到 [l,mid][l,mid],必定有 pn>midp_n>mid')。

这个做法可以通过 G2,代码。想过掉 G3,我们需要想方法杀掉那一次多余的询问。

怎么杀?对于 rl+1=2r-l+1=2 的情况,使用两次询问有点浪费,我们看能不能只用一次询问杀掉它。核心思想是,充分利用我们之前问出来的信息。当我们递归到 [l,r][l,r] 时,曾令一个 mid=l1mid=l-1,也令了一个 mid=rmid=r,因此我们知道 Q(1,l1,2),Q(1,r,2),Q(l,n,2),Q(r+1,n,2)Q(1,l-1,2),Q(1,r,2),Q(l,n,2),Q(r+1,n,2) 的答案。现在 l,rl,r 中一个是 11,一个是和其他数能匹配上的某个奇怪的东西,吗?注意,另一个可能是 nn,如果我们还没有确定 nn 的位置,那么通过询问 Q(r,n,n)Q(r,n,n)Q(1,l,n)Q(1,l,n) 将其判掉。

现在再看怎么搞 l,rl,r 一个是 11,另一个是可匹配数。可匹配数只能配在 [1,l1][1,l-1][r+1,n][r+1,n],如果 Q(1,l1,2)+1=Q(1,r,2)Q(1,l-1,2)+1=Q(1,r,2),那么说明可匹配数的匹配数是开在 [1,l1][1,l-1] 的(这个数除以二下去整的值与 [1,l1][1,l-1] 中的某个数撞了),否则开在 [r+1,n][r+1,n]。确定了这一点之后,我们就可以锁定 11 的位置了!以开在 [1,l1][1,l-1] 为例,如果 Q(1,l1,2)=Q(1,l,2)Q(1,l-1,2)=Q(1,l,2),说明 ll 处开可匹配数,与 [1,l1][1,l-1] 中的某个数匹配,11 就开在 rr 处。

这样在 rl+1=2r-l+1=2 时我们只花费了一次询问,可以通过 G3。代码


Nothing built can last forever.
本站由 iznomia 使用 Stellar 1.30.4 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。