杂项

\((max, +)\) 卷积

给定两个长为 \(n\) 的序列 \(A,B\) ,求它们的 \((\max,+)\) 卷积 \(C\)\(\displaystyle C_i=\max_{k=1}^i \{A_i+B_{i-k}\}\)

这类问题的优化基于以下假设:

保证 \(A,B\) 是凸函数。举例:\(\displaystyle f_{i, j} = \max_{k < j} \{f_{i - 1, k} + a_i\}\)

结论:凸函数的 \((max,+)\) 卷积为凸函数。

考虑对 \(A\) 进行差分,由于是凸函数,这个差分得到的数组是单调的。同时,差分后 \((max, +)\) 卷积就变成了从 \(A\)\(B\) 中分别选一个前缀,满足一共选 \(i\) 个数,最大化前缀和。很显然的贪心就是我们直接选前 \(i\) 大的就行,因为差分后得到的数组有单调性。

所以直接把 \(A,B\) 的差分数组归并起来就得到 \(C\) 的差分数组。

发现这个其实就是 闵可夫斯基和 弱化版。

vector<int> max_add_convolution(vector<int> a, vector<int> b) {
    for (int i = a.size() - 1; i >= 1; i--)
        a[i] -= a[i - 1];
    for (int i = b.size() - 1; i >= 1; i--)
        b[i] -= b[i - 1];
    vector<int> c(a.size() + b.size() - 1);
    c[0] = a[0] + b[0];
    merge(a.begin() + 1, a.end(), b.begin() + 1, b.end(), c.begin() + 1, greater<>());
    for (int i = 1; i < a.size() + b.size() - 1; i++)
        c[i] += c[i - 1];
    return c;
}

用途的话,考虑 01 背包。

圆方树

为实用考虑,将 点双连通 定义为:不存在割点。这样做的好处是:

点双连通图中,任意两个点之间 至少存在 \(2\) 条相互(在起点和终点外)不在任意节点相交的路径。

后面这个性质经常出现在题目中(需要点双缩点的标志)。注意这一定义下,仅两个点和一条边构成的图亦为点双。因此,我们不用记录入边并避免走其反边。相当于直接把走该节点的入边也当做重边。

关于构建,每次访问一个节点将其入栈。由于搜索树没有横叉边,所以如果出现 \(dfn_x=low_y\) 意味着找到了儿子 \(y\) 子树中的一条链与 \(x\) 构成一个点双。这时候依次出栈直到弹出 \(y\) 就得到从下往上的该点双的点的序列。

最后这个点双再加上 \(x\) 但是 \(x\) 作为割点不出栈

// n0 为原图,n 为新图点数,add 为新图加边
namespace Org {
struct Edg {
	int y, nxt;
} e[maxn * 2];
int id, dfn[maxn], low[maxn], b[maxn], num;
int stk[maxn], top;
void tarjan(int x) {
	low[x] = dfn[x] = ++num, stk[++top] = x;
	for (int i = hd[x]; i; i = e[i].nxt) {
		int y = e[i].y;
		if (dfn[y]) low[x] = min(low[x], dfn[y]);
		else {
			tarjan(y), low[x] = min(low[x], low[y]);
			if (low[y] == dfn[x]) {
				++id, add(id, x);
				for (int z = 0; z != y; --top)
					z = stk[top], add(id, z);
			}
		}
	}
}
void solve() {
	id = n0;
	for (int x = 1; x <= n0; ++x)
		if (!dfn[x]) tarjan(x), --top;
	n = id;
	for (int x = 1; x <= n; ++x)
		for (int y : ::e[x])
			if (x <= y) printf("%d %d\n", x, y);
}
}

由于一个圆点实际上包含于与其相连的所有方点对应的点双中,考虑一个常见的问题:

要求支持单点修改,同时也会更新该点所在所有点双的权值。

如果暴力更新显然菊花图可卡,考虑一种套路:

每个方点 只维护其所有儿子(必然为圆点)的权值,每个单点修改只更新父亲(必然为方点)。在查询时,只需要对于路径 LCA / 子树根为方点的情况,额外考虑父亲(必然为圆点)的贡献。

wqs 二分

要求 恰好\(m\) 个关键物品并计算一个 代价最小化 问题,设答案为 \(f(m)\)。若 \(f(m)\) 为非严格凸函数,就可以采取此方法把复杂度 \(O(m)\) 部分(如 DP 的额外一维等)优化为 \(O(\log S)\),其中 \(S\) 为答案的上界。

具体来说,我们给每个关键物品,让选择其的代价额外减去 \(k\),然后正常跑出最小代价,并记录最小代价时选择了多少个物品,设为 \(x\)

显然这就对应于快速求出使得 \(b=f(x)-kx\) 最小的 \(x\),也就是凸包关于斜率为 \(k\) 直线的切点。显然 \(x\) 关于 \(k\) 单调

那么我们直接二分 \(k\),就可以确定给定的 \(m\) 对应的 \(k\),从而计算得到 \(f(m)=km+b\) 解决这个问题。


考虑 \(f\) 往往不是严格凸的,因此会出现在不同的在同一条直线上的点 \((x,f(x))\)。会发现若这条直线不过 \(m\) 其实没什么影响,但若过 \(m\),显然该直线的 \(k,b\) 都是确定。可以发现:

