LOJ 3400 Storm & CodeChef SELEDGE 命题报告(上)

关于本文

SELEDGE 是我在 CodeChef 网站上命制的题目。

后来我将该题改编为集训队作业自选题,并由 EI 鸽鸽将其公开在了 LOJ 上(赞美 EI !),题号是 LOJ 3400 。本文将以 LOJ 上的题目版本为准。

本文分为上、下两个部分,前者基本上是复制粘贴自己先前写的题解,后者是对解法二的复杂度上界进行了改进。

题目大意

给定一张 \(N\) 个点 \(M\) 条边的无向图,点和边都有非负权,寻找一个边集的 \(\leq K\) 元子集,以最大化:该边集覆盖的点集的权值和减掉该边集本身的权值和。

数据范围

多组数据,每个测试点内至多 5 组数据,满足一个测试点中 \(\sum 2^K(N+M)\leq 10^6\)

\(2\text{s},512\text{MB}\)

标准算法(解法一)

观察:如果选出的边中有三条边构成一条链,则删掉中间的那条一定不劣;如果选出的边中有若干条构成环,则删掉任何一条一定不劣。

推论:最优解选出的边集,一定构成若干棵直径不超过 2 的树,即若干个不相交的菊花图。

推论:最优解选出的边集,一定构成一张二分图。

我们对每个点等概率独立随机地染上黑白两种颜色之一,并要求这一染色方案,恰好也是最优解所对应的二分图的黑白染色方案。

尝试计算最优解符合这一要求的概率:

  • 考虑一张 \(n\) 个点的菊花图,显然它有 2 种染色方案,所以它被染对颜色的概率是 \(\dfrac 2{2^n}=2^{1-n}\)
  • 假设最优解中每个菊花的结点数分别为 \(a_1,\cdots,a_l\) ,则一定有 \((a_1-1)+\cdots+(a_l-1)\leq K\) ,其中 \(K\) 表示最多能够选出的边数。
  • 从而所有菊花都被染对颜色的概率是 \(2^{1-a_1}\cdots 2^{1-a_l}\geq 2^{-K}\)

下面,我们尝试计算满足上述要求的解中的最优者。建立费用流模型计算答案:

  • 建立二分图,白点在左侧并与 \(S\) 相连,黑点在右侧并与 \(T\) 相连。
    • 对于白点 \(v\) ,从 \(S\) 向它连一条容量为 1、费用为 \(-A_v\) 的边,和一条容量为 \(\infty\) 、费用为 0 的边。
    • 对于黑点 \(v\) ,从它向 \(T\) 连一条容量为 1、费用为 \(-A_v\) 的边,和一条容量为 \(\infty\) 、费用为 0 的边。
  • 对于原图中的边 \((u,v,B)\) 满足 \(u\) 为白色、 \(v\) 为黑色,连一条从 \(u\)\(v\) 的边,容量为 1,费用为 \(B\)
  • 在该图中限制流量不超过 \(K\) ,则最小费用的相反数就是答案。

解释:

  • 一单位从 \(S\) 到白点 \(u\) 再经边 \((u,v,B)\) 到达黑点 \(v\) 再到达 \(T\) 的流量,表示在原图中选择边 \((u,v,B)\)
  • 结点与 \(S/T\) 的连边的作用是处理点权。当每个点拥有第一份流量时即会产生对应的贡献,流量继续增加时则不会产生贡献。

用基于 SPFA 的连续最短路算法求解该费用流模型,则复杂度是 \(O\big(K^2(N+M)\big)\) ,证明:

  • 首先,显然 SPFA 的运行次数 \(\leq K\)
  • 然后,在一次 SPFA 中,任何一个结点至多入队 \(O(K)\) 次。这是因为:
    • 任意时刻有流量的边不会超过 \(3K\) 条,否则就意味着在原图中选了超过 \(K\) 条边。
    • 对于任何一条长为 \(L\) 的增广路,其中至少有 \(\dfrac L2-2\) 条边是某条有流量的边的反向边,因为正向边都是从图的左侧指向右侧,只有这些反向边才会从右侧指向左侧。
    • 综合以上两条,得到任意一条增广路的长度不超过 \(6K+4\) ,因此每个点的入队次数 \(\leq 6K+5\)
  • 综上,复杂度是 \(O\big(K^2(N+M)\big)\)

