题目
思路
A t C o d e r AtCoder AtCoder 是不是很喜欢玩这种重新染色的套路?
从树的情况开始做起。发现操作同色点不太好,转为操作异色点——只需要将深度为奇数的点的颜色翻转。
考虑黑点和白点交换颜色,实际上就是 白点在移动,白点没覆盖则为黑点。而目标是将所有点的颜色翻转,即原来是黑点的——下文称其为坑——将会有一个白点填进来。
两个白点之间不能操作。然而这并不代表两个白点之间不能互相跃过,因为两种情况代价相同,即 a − b − o a-b-o a−b−o 变为 a − o − b a-o-b a−o−b 再变为 o − a − b o-a-b o−a−b 是两步,其中 a , b a,b a,b 是白色球;而 a − b − o a-b-o a−b−o 直接走到 o − b − a o-b-a o−b−a 也是两步,效果相同。
所以问题变为匹配白点和坑,使得走过的总距离最小。可以计算每一条边的贡献,必须经过这条边的点即某一边点的数量与坑的数量差的绝对值。
这样的方案肯定可以构造出来——找到一个白点,其旁边必然有一条边有它提供的贡献,走过去即可。
考虑基环树。如果环是一个偶环,那么黑白染色仍然可行,也就是说,现在白点可以在一个环上移动了。而树的部分的最优操作显然仍然与上面相同,进行完这些操作,相当于根节点(环上点)有若干个白点,或者缺少若干个白点。
原来这就是 传递糖果。 即,设一号点从
k
k
k 号点得到了
x
x
x 个点(假定
x
x
x 可以为负,表示给出),那么一号点就会给出
x
+
c
1
x+c_1
x+c1,这里
c
c
c 为原本拥有的白点(可以为负,表示需要),同理二号点需要给出
x
+
c
1
+
c
2
x+c_1+c_2
x+c1+c2,那么总传递量是
∑
i
=
1
k
∣
x
+
∑
j
=
1
i
−
1
c
j
∣
\sum_{i=1}^{k}\left|x+\sum_{j=1}^{i-1}c_j\right|
i=1∑k∣∣∣∣∣x+j=1∑i−1cj∣∣∣∣∣
也就是
x
x
x 与每一个
c
c
c 的前缀和相加,求绝对值后相加。放在数轴上很容易发现,需要取
−
x
-x
−x 为中位数。
如果环是一个奇环呢?那么有一条边就仍然保持着 “同色才可操作” 的性质。不妨设白点比坑数量更多,则这一条边需要用来将白色删去。
注意到 白点的移动是没有顺序要求的,所以 “同时存在白球于顶点处,将其删去”,等同于 “分别存在过若干白球,将其删去”,也就是端点处额外需要若干个白球。将其直接加进 c c c 中,则问题变成了序列上移动,显然序列也是树的一种。
这样一来,我们对三种情况进行了讨论,并得到了一个 O ( n ) \mathcal O(n) O(n) 的做法。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long int_;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+int(c^48);
return a*int(f);
}
inline void writeint(int x){
if(x < 0) putchar('-'), x = -x;
if(x > 9) writeint(x/10);
putchar(char((x-x/10*10)^48));
}
inline int ABS(const int &x){
return x < 0 ? -x : x;
}
const int MaxN = 100005;
struct Edge{
int to, nxt;
Edge(int T=0,int N=0){
to = T, nxt = N;
}
};
Edge e[MaxN<<1];
int head[MaxN], cntEdge;
void addEdge(int a,int b){
e[cntEdge] = Edge(b,head[a]);
head[a] = cntEdge ++;
}
int deg[MaxN];
void topo(int x){
deg[x] = 0;
for(int i=head[x]; ~i; i=e[i].nxt){
-- deg[e[i].to];
if(deg[e[i].to] == 1)
topo(e[i].to);
}
}
int cnt[MaxN], sgn[MaxN];
int_ ans;
void scanTree(int x,int pre){
cnt[x] = sgn[x];
for(int i=head[x]; ~i; i=e[i].nxt){
if(e[i].to == pre) continue;
if(deg[e[i].to] > 0) continue;
sgn[e[i].to] = -sgn[x];
scanTree(e[i].to,x);
ans += ABS(cnt[e[i].to]);
cnt[x] += cnt[e[i].to];
}
}
int sta[MaxN], top;
void traceCycle(int x){
const int from = x;
sta[top = 1] = from;
for(int i=head[x]; ~i; i=e[i].nxt)
if(deg[e[i].to] > 0){
x = e[i].to; break;
}
int pre = from; // not go back
while(x != from){
sta[++ top] = x;
for(int i=head[x]; ~i; i=e[i].nxt)
if(e[i].to != pre && deg[e[i].to] > 0){
pre = x; x = e[i].to; break;
}
}
}
int sum[MaxN]; // prefix sum of cnt
int main(){
int n = readint(), m = readint();
memset(head+1,-1,n<<2);
for(int i=1,a,b; i<=m; ++i){
a = readint(), b = readint();
addEdge(a,b), addEdge(b,a);
++ deg[a], ++ deg[b];
}
if(m == n-1){ // Tree
memset(deg+1,0,n<<2);
sgn[1] = 1; scanTree(1,0);
if(cnt[1]) puts("-1");
else printf("%lld\n",ans);
return 0; // done
}
rep(i,1,n) if(deg[i] == 1) topo(i);
rep(i,1,n) if(deg[i] > 0){
traceCycle(i); break;
}
sgn[sta[0] = 0] = -1;
int all = 0; // all black-white
for(int i=1; i<=top; ++i){
sgn[sta[i]] = -sgn[sta[i-1]];
scanTree(sta[i],0);
all += cnt[sta[i]];
}
if((all&1) || (all && !(top&1)))
return puts("-1")*0;
cnt[sta[1]] -= (all>>1);
cnt[sta[top]] -= (all>>1);
ans += ABS(all>>1); // delete or make
rep(i,1,top){
if(top&1) ans += ABS(sum[i-1]);
sum[i] = sum[i-1]+cnt[sta[i]];
}
if(!(top&1)){
nth_element(sum+1,sum+(top+1)/2,sum+top+1);
int mid = sum[(top+1)>>1]; // median
rep(i,1,top) ans += ABS(mid-sum[i]);
}
printf("%lld\n",ans);
return 0;
}
后记
当我打完树的部分,测样例,过了。
我又去打环的部分,结果忘记让树的处理将 d e g deg deg 数组清空,却 一直没有回头重新将所有样例测一遍。
最后只得了环的分。另外在这里留个坑:本场考试的次日,我又犯了同样的错误。