紫丁香 题解
前言
来自一场 noip \text{noip} noip 提高模拟赛的题目。
题目描述
有 n n n 点 m m m 边的 简单无向连通图,点编号为 0 ∼ n − 1 0\sim n-1 0∼n−1,要求删掉若干条边,最大化奇数度点的个数。
求:能得到最大答案的构造,用 m m m 长的 01 01 01 串表示, 1 1 1 表示保留该边, 0 0 0 表示不保留,输出字典序最大的方案。
思路
建树
首先,我们可以在图中建出一棵树。
由于最后要输出字典序最大,所以我们要尽可能地删后面的边。
所以可以用按秩(按秩序)并查集建出一棵树。注意,为什么要按秩?因为我们可以通过节点度数大小按秩建树,这样有利于树的深度不会太深。
code
int find(int x) {
// note:不能使用路径压缩
return fa[x] == x ? x : find(fa[x]);
}
bool merge(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry)
return 0;
if (d[rx] > d[ry])
swap(rx, ry);
Fa[++tot] = { rx, ry };
d[ry] += d[rx];
fa[rx] = ry;
return 1;
}
signed main() {
for (int i = m; i >= 1; i--)
chose[i] = merge(E[i].ft, E[i].sd);
}
遍历边
然后一条一条边遍历,如果非树边,直接记录一下该边不用删,然后从自己已知爬到祖先,让每个度 ++
。所以之前为什么要按秩并查集建树呢?——就是因为这里
f
a
i
fa_i
fai 必须记录自己的父亲,不可以压缩路径记录祖先,时间复杂度会
T
\text{T}
T,所以要是树的深度尽可能小。
code
void change(int x) {
while (x) {
d[x]++;
if (x == fa[x])
break;
x = fa[x];
}
}
signed main() {
for (int i = 1; i <= m; i++) {
if (chose[i] == 0) {
ans[i] = 1;
change(E[i].ft), change(E[i].sd);
}
}
}
这是为了将那些之前没有增加进树的度的点更新度的状态 [ ∗ ] ^{[*]} [∗] (注释见结尾)。
如果遍历到了树边 ( u , v ) ∈ E (u,v)\in E (u,v)∈E,考虑要不要保留。如果要保留,那把之前不连通时,各自的祖先 ( r o o t u , r o o t v ) (root_u,root_v) (rootu,rootv) 拿出来,断边。
再看这俩祖先的度有没有成为奇数。
如果任意一个是奇数,那就可以保留 ( u , v ) (u,v) (u,v) 该边,并且做更新度状态的操作(同上)。否则没有必要保留,先前断掉的 ( r o o t u , r o o t v ) (root_u,root_v) (rootu,rootv) 也不需要再次建边。
code
bool split(int x, int y) {
int rx = Fa[tot].ft, ry = Fa[tot].sd;
tot--;
d[ry] -= d[rx];
fa[rx] = rx;
if ((d[rx] & 1) || (d[ry] & 1)) {
change(x);
change(y);
return 1;
}
return 0;
}
signed main() {
for (int i = 1; i <= m; i++) {
if (chose[i])
ans[i] = split(E[i].ft, E[i].sd);
}
}
通过一系列操作,就可以得到哪些边要删,哪些边不用删了。
完整代码
code
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define pii pair<int, int>
#define ft first
#define sd second
const int MAXN = 6e5 + 5, MAXM = 9e5 + 5;
int n, m;
int tot;
int fa[MAXN], d[MAXN];
pair<int, int> E[MAXM], Fa[MAXM];
bool ans[MAXM], chose[MAXM];
int find(int x) {
// note:不能使用路径压缩
return fa[x] == x ? x : find(fa[x]);
}
bool merge(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry)
return 0;
if (d[rx] > d[ry])
swap(rx, ry);
Fa[++tot] = { rx, ry };
d[ry] += d[rx];
fa[rx] = ry;
return 1;
}
void change(int x) {
while (x) {
d[x]++;
if (x == fa[x])
break;
x = fa[x];
}
}
bool split(int x, int y) {
int rx = Fa[tot].ft, ry = Fa[tot].sd;
tot--;
d[ry] -= d[rx];
fa[rx] = rx;
if ((d[rx] & 1) || (d[ry] & 1)) {
change(x);
change(y);
return 1;
}
return 0;
}
signed main() {
scanf("%lld%lld", &n, &m);
for (int i = 1; i <= n; i++) fa[i] = i, d[i] = 1;
for (int i = 1, u, v; i <= m; i++) {
scanf("%lld%lld", &u, &v);
u++, v++;
E[i] = { u, v };
}
for (int i = m; i >= 1; i--) chose[i] = merge(E[i].ft, E[i].sd);
for (int i = 1; i <= m; i++) {
if (chose[i])
ans[i] = split(E[i].ft, E[i].sd);
else {
ans[i] = 1;
change(E[i].ft), change(E[i].sd);
}
}
for (int i = 1; i <= m; i++) putchar(ans[i] ? '1' : '0');
return 0;
}
结尾
[
∗
]
^{[*]}
[∗]:个人认为最难点在于 change
函数。为什么要从
u
→
r
o
o
t
u
u\to root_u
u→rootu 的路径上所有点都要度 ++
?一说,为了该边路径上每个点的奇偶性(好像不太对)。
现在我还没找到最准确的答案,现在属于似懂非懂。如果你想到了,欢迎评论交流。