引理:如果一枚硬币以 \(p\) 的概率掷出正面,则连续掷 \(\dfrac 1p\cdot -\log\epsilon\) 次能以 \(1-\epsilon\) 的概率掷出至少一次正面。

  • 证明:全反面的概率 \((1-p)^{\frac 1p\cdot -\log\epsilon}=\big((1-p)^{\frac 1p}\big)^{-\log \epsilon}\leq \Big(\dfrac 1e\Big)^{-\log\epsilon}=\epsilon\)

注意到我们已经能够以 \(O\big(K^2(N+M)\big)\) 的复杂度获得以 \(2^{-K}\) 的概率正确的解。那么我们把整个过程重复 \(-2^K \log\epsilon\) 次以得到 \(1-\epsilon\) 的正确率,则总复杂度 \(O\big(2^KK^2(N+M)\cdot -\log\epsilon\big)\) ,可获得满分。

针对较小的 \(K\) 的解法(解法二)

该解法是作者与 Istvan Nagy ( CodeChef 验题人)在讨论中得到的,在此致谢。

该解法对应于子任务 2 ,复杂度为 \(O\big(N+M+f(K)\big)\) ,在 \(N,M\) 远大于 \(K\) 时优于本文中其他两个算法。

考虑搜索,假设已经钦定一个边集 \(S\) 要出现在答案中,考虑如何选取剩下的边以最大化目标函数。

对于边 \(e\notin S\) ,我们定义其“当前贡献”为:它的未被 \(S\) 覆盖的端点的权值和,减掉它自己的权值。

考虑当前贡献最大的边 \(e_0\) ,分类讨论:

  • 如果 \(e_0\) 出现在答案中,皆大欢喜。
  • 如果 \(e_0\) 没有出现在答案中,继续分类讨论:
    • 如果 \(e_0\) 的两个端点都未被 \(S\) 覆盖,继续分类讨论:
      • 如果两个端点在最优解中仍然都没被覆盖:这种情况不可能出现,否则可以随便把另一条边替换成 \(e_0\) 而使得答案不变劣。
      • 如果两个端点恰有一个(记为 \(v\) )在最优解中被覆盖:也不可能出现,否则把 \(v\) 的邻边替换成 \(e_0\) 一定不劣。
      • 如果两个端点在最优解中都被覆盖:这是唯一可能的情况,记为情形 I 。
    • 如果 \(e_0\) 的两个端点中有一或两个被 \(S\) 覆盖,与上一情况同理进行分类讨论,可知 此种情形不可能出现。

假设我们遇到了情形 I ,则需要选择两条边来分别覆盖 \(e_0\) 的两个端点。设 \(v\)\(e_0\) 的一个端点,考虑如何选择边来将其覆盖:

  • 先把 \(e_0\) 从图中删去。
  • \(v\) 的邻边按当前贡献降序排列,假设其中第 \(k\) 条边 \(e_k\) 是这些边中首条出现在答案中的,则:
    • \(e_1,e_2,\cdots,e_{k-1}\) 的另一个端点一定也被答案中其他边覆盖,否则把 \(e_k\) 换成 \(e_{1,\cdots,k-1}\) 中的某一个一定不劣。
  • 等等,如果 \(e_{1,2,\cdots}\) 中有些边的另一端点已经被覆盖了呢?那么轮到这条边(即确定了排在它前面的边都不选)的时候一定会选它,否则必定可以做不劣的调整。

于是得到搜索算法:(下述伪代码经过简化,忽略了若干种不影响复杂度分析的边界情况)

S: set /* set of chosen edges */
L: set /* set of edges left on graph */
res: int /* maximum value over all schemes */

