概述
这是一个简单的图论问题:求一个无向连通图 G G G 的子图,使得 ∣ E ′ ∣ ∣ V ′ ∣ |E'|\over|V'| ∣V′∣∣E′∣ 最大。后文中称呼 ∣ E ′ ∣ ∣ V ′ ∣ |E'|\over|V'| ∣V′∣∣E′∣ 为该子图的 密度。
注:是否要求该子图为连通图并不重要——如果子图不连通,那么两个连通分量中可以取 ∣ E ∣ ∣ V ∣ |E|\over|V| ∣V∣∣E∣ 较大的一个,所以只会保留一个连通分量。
第一步转化
考虑 0 / 1 0/1 0/1 分数规划。二分答案后,选择一条边则增加 1 1 1 的权值,选择一个点则减少 w w w 的权值。
只需要判断最大权值是否为正。
方案一
首先有一个简单的方案:选择一条边则必须选择其端点。这可以转化为最大权闭合子图。
但是,我们把边和点都视为物品,所以新的图中有 O ( n + m ) \mathcal O(n+m) O(n+m) 个点,在稠密图中,这是非常大的开销。
方案二
注意边和点并不是那么独立的物品。比如说,很明显地,选择了一个点集之后,一定会选择导出子图。
那么导出子图怎么算边数呢?考虑用度数。如果记
c
(
S
)
c(S)
c(S) 为,有多少条边满足只有一个端点在
S
S
S 中,则
∣
E
′
∣
=
1
2
[
∑
x
∈
V
′
deg
(
x
)
−
c
(
V
′
)
]
|E'|=\frac{1}{2}\left[\sum_{x\in V'}\deg(x)-c(V')\right]
∣E′∣=21[x∈V′∑deg(x)−c(V′)]
此时我们惊讶地发现:
c
(
V
′
)
c(V')
c(V′) 正好是一个割。它不就是使得
V
′
V'
V′ 与其补集不连通的删边数量吗?
于是考虑回到网络流,但是此时只有点是物品。为了与最小割相符,考虑最小化 w ∣ V ′ ∣ − ∣ E ′ ∣ w|V'|-|E'| w∣V′∣−∣E′∣ 。
此时,选中一个点 x x x 可以提供权值 w − 1 2 deg ( x ) w-\frac{1}{2}\deg(x) w−21deg(x),而两个相邻的点如果只有一个被选中,就提供 − 1 2 -\frac{1}{2} −21 的贡献。由于分数太麻烦,我们又只判定正负,所以去掉 1 2 \frac{1}{2} 21,两个权值都 × 2 \times 2 ×2 。
不妨设 S S S 部中的点表示被选中,建出网络流图,即 x x x 向 T T T 连边,容量为 2 w − deg ( x ) 2w-\deg(x) 2w−deg(x),原图中有连边的两个点连双向边,容量为 1 1 1 。此时你一看:根本没有流量?
问题出在哪里?在于 2 w − deg ( x ) 2w-\deg(x) 2w−deg(x) 非正,不能作为流量。于是我们可以让 x x x 被选和不被选择的权值都增加 r r r,也就是 S S S 向 x x x 连边,容量为 r r r,但是 x x x 向 T T T 的连边容量变为 2 w − deg ( x ) + r 2w-\deg(x)+r 2w−deg(x)+r 。显然这会让我们希望的最大流(也就是有负容量的最小割)增加 n r nr nr,最后减去即可。取 r = m r=m r=m 就可以让边权变为正数。
一个细节
但是上面的做法是正确的吗?我们好像忽略了一点:网络流允许 S = { s } S=\{s\} S={s},也就是说,选出了 V ′ = ∅ V'=\varnothing V′=∅,这时候权值一定为 0 0 0 。太糟了。
但是利用我们 0 / 1 0/1 0/1 分数规划的知识:当 w w w 越大时,求出的最小割就越大(并且是严格递增的),唯一的零点就是答案。而 V ′ = ∅ V'=\varnothing V′=∅ 则相当于将函数值与 0 0 0 取较小值。那么显然零点就变成了原本的零点与其右侧的所有点。问题转化为,求出最靠左的零点。
那么,如果当前函数值(检测值)是零,说明答案应当不超过该值;如果函数值不为零,则答案应该大于该值。
另一个细节
那么二分到何时截止呢?其实很容易发现,任意两个子图的密度差不小于 1 n 2 \frac{1}{n^2} n21,除非密度相等。证明是极其简单的,分数作差时,差值的分母最大是两个分母的乘积即 n 2 n^2 n2,分子至少为 1 1 1 嘛。
所以答案的范围大约在 O ( 1 n 2 ) \mathcal O(\frac{1}{n^2}) O(n21) 级别时,就可以停止了。
代码实现
略去了网络流的部分,也略去了数据读入部分。使用链式前向星存图,边从 0 0 0 开始存储,故 h e a d = − 1 head=-1 head=−1 为终止。
int deg[MAXN], n, m;
bool check(double w){
cntEdge = m<<1; // restore
head[n+1] = head[n+2] = -1;
for(int i=1; i<=n; ++i)
while(head[i] >= cntEdge)
head[i] = e[head[i]].nxt;
for(int i=1; i<=n; ++i){
for(int j=head[i]; ~j; j=e[j].nxt)
e[j].val = 1; // restore
addEdge(n+1,i,m); // source
addEdge(i,n+2,2*w-deg[i]+m);
}
return dinic(n+1,n+2) >= n*m;
}
int main(){
const double GAP = 0.05/n/n;
double ansl = 0, ansr = 50;
while(ansl+GAP < ansr)
if(check((ansl+ansr)/2) == false)
ansl = (ansl+ansr)/2;
else ansr = (ansl+ansr)/2;
}