题解 CF125E 【MST Company】
这里主要讲下 wqs 二分没法正好二分出需要的最小生成树方案时该如何构造方案
解析
题意以及前面基本的 wqs 二分部分就直接略过了
考虑为什么我们无法直接二分出需要的最小生成树方案:此时函数的最小值实际上不再是一个点,而是一段区间,设其为
这实际上就代表,在当前的图上(所有与
接着我们直接给出(也可以认为由生成树的性质而启发得到的...)构造的方法:
(下面为了方便,均将 “与
设要求构造的方案需要
在做 Kruskal 时(尚不清楚是否有基于 prim 的构造方法...),我们记录已经选择的特殊边的数量
一种方案是:在做每一组时,先尝试使用特殊边,如果可行,且
另一种方案是:先找出一种选择特殊边最少的最小生成树方案
最后考虑证明这种构造方式
事实上,我们可以发现这种构造方式和原来的 Kruskal 算法是没有本质区别的...;且目标的生成树方案已经保证存在,因此该构造方式一定有解
CODE
代码里可能显式或隐式地将 与
另外代码里用的是后一种构造方式
仅供参考,有些地方的实现可能很烂
#include <cstdio>
#include <algorithm>
using std::sort;
const int MAXN =5e3+20, MAXM =1e5+20;
/*------------------------------Disjoint Set------------------------------*/
int fa[MAXN], rank[MAXN];
int get_root(const int &x){
if(fa[x] == x)
return x;
else
return fa[x] =get_root(fa[x]);
}
inline void merge(int x, int y){
x =get_root(x), y =get_root(y);
if(rank[x] == rank[y])
++rank[x];
else if(rank[x] < rank[y])
rank[y] ^=rank[x] ^=rank[y] ^=rank[x];
fa[y] =x;
}
void clear_disjointSet(const int &n){
for(int i =1; i <= n; ++i)
fa[i] =i, rank[i] =0;
}
/*------------------------------Kruskal------------------------------*/
struct edge{
int u, v, w, id;
bool col;
}e_b[MAXM], e_w[MAXM], e_qaq[MAXM];
int totb, totw, tot;
bool cmp(const edge &A, const edge &B){
return A.w < B.w;
}
int edge_chosen_id[MAXN];
#define e e_qaq
int debug_val;/*最小生成树权*/
int calc(const int &n, const int &C){
debug_val =0;
tot =0;
for(int i =0, j =0; i < totb || j < totw;){
if(i < totb && j < totw){
if(e_b[i].w <= e_w[j].w+C)/*优先黑边 -> 尽可能少选白边 ( -> 最小值区间左端点 )*/
e[tot++] =e_b[i++];
else
e[tot] =e_w[j++], e[tot++].w +=C;
}
else if(i < totb)
e[tot++] =e_b[i++];
else
e[tot] =e_w[j++], e[tot++].w +=C;
}
clear_disjointSet(n);
int cnt =0, ret =0;
for(int i =0; cnt < n-1 && i < tot; ++i)
if(get_root(e[i].u) != get_root(e[i].v)){
debug_val +=e[i].w;
if(e[i].col == 0)
++ret;
merge(e[i].u, e[i].v);
edge_chosen_id[cnt] =e[i].id;
++cnt;
}
return ret;
}
#undef e
/*------------------------------Wrok------------------------------*/
bool check_illegal(const int &n, const int &k){/*构造生成树时尽可能地不选白边*/
clear_disjointSet(n);
int cnt =0, cnt_w =0;
for(int i =0; i < totb && cnt < n-1; ++i)
if(get_root(e_b[i].u) != get_root(e_b[i].v)){
merge(e_b[i].u, e_b[i].v);
++cnt;
}
for(int i =0; i < totw && cnt < n-1; ++i)
if(get_root(e_w[i].u) != get_root(e_w[i].v)){
merge(e_w[i].u, e_w[i].v);
++cnt_w;
++cnt;
}
return (k < cnt_w || cnt != n-1);
}
edge e_backup[MAXM];
int tmp_backup, C_backup, debug_val_backup;
int cnt_l[MAXM], cnt_r[MAXM], tot_cnt;
bool e_chosen[MAXM];/*标记每种不同的边权中,选最少白边方案 ( 可能是多种方案中的一种 ) 中的白边*/
int e_ans[MAXN], tote_ans;
#define e e_backup
/*->>这里和 wqs 二分基本无关*/
/*解应当一定存在*/
void find_a_solution(const int &n, const int &m, const int &k){
for(int i =0; i < m; ++i){
if(i == 0){
cnt_l[0] =0;
++tot_cnt;
}
else if(e[i].w != e[i-1].w){
cnt_r[tot_cnt-1] =i-1;
cnt_l[tot_cnt] =i;
++tot_cnt;
}
if(i == m-1)
cnt_r[tot_cnt-1] =m-1;
}
/*找出一种最小生成树方案,使得白边最少*/
clear_disjointSet(n);
int cnt =0;
for(int i =0; cnt < n-1; ++i){
if(get_root(e[i].u) != get_root(e[i].v)){
if(e[i].col == 0)
e_chosen[i] =1;
merge(e[i].u, e[i].v);
++cnt;
}
++i;
}
clear_disjointSet(n);
cnt =0;
int delta_w =k-tmp_backup;/*还差多少白边*/
for(int chunk =0; chunk < tot_cnt && cnt < n-1; ++chunk){/*注意只有同边权的边之间会 " 替换 " ( 影响 )*/
for(int i =cnt_l[chunk]; i <= cnt_r[chunk]; ++i)/*先选 " 白边最少的方案 " 中的白边*/
if(e_chosen[i]){
merge(e[i].u, e[i].v);
e_ans[tote_ans++] =e[i].id;
++cnt;
}
if(delta_w > 0)/*还需要多选一些白边*/
for(int i =cnt_l[chunk]; delta_w > 0 && i <= cnt_r[chunk]; ++i)
if(!e_chosen[i] && e[i].col == 0){
if(get_root(e[i].u) != get_root(e[i].v)){
merge(e[i].u, e[i].v);
e_ans[tote_ans++] =e[i].id;
--delta_w;
++cnt;
}
}
for(int i =cnt_l[chunk]; i <= cnt_r[chunk]; ++i)/*最后选黑边*/
if(e[i].col == 1){
if(get_root(e[i].u) != get_root(e[i].v)){
merge(e[i].u, e[i].v);
e_ans[tote_ans++] =e[i].id;
++cnt;
}
}
}
}
#undef e
/*------------------------------Main------------------------------*/
int read(){
int x =0; char c =getchar();
while(c < '0' || c > '9') c =getchar();
while(c >= '0' && c <= '9') x =(x<<1)+(x<<3)+(48^c), c =getchar();
return x;
}
int main(){
int n =read(), m =read(), k =read();
for(int i =0; i < m; ++i){
int s =read(), t =read(), c =read();
if(s == 1 || t == 1){
e_w[totw].u =s;
e_w[totw].v =t;
e_w[totw].w =c;
e_w[totw].col =0;
e_w[totw].id =i+1;
++totw;
}
else{
e_b[totb].u =s;
e_b[totb].v =t;
e_b[totb].w =c;
e_b[totb].col =1;
e_b[totb].id =i+1;
++totb;
}
}
sort(e_b, e_b+totb, cmp);
sort(e_w, e_w+totw, cmp);
/*检查 k 是否合法 ( 函数取值在 k 是否不存在 )*/
if(k > n-1 || k > totw){/*上界检查 - 边不足*/
puts("-1");
return 0;
}
else if(check_illegal(n, k)){/*下界检查 - 最少可选的白边过多;以及顺便检查是否联通*/
puts("-1");
return 0;
}
int l =-5e8, r =5e8;
while(l <= r){/*wqs*/
int mid =(l+r)>>1;
int tmp =calc(n, mid);
if(tmp < k){
debug_val_backup =debug_val;
tmp_backup =tmp;
C_backup =mid;
for(int i =0; i < tot; ++i)
e_backup[i] =e_qaq[i];
}
if(tmp < k)
r =mid-1;
else if(tmp > k)
l =mid+1;
else
break;
}
if(l <= r){/*直接找到了答案*/
printf("%d\n", n-1);
for(int i =0; i < n-1; ++i)
printf("%d ", edge_chosen_id[i]);
return 0;
}
else{/*需要构造方案*/
find_a_solution(n, m, k);
// printf("%d\n", debug_val_backup-C_backup*k);/*最小生成树权*/
printf("%d\n", n-1);
for(int j =0; j < n-1; ++j)
printf("%d ", e_ans[j]);
}
}
一些话
原本是打算好好证明下这种构造方式的正确性的...
结果写着写着发现与其写这么一大段还不如直接归约到 Kruskal 上...(当然也没写完,因为时间等等原因 \fad)
如果有人想继续探索这种构造方案的性质的话,这里就放几个之前写在正文的引理:
引理
证明只需考虑反证法即可(以下证明搬自维基):
引理
只需套用证明 引理
引理
(“替换” 这个词可能有点不严谨...这样写感觉比较简洁,见谅X)
证明很显然。否则 “替换” 后的方案就不再是生成树了
希望有所帮助X