只是日常中冒出的一些灵感,仅作记录,可能背后有什么高深的思想,但我不太会。
原本想写在个本子上的,但是 whk 作业多回宿舍又摆烂了,就只能写在这里了。
处理决策单调性问题时,如果转移时是 \(f_i + \operatorname{cost}(i, j) \to f_j\),且 \(\operatorname{cost}(i, j)\) 不满足啥四边形不等式。
(虽然我也不知道不满足四边形不等式什么样的 \(\operatorname{cost}\) 才能决策单调,有点疑惑。)
先考虑如果转移是 \(f_i + \operatorname{cost}(i, j) \to g_j\) 时。
一个做法是考虑函数 \(\operatorname{solve}(li, ri, lj, rj)\) 表示对于 \(j\in [lj, rj]\),其最优的 \(i\) 一定在 \([li, ri]\) 中。
那么就可以先得到 \(mid = \frac{lj + rj}{2}\) 时最优的 \(i\),记为 \(p\)。
那么按照决策单调性,对于 \(j < mid\),最优的 \(i\) 一定 \(\le p\);对于 \(j > mid\),最优的 \(i\) 一定 \(\ge p\)。
所以可以递归下去 \(\operatorname{solve}(li, p, lj, mid - 1), \operatorname{solve}(p, ri, mid + 1, rj)\)。
如果不熟悉这个
时间复杂度:只会递归 \(\log n\) 层,每一层的 \([lj, rj]\) 对应的 \(ri + li - 1\) 的和是 \(\mathcal{O}(n)\) 的,所以是 \(\mathcal{O}(n\log n)\) 的。
优势:可以类似莫队的指针移动 \((i, j)\)(\(i\to i \pm 1, j\to j \pm 1\))来动态维护 \(\operatorname{cost}(i, j)\)。
复杂度分析:
- 对于 \(j\) 的移动,考虑强化到对于每个 \([lj, rj]\) 的移动都要走过 \(mid\to mid_{lj, mid - 1}\to mid_{mid + 1, rj}\to mid\),那么这个是关于 \(j\) 区间长度线性的,所以是 \(\mathcal{O}(n\log n)\) 的。
- 对于 \(i\) 的移动,首先对于 \([li, ri]\) 的移动依然是 \(\mathcal{O}(n\log n)\) 的,对于递归两个儿子中间的 \(\operatorname{solve}\to \operatorname{solve}\) 的这个过程,最坏情况下是 \(li\to p\) 的移动,但这依然关于 \(i\) 区间长度线性,所以也是 \(\mathcal{O}(n\log n)\) 的。
于是同时可以借用 cdq 的思想,外层再套一个 \(\operatorname{solve2}(l, r)\)。
记 \(mid = \frac{l + r}{2}\),先递归 \(\operatorname{solve2}(l, mid)\) 得到 \(f_{l\sim mid}\) 的信息,然后调用 \(\operatorname{solve}(l, mid, mid + 1, r)\),就可以处理出 \(f_{l\sim mid}\to f_{mid + 1 \sim r}\) 的转移,然后再递归下去 \(\operatorname{solve2}(mid + 1, r)\)。
复杂度:\(\mathcal{O}(n\log^2 n)\),分析可以直接把每一个 \(\operatorname{solve}(l, mid, mid + 1, r)\) 的时间开销加起来。
可能的优势:依然可以使用莫队的思想维护 \(i, j\) 指针得到 \(\operatorname{cost}(i, j)\)。
复杂度分析:
- 对于 \(\operatorname{solve}(l, mid, mid + 1, r)\) 依然是一样的分析,此处略过。
- 对于 \(\operatorname{solve2}\to \operatorname{solve2}\) 中间的指针变化,可以直接考虑把前面一个 \(\operatorname{solve2}\) 的贡献清空,这个的复杂度是关于区间长度线性的,所以只是 \(\mathcal{O}(n\log n)\) 的。
感觉可能是有点用的?
我在 NOIP 2024 考场上发现的东西,看起来网上还没有这些东西?
整体二分的核心思想其实是假设知道了 \(q\) 个询问各自的答案 \(ans_i\)。
那么对于询问 \(i\),只需要 \(\operatorname{check}\) 在一个 \([1, ans_{\max}]\) 的线段树上从 \(ans_i\) 开始往上爬的结点。
于是就只需要 \(\operatorname{check}\) 一共 \(\mathcal{O}(q\log ans_{\max})\) 个位置了。
一般的写法其实是就类似的在这个线段树上递归,如果访问到一个无用节点(不存在询问的答案在这个区间中)就退出。
实际上可以抛弃这个递归的写法,一个更好看的写法是考虑逐层处理,一层一层从左到右扫过这一层的节点并 \(\operatorname{check}\)。
因为有点摆所以我直接放核心代码吧
bool flag;do {for (int i = 1; i <= mxans; i++) qmid[i].clear();for (int i = 1; i <= q; i++) {if (ansl[i] <= ansr[i]) {int mid = ansl[i] + ansr[i] >> 1;qmid[mid].push_back(i);}}init();for (int a = 1; a <= mxans; a++) {update(a);for (int i : qmid[a]) {if (check(i, a)) ans[i] = a, ansl[i] = a + 1;else ansr[i] = a - 1;}}flag = false;for (int i = 1; i <= q; i++) {flag |= ansl[i] <= ansr[i];}} while (fl);