先随便找一棵生成树。假如这棵树不存在度数大于n/2
的点,已经做完。否则这样的点一定只有1个(由树的度数之和易证)。那么我们就以它为根,记为rt
,调整到rt
符合要求为止。
于是有一个不是很难想到的贪心调整方案:每次找一条不在树上但在图上的边(u,v)
,若连接以后构成的环包含rt
,则对于rt
的两个在环上的邻居(注:必定是2个),记为fu
和fv
,先尝试删除(rt,fu)
,如果这样导致u
或v
的度数不合题意,那么尝试删除(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_uf
和dfs
。 - 恰好建生成树的最简单方案(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