@bzoj - 2654@ tree
@description@
给你一个无向带权连通图,每条边是黑色或白色。让你求一棵最小权的恰好有 need 条白色边的生成树。
题目保证有解。
input
第一行V, E, need分别表示点数,边数和需要的白色边数。
接下来 E 行,每行 s, t, c, col 表示这边的端点(点从0开始标号),边权,颜色(0白色,1黑色)。
output
一行表示所求生成树的边权和。
V <= 50000, E<=100000, 所有数据边权为[1, 100]中的正整数。
sample Input
2 2 1
0 1 1 1
0 1 2 0
sample output
2
@solution@
dp 凸优化的起源……尽管这道题并不是 dp……
事实上这个算法有三个名字:WQS 二分,dp 凸优化,带权二分。
大家可以去看看这一位 dalao 的博客。
@part - 1@
一个直观的想法是,我们先用 K 条白边生成一棵最小生成树,再把黑边加进去。
然而好像是能被卡掉的。所以我们不能这么搞。
那怎么去限制白边数量呢?我们给每条白边二分一个附加权值,再生成最小生成树,求出白边数量调整二分。
可以从直观上感性理解:假如这个附加权值越大,则我们选取的白边会越少,反之会越大。因此附加权值与白边数量的关系是单调的,我们就可以玩二分了。
最后从这个生成树中减去附加权值即可。
@part - 2@
然而这个算法……看起来并不是很严谨啊。
会不会出现一种情况:二分的附加权值稍稍大一点白边数量就不够,二分的附加权值稍稍小一点白边数量又多出来了。总之就是二分不到题目说的 K 条。
以及:这个附加权值可以为小数吗?我二分需要管它的精度问题吗?
好的。我们慢慢来。
先定义一个函数 \(f(x)\) 表示当我选取 \(x\) 条白边的时候,求得的最小生成树权值。
经过 dalao 们的打表严格证明,我们可以得到 \(f(x)\) 是下凸函数。
并且,我们也可以求出 \(f(x)\) 的最小值(原图中的最小生成树)。
好,我们再来看我们刚刚的算法干了什么。我们二分了一个附加权值 \(k\),并把所有白边都加上了这个附加权值,得到一个新函数 \(g(x) = f(x)+k*x\)。
然后我们找到这个新函数的最小值(新图中的最小生成树)。求出它对应的横坐标(生成树所含的白边数量),并以此调整二分。
上面我所提到的那位 dalao 提出了一种理解该算法的方法:我们对 \(f(x)\) 与 \(g(x)\) 分别求导,得到 \(f'(x)\) 与 \(g'(x)\)。
因为 \(f(x)\) 是下凸的,所以 \(f'(x)\) 是单增的,同时 \(f'(x)\) 的零点对应着 \(f(x)\) 的最小值。
可以得到 \(g'(x) = f'(x) + k\),即将 \(f'(x)\) 平移 \(k\) 个单位。这样就可以说明该算法的正确性了。
同时,因为 \(f(x)\) 上的点都是整数,所以 \(f'(x)\) 上的点也应该都是整数,所以我们不需要进行小数二分。
@accepted code@
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int MAXN = 50000;
struct edge{
int u, v, w;
edge(int _u=0, int _v=0, int _w=0):u(_u), v(_v), w(_w){}
};
bool operator < (edge a, edge b) {
return a.w < b.w;
}
vector<edge>edges[2];
void init() {
for(int i=0;i<2;i++)
edges[i].clear();
}
int fa[MAXN + 5];
int find(int x) {
return (fa[x] == x) ? x : fa[x] = find(fa[x]);
}
int N, M, K, ans, cnt, p, q;
bool check(int x) {
ans = cnt = p = q = 0;
for(int i=1;i<=N;i++)
fa[i] = i;
while( p < edges[0].size() && q < edges[1].size() ) {
if( find(edges[0][p].u) == find(edges[0][p].v) )
p++;
else if( find(edges[1][q].u) == find(edges[1][q].v) )
q++;
else {
if( edges[0][p].w + x < edges[1][q].w ) {
ans += edges[0][p].w + x;
fa[find(edges[0][p].u)] = find(edges[0][p].v);
cnt++; p++;
}
else {
ans += edges[1][q].w;
fa[find(edges[1][q].u)] = find(edges[1][q].v);
q++;
}
}
}
while( p < edges[0].size() ) {
if( find(edges[0][p].u) == find(edges[0][p].v) )
p++;
else {
ans += edges[0][p].w + x;
fa[find(edges[0][p].u)] = find(edges[0][p].v);
cnt++; p++;
}
}
while( q < edges[1].size() ) {
if( find(edges[1][q].u) == find(edges[1][q].v) )
q++;
else {
ans += edges[1][q].w;
fa[find(edges[1][q].u)] = find(edges[1][q].v);
q++;
}
}
return cnt <= K;
}
int main() {
init();
scanf("%d%d%d", &N, &M, &K);
for(int i=1;i<=M;i++) {
int a, b, c, x;
scanf("%d%d%d%d", &a, &b, &c, &x);
edges[x].push_back(edge(a + 1, b + 1, c));
}
for(int i=0;i<2;i++)
sort(edges[i].begin(), edges[i].end());
int le = -100, ri = 100;
while( le < ri ) {
int mid = (le + ri) >> 1;
if( check(mid) ) ri = mid;
else le = mid + 1;
}
check(le);
printf("%d\n", ans - K*le);
}
@details@
事实证明,这道题 bzoj 的数据非常的水。
我带权二分的时候连那个二分出来的附加权值都没有加到边权里去。
然后在 bzoj 上它竟然 AC 了?
建议大家如果要检测自己代码的正确性,去 hdu4253 (需要写多组数据)。
否则你连自己代码有错都不知道,一懵一懵地好像自己就 AC 了。