直接把 \(m\) 代入该直线即可得到解。

因此,我们不妨钦定每次对于一个 \(k\),都只算出其对应的 最小的 \(x\)。实现的方法是,计算代价时,在代价最小基础上,令第二维 关键物品的数目尽可能小


再证明一个细节,即假设要求选 \(m\) 个物品:

  1. \(k=mid\),切点横坐标为 \(m-1\)
  2. \(k=mid+1\),切点横坐标为 \(m+1\)

此处的解决是:直接类比一般二分,用结果 \(\ge m\) 的最小 \(mid\) 对应的结果作为答案。感性的理解是,由于 \(m, k\) 为整数,所以除非在一条直线上,否则上述问题不会发生。

所以这里的处理是:

if (x > m) r = mid + 1;
else if (x < m) l = mid;
else return b + k * mid;

注意这里不能交换 \(l,r\) 的地位,也就是在 \(x>m\) 时不能允许 \(r\) 取到 \(mid\)

另外一个细节是,(l + r) / 2 在有负数时不等价于 \(\lfloor\dfrac{l+r}{2}\rfloor\)。若 \(l,r\) 均为整数可以写为 (l + r) >> 1,始终向下取整。而上述做法中为了避免死循环,应该向上取整,则写为 (l + r + 1) >> 1

Broůvka 最小生成树

基本只会用于完全图或稠密图最小生成树。特征是边权都通过点与点之间的关系生成。

对于这类问题,Kruskal 和 Prim,前者有对边排序的操作,若采用类似 P5283 的做法又可能会导致遍历过多同一连通块内的边,复杂度没有保证;前者类比完全图上 Dijkstra,对于能够快速更新其它点 \(dis\) 的情形还是可做的。

那么考虑一种更加泛用的情况:只要对于一个点,可以不用暴力遍历就 \(O(k)\) 求出其对于连通块外的点连出的最小边,就能 \(O(kn\log n)\) 而与 \(m\) 无关地解决这一问题。

通用的实现(有些题往往有更简单的具体实现)是:

  1. 遍历当前各个连通块 \(i\),对于其中每个点 \(x\)\(O(k)\) 查询数据结构中的每个点 \(y\) 中使得 \(w(x,y)\) 最小的 \(y\),用这一边权更新 \(s_i\) \(s_{f_y}\)最后 将所有 \(i\) 中的点加入数据结构。

  2. 结束遍历后,再对每个 \(i\),把使得 \(s_i\) 最小的边插入 MST 并合并对应连通块。

  3. 重复 1, 2 操作直到只有一个连通块。

显然每次 1, 2 操作至少使得连通块数目减半,所以复杂度为 \(O(kn\log n)\)

边 Dijkstra

每次遍历 \(x\) 的出边,将每条边赋予 \(d_x + w(x,y)\) 的权值加入堆而不是直接更新 \(d_y\)。每次取出堆顶的边进行转移。

多了一个很好的性质:每个点第一次被转移最短路就已经正确。根据边权非负下 Dijkstra 的贪心结论可知。

这时候在一些优化建图的题目就很优秀了,可以类似 BFS 拿并查集优化,不重复转移节点。可以拿来减小空间或者时间,因为不需要把图完全建出来。

最小环

  1. 枚举点,暂时断开其所有入边,跑其出发的最短路。然后考虑入边连向它的点,用这一长度加上最短路更新。\(O(nm\log n)\)

  2. Floyd。即 \(\min \{d(i,i)\}\)

树上高斯消元

几乎只用在随机游走问题上,当然一些奇怪的题目状态构成树也可以做。特点是 \(f_i\) 对应的方程中仅当 \(j\)\(i\) 或者与 \(i\) 相邻的点时 \(f_j\) 的系数期不为 \(0\)。希望做到 \(O(n)\)

考虑自底向上把所有点的方程表示为 \(A_i\cdot f_{i}+B_i\cdot f_{fa} + C_i = 0\)。对于节点 \(u\),若其儿子均已表示完成,那么 \(f_i\) 对应的方程中除了 \(f_i,f_{fa}\) 之外的项都可以代换成仅含变量 \(f_i\) 的式。于是 \(u\) 的方程也可以表示成 \(A_i\cdot f_{i}+B_i\cdot f_{fa} + C_i = 0\) 的形式。

最后由于根节点与父亲相关的项不存在,那么直接解出了 \(f_{rt}=-\dfrac{C_{rt}}{A_{rt}}\),然后再自顶向下代入即可。

多哈希表

// hsh 为双哈希第二维
struct Hash {
	const static int num = maxn * maxk, lim = 10000007;
	int tot, hd[lim], val[num], nxt[num];
	unsigned long long hsh[num];
	int& get(int x, unsigned long long y) {
		x = x % lim;
		for (int p = hd[x]; p; p = nxt[p])
			if (hsh[p] == y) return val[p];
		nxt[++tot] = hd[x], hd[x] = tot, hsh[tot] = y;
		return val[tot];
	}
} t;
posted @ 2023-10-29 20:44  音街ウナ  阅读(20)  评论(0)    收藏  举报