ICPC20南京D-贪心结论+大模拟

传送门

先随便找一棵生成树。假如这棵树不存在度数大于n/2的点,已经做完。否则这样的点一定只有1个(由树的度数之和易证)。那么我们就以它为根,记为rt,调整到rt符合要求为止。

于是有一个不是很难想到的贪心调整方案:每次找一条不在树上但在图上的边(u,v),若连接以后构成的环包含rt,则对于rt的两个在环上的邻居(注:必定是2个),记为fufv,先尝试删除(rt,fu),如果这样导致uv的度数不合题意,那么尝试删除(rt,fv),如果还不合题意,就放弃修改。有一种能成功,就直接采纳了,继续看下一条边(可以理解为,(rt,fu)是正宫,(rt,fv)是备胎)。做完整个过程,再看一下得到的树是否符合题意。

这个贪心调整方案挺佛系的,当前的边做不到,也不会尝试挽回,直接放弃了。它为什么正确呢?不管是官方题解还是网上的题解,我都没有找到证明。反正我不会证,不管啦

思想很简单,但实现起来很麻烦,这也是为什么当时只有10个队过。所以下面说一下我的实现思路。

  • 我们用inTr[idx]数组来表示下标为idx的边是否被选进生成树里。于是我们还需要一个map映射mp[{u,v}],从点对(u,v)获取它对应的边的下标。这样才能完成生成树的边的替换的过程。
  • 怎么求rt是否在环里?我们不妨考虑把rt到它的邻居的边都删掉,这样就形成若干棵子树。u和v在不同的子树里,等价于rt在环里。怎样判定u和v在不同的子树?最简单的实现方案是并查集。我们rt的邻居都为连通块的代表元即可。这个建并查集的过程有点麻烦,所以我写了函数rebuild_ufdfs
  • 恰好建生成树的最简单方案(kruskal)也要用到并查集,所以我们直接复用了同一段空间。使用前记得先清空。

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int N = 1e5 + 5,M = 2e5 + 5;

int n,m,fa[N],deg[N];
vector<int> G[N];//仅用于重建并查集
vector<pair<int,int> > e;
bool inTr[M];

void dbg(){puts("");}
template<typename T, typename... R>void dbg(const T &f, const R &... r) {
    cout << f << " ";
    dbg(r...);
}
template<typename Type>inline void read(Type &xx){
    Type f = 1;char ch;xx = 0;
    for(ch = getchar();ch < '0' || ch > '9';ch = getchar()) if(ch == '-') f = -1;
    for(;ch >= '0' && ch <= '9';ch = getchar()) xx = xx * 10 + ch - '0';
    xx *= f;
}

int find(int x){return x == fa[x] ? x : fa[x] = find(fa[x]);}

void init_uf(){
    rep(i,1,n) fa[i] = i;
}

void init(){
    re_(i,0,m) inTr[i] = false;
    rep(i,1,n) deg[i] = 0,G[i].clear();
    init_uf();
    e.clear();
}

void dfs(int u,int ufa = 0){
    for(int v: G[u]){
        if(v == ufa) continue;
        if(ufa) fa[v] = u;
        dfs(v,u);
    }
}

//重建并查集,必须保证每个连通块的代表元都是rt的邻居!
void rebuild_uf(int rt){
    init_uf();
    re_(i,0,m){
        if(!inTr[i]) continue;
        int u = e[i].first,v = e[i].second;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs(rt);
}

bool solve(){
    read(n);read(m);
    init();
    map<pair<int,int>,int> mp;
    re_(i,0,m){
        int u,v;read(u);read(v);
        if(u == v || mp.count({u,v})) continue;
        mp[{u,v}] = mp[{v,u}] = i;
        e.push_back({u,v});
    }
    mp.clear();
    m = e.size();
    re_(i,0,m){
        int u = e[i].first,v = e[i].second;
        mp[{u,v}] = mp[{v,u}] = i;
    }
    re_(i,0,m){
        int u = e[i].first,v = e[i].second;
        int fu = find(u),fv = find(v);
        if(fu == fv) continue;
        fa[fu] = fv;
        deg[u]++;deg[v]++;
        inTr[i] = true;
    }
    // rep(i,1,n) if(find(1) != find(i)) return false;//生成树不存在
    int rt = -1;
    rep(i,1,n) if(deg[i] > n/2){
        rt = i;break;
    }
    // dbg("??",rt);//
    if(rt == -1) return true;
    rebuild_uf(rt);
    re_(i,0,m){
        if(inTr[i]) continue;
        //非生成树的边
        int u = e[i].first,v = e[i].second;
        if(u == rt || v == rt) continue;//是根,则此边无贡献,跳过
        int fu = find(u),fv = find(v);
        if(fu == fv) continue;//同一棵子树,则跳过
        deg[u]++;deg[v]++;
        deg[rt]--;deg[fu]--;
        int idx1 = mp[{rt,fu}];
        inTr[idx1] = false;
        inTr[i] = true;
        if(deg[u] > n/2 || deg[v] > n/2){
            deg[fu]++;deg[fv]--;//更换策略
            int idx2 = mp[{rt,fv}];
            inTr[idx1] = true;
            inTr[idx2] = false;
            if(deg[u] > n/2 || deg[v] > n/2){
                deg[u]--;deg[v]--;deg[rt]++;deg[fv]++;//彻底放弃
                inTr[i] = false;
                inTr[idx2] = true;
                continue;
            }
            else{
                fa[fv] = fu;//两棵子树变为一棵
                if(deg[rt] <= n/2) return true;
            }
        }
        else{
            fa[fu] = fv;//两棵子树变为一棵
            if(deg[rt] <= n/2) return true;
        }
    }
    rep(i,1,n) if(deg[i] > n/2) return false;
    return true;
}

int main(int argc, char** argv) {
    int T;read(T);
    while(T--){
        bool fl = solve();
        puts(fl ? "Yes" : "No");
        if(fl) re_(i,0,m) if(inTr[i]) printf("%d %d\n",e[i].first,e[i].second);
    }
    return 0;
}

这题需要重建生成树的情况的测试数据,一开始我认为无法自行构造出来,后来偶然发现4个点的完全图恰好可以作为本题的测试数据

111
4 6
1 2
1 3
1 4
2 3
2 4
3 4
6 9
1 2
1 3
1 4
2 3
2 4
3 4
4 5
4 6
4 6
3 4
1 3
2 3
3 3
1 2
//这个是偶然发现的可以作为测试数据的(没找到的时候,只能肉眼查错……)
Yes
1 3
1 4
2 3
//后两个是样例
Yes
1 2
1 3
1 4
4 5
4 6
No
上一篇:1553B总线基础知识及扩展


下一篇:单链表直接插入排序