procedure DetermineBestEdge /* called at the beginning of the program */
	res := max(res,cost(S))
	if |S| > K
		return
	e := edge in L with largest current contribution
	S := S∪{e}
	L := L\{e}
	DetermineBestEdge() /* suppose that e is chosen */
	S := S\{e}
	CoverVertices({e's endpoints}) /* suppose that e is not chosen */
	L := L∪{e} /* restore to prior state */

procedure CoverVertices( todo: stack /* stack of vertices to cover, passed by value not by reference */ )
	if todo is empty
		DetermineBestEdge()
		return
	v := todo.pop()
	es := array of v's neighbouring edges in L, sorted by current contribution
	if |S| >= K
		return
	for e ∈ es
		S := S∪{e}
		L := L\{e}
		CoverVertices(todo) /* suppose that e is chosen */
		S := S\{e}
		push e's other endpoint into todo /* suppose that e is not chosen */
	L := L∪es /* restore to prior state */

尝试分析该算法的状态数:

  • 考虑任一时刻的递归栈(注意区分递归栈与 todo 栈;递归栈中包含了每一层递归中的 todo 栈信息),可以发现存在一个 从递归栈到(儿子有序的)有根树的单射。具体地说:
    • 以下我们将忽略“无用”的函数实例。具体地说:
      • 如果一个 CoverVertices 函数进入了 todo is empty 这个分支,则我们把它从递归栈中删去,并认为是它的前驱(即递归栈中的上一个函数)直接调用了 DetermineBestEdge()
    • 对每个 DetermineBestEdge 函数的实例 \(a\) ,建立结点 \(X_a\) ;对每个 CoverVertices 函数的实例 \(b\) ,建立结点 \(Y_b\)
    • 对每个 \(Y_b\) ,在函数 \(b\) 中都一定执行了 todo.pop() 。设所弹出的元素是在结点 \(Z\) 所对应的函数实例中被压入的,则把 \(Y_b\) 的父亲定为 \(Z\) 。这样会得到一个森林,森林中的树根有且仅有全体 \(X\) 类型结点。
    • 建立一个“超级根结点” \(R\) ,然后把森林中每棵树的树根作为 \(R\) 的儿子。
    • 把每个结点的儿子按照在递归栈中的出现顺序排列。
    • (从一棵上述的有根树,容易基于刚刚的伪代码唯一地还原出递归栈,所以这是一个单射)
  • 考虑压缩有根树的结点数。
    • 对于一个 DetermineBestEdge 的实例对应的结点 \(X\)
      • 如果 \(X\) 有儿子(易见一定是两个),则把它的两个儿子结点直接连向 \(X\) 的父亲 \(R\) ,并把 \(X\) 删除。
      • 如果 \(X\) 没有儿子,则不做任何修改。
    • 这样有一个问题:\(R\) 可能会有连续多个儿子,而我们不知道这些儿子中哪些是单独的 \(X\) 类型结点,哪些是两个一组的 \(Y\) 类型结点。所以我们还需要记录 把 \(R\) 的儿子划分成长为1或2的连续段的方案。易见这样的方案有 \(F_{d}\) 种,其中 \(F\) 表示斐波那契数列,\(d\) 表示 \(R\) 的度数。
  • 观察:这棵树中的每个结点(除了 \(R\) 以外)在递归栈中对应的函数实例 \(c\) ,都一定选择了至少一条边(即执行了 S := S∪{e} )。
    • 这里指的不是“ \(c\) 在其函数体中选择了一条边”,而是“ \(c\) 的前驱 \(d\) 一定是先选择一条边,随后再调用 \(c\) ”。
    • 这里“前驱”的定义是:递归栈中在 \(c\) 之前的最后一个函数实例,满足 在压缩结点数的时候 它没有被删除。
  • 推论:树的大小 \(\leq K+1\)
  • 推论:树的个数 \(\leq \sum\limits_{i=2}^{K+1} C_{i-1}\) ,其中 \(C\) 表示卡特兰数。
  • 再算上至多 \(F_d\) 种划分,我们得到:可能的递归栈数量(即状态数)不超过 \(\sum\limits_{i=2}^{K+1} F_{i-1}C_{i-1}=O(F_KC_K)\)
    • 这个等号是因为斐波那契数指数级增长的性质。

经过 \(O\big((N+M)\log N\big)\)\(\log\) 来自排序)的预处理后,每个状态的复杂度开销可以做到 \(O(K)\) 。具体地说:

  • 预处理是把每个点的邻边按初始时的当前贡献排序,再把所有边按当前贡献排序。
  • 每个状态中要查询一个点的当前贡献最大的邻边,而 这些邻边中当前贡献相比初始时改变了的 显然只有 \(O(K)\) 条。于是可以 \(O(K)\) 地查询。
  • 至于寻找全局最大的当前贡献,可以通过维护每个点的最大当前贡献得到。

于是总复杂度为 \(O\big((N+M)\log N+F_KC_KK\big)\)

注意到 \(O\big((N+M)\log N\big)\) 的部分可以用 \(O\big(N+M\big)\) 的过程替代:(虽然没啥实际意义)

  • 预处理时:
    • 对每个点用快速选择求出前 \(K\) 优的边(代替前述预处理中对每个点邻边排序的过程)
    • 再从所有边中快速选择求出前 \(K\) 优的那些(代替前述预处理中对所有边排序的过程)
    • 这样我们得到了若干个大小为 \(K\) 的边集。
  • 在算法过程中,每次要找一条“最优的边”(包括找全局最优和一个点邻边中的最优)时,显然不会用到前 \(K\) 优的边以外的边。那么找到对应的 \(K\) 元边集,然后在里面暴力即可。
  • 这样,我们在预处理时就没有排序,排序的复杂度将被转嫁到 \(O\big(F_KC_KK\big)\) 这一部分上。总复杂度 \(O\big(N+M+f(K)\big)\)

基于带权带花树的解法(解法三)

(该解法可获得满分,其前半部分对应于子任务 1 )

类比边覆盖问题的解法,考虑按任意顺序加入答案中的边,则每条被加入的边:

  • 要么首次覆盖了其两个端点之一
  • 要么首次覆盖了其全部两个端点

于是我们考虑先决定哪些边覆盖了其全部两个端点,再用其余的边每次覆盖一个端点。

于是可以建出这样的一般图匹配模型:

  1. 对原图的每个结点,建立模型中的对应结点。

  2. 对原图中的每条边 \((u,v,B)\) ,在模型中连边 \((u,v,\textit{weight}(u)+\textit{weight}(v)-B)\)

  3. 对原图中每个结点 \(v\) ,找出其邻边的最小权值 \(B_0\) ,在模型中建立新点 \(v'\) 并连边 \((v,v',\textit{weight}(v)-B_0)\)

则该模型中恰包含 \(K\) 条边的最大权匹配就是答案。

解释:

  • (2)中的每条边表示一条“首次覆盖了其全部两个端点”的边。每选择一条这样的边,就意味着其两个端点不再能够选择(3)中的边,这与原问题是相符的。
  • (3)中的每条边表示一条“首次覆盖了其两个端点之一”的边。易见对于被覆盖的 \(v\) ,我们只会选择 \(v\) 的权值最小的邻边。
  • 如果一条实际上覆盖了两个端点的边,被当成只覆盖了一个端点呢?
    • 这样答案一定不会减少,所以这样的方案不优。

如何求出恰包含 \(K\) 条边的最大权匹配?

  • 引理:记 \(f(K)\) 为恰包含 \(K\) 条边的最大权匹配,则 \(f\) 是凸函数。证明:
    • 任取一个 \(L\) ,考虑 \(f(L-1)\)\(f(L+1)\) 对应方案的边集的并集。
    • 显然这个边集构成了若干条链和若干个偶环的并,而且其中包含奇数条边的链的数量为偶数。
    • 因此,这个边集显然可以划分为两组等大小(即大小为 \(L\) )的匹配,这两组中至少有一组的权值 \(\geq \dfrac{f(L-1)+f(L+1)}2\)
    • 由此得到 \(f(L-1)+f(L+1)\leq 2f(L)\) ,所以 \(f\) 为凸函数。
  • 那么采用WQS二分的技巧,将额外的权值加在边上,然后用带权带花树算法跑最大权匹配即可。
  • 复杂度 \(O(N(N+M)\log N\log W)\) ,其中 \(W\) 表示边权上限。稍微注意一下常数即可通过子任务 1 。

这个算法可以进一步优化以获得满分:

  • 在带权带花树算法的增广过程中,把对每个点 DFS ,改成以全体未匹配点为起点集合做 BFS 。
  • 那么,直接进行 \(K\) 次增广,即可得到大小为 \(K\) 的最大权匹配。复杂度 \(O(K(N+M)\log N)\)
  • 在 WC2020 营员交流中,徐翊轩题为《浅谈二分图最大权匹配》的报告提到了这一算法,并给出了一个并不严格的正确性说明。暂未见过严格的证明。
posted @ 2021-05-18 10:55  TianyiQ  阅读(221)  评论(0编辑  收藏  举报