Atcoder Grand Contest 004(A~F)
这场半 VP 做的,就不分赛时赛后写了,直接放每道题的解法。
A - Divide a Cuboid
当某一维的长度为偶数的时候,显然可以在这一维的中间切,两部分方块的最小差为 \(0\)。
当每一维的长度为奇数的时候,设该立方体的大小为 \(n\times m\times f\),则假定我们要切 \(n\) 这一维,我们显然会分给一部分 \(\left\lfloor\frac{n}{2}\right\rfloor\),分给另一部分 \(\left\lceil\frac{n}{2}\right\rceil\)。那么两部分的最小差就是 \(m\times f\)。
于是我们比较 \(n,m,f\) 两两乘积的大小,输出最小的一个乘积即可。
B - Colorful Slimes
当执行操作 \(2\) 的次数为 \(x\) 次时,对于位置 \(i\),贪心地来讲,我们只会 \(a_i\) 及其前面 \(x\) 个 \(a\) 中的最小值作为第 \(i\) 个位置对于答案的贡献。
而有意义的 \(x\) 是满足 \(0\le x<n\) 的,所以我们枚举每一个 \(x\),动态更新每一个 \(a_i\) 当前对于答案的贡献,求和取最小值即可。
时间复杂度 \(O(n^2)\)。
C - AND Grid
观察到一个特殊的条件,边缘的一周是一定不会被涂成紫色的。
所以我们不妨让一种颜色涂满整个边缘,并对于每一个紫色连通块选出一个代表方格 \(a\),并从 \(a\) 开始向上向下拉一条竖线,如果拉的过程中与其它连通块相交就停下,否则直到拉到边缘位置再停下。此时我们就得到了一个颜色的合法涂色。
然后我们将非紫色的连通块 是否涂色 的状态取反,即为另一个颜色的涂色情况。
可以证明这样涂色一定是合法的。时间复杂度 \(O(n^2)\)。
D - Teleporter
首先注意到如果 \(k\) 步之后每一个节点都会积存在根节点,根本原理应该是根节点指向自己,否则深度不同的两个节点最终所在位置一定不同。
那么如果根节点没有指向自己,我们就需要花费一个贡献把它掰回来,然后考虑其它节点。
考虑一种很经典的 trick。我们将所有节点按照深度扔进一个堆中,每次从堆里取出当前深度最大且没有被标记的节点,设根节点深度为 \(0\),如果此时它的深度大于 \(k\),即无法从它出发在 \(k\) 步之内到达根节点,就从它开始向上遍历到它的 \(k-1\) 级祖先,并从每一个祖先出发把儿子中所有未标记的节点进行标记。相当于我们将这棵子树花费一个代价接到根节点上。
一直执行类似操作直到堆空即可。时间复杂度 \(O(n\log n)\).
E - Salvage Robots
从 E 开始难度就上来了,题目也有趣了起来。
首先很容易想到的一点是,机器人太多了,而终点只有一个,所以为了缩减信息,我们不妨将【移动机器人】看作等价的【移动终点及其周围的网格】,再考虑怎么做。
考虑当终点向一个方向移动一步时,另一个方向就会有一列或一行机器人被削掉;当我们确定了向四个方向移动的最远距离的时候,这四个方向最远距离确定的矩形我们显然是都可以再走一遍的,因为全部走一遍不仅不会造成更多的机器人被削掉,还可以获得尽可能更多的贡献,显然不劣。
那么其实我们已经将这道题转化地有了动态规划的雏形。我们只关注当前向四个方向各自走的最远距离,所以不妨设 \(dp_{i,j,p,q}\) 表示从终点向上走 \(i\) 步,从终点向下走 \(j\) 步,从终点向左走 \(p\) 步,从终点向右走 \(q\) 步确定的矩形最多能获得的机器人个数。
转移其实也很清晰了。设终点的坐标为 \(x,y\),不妨考虑向上方多走一步,则此时我们增加的贡献应该是第 \(x-i-1\) 行的一段连续方格的机器人个数,设这个区间为 \([l,r]\),则 \(l\) 应该是我们向左走到的地方,与我们向右走导致左侧削掉的地方的较大值,\(r\) 同理,为我们向右走到的地方,与我们向左走导致右侧削掉的地方的较小值,即 \(l=\max(y-p,q+1),r=\min(y+q,m-p)\)。
其它四个方向的转移同理,转移过程中动态更新答案即可。总时间复杂度 \(O(n^4)\),常数很小。
好像卡空间,只能开 short 储存,求 max min 的时候需要转 int。
F - Namori
顶级思维题。
先从简单的来,考虑树怎么做。首先翻转同色节点显然是不好做的,这会产生很多复杂的情况,并不好处理。
考虑到树是个二分图,所以我们不妨对于树进行一遍黑白染色,将相邻的节点染成不同的颜色,此时我们的问题就变成每次翻转一对相邻的不同颜色节点,需要求出将所有颜色翻转,将黑色节点变成白色,白色节点变成黑色所需要的最少操作次数。那么当黑白节点个数不同的时候,一定无解。
那么每次操作其实就相当于是将一个黑色节点向一个方向挪动一格,我们不妨将这个转化后的问题看作是将黑色节点移动到白色节点上,则这个问题是好做的。
设 \(sum_i\) 为以 \(i\) 为根的子树内黑白节点个数之差,那么节点 \(i\) 对于答案的贡献就是 \(|sum_i|\)。
这意味着对于节点 \(i\),如果黑色节点比白色节点更少,则一定至少会从祖先运来 \(sum_i\) 个黑色节点;反之则一定会至少从这个节点运出 \(sum_i\) 个黑色节点。很容易证明这个【至少】的下界是可以取到的,所以总贡献就是 \(\sum_{i=1}^n sum_i\)。
于是树的问题就解决了。考虑基环树的情况怎么做。
如果这个环的长度是偶数,那么此时这棵基环树也是一个二分图。
依旧是先黑白染色,然后考虑将环上的某一条边作为 dfs 树的一条返祖边,考虑怎么将这条边 \((u,v)\) 拆掉。
设 \(u\) 会向祖先 \(v\) 运送 \(x\) 个黑色节点,则拆掉之后对于原树,从 \(u\) 到 \(v\) 的所有节点的子树中都会少 \(x\) 个黑色节点,则此时该操作会产生影响的贡献为 \(\sum_{p=u}^{p=fa_p}[p\in[v,u]] |sum_p-x|+x\)。
此时我们的目标是想让这个式子取得最小值,易证明当 \(x\) 为所有 \(sum_p\) 的中位数时,该式子的值最小。于是我们得到 \(x\) 的取值后再做一遍加和即可得到答案。
无解的情况依旧是黑白两色节点个数不同。
环长为奇数的情况就麻烦了些,此时染色过后环上会多出一对同色节点。对比在题目要求问题中的翻转条件,我们其实只有在这两个节点同色的时候才能翻转这两个节点的颜色,那么翻转一次就会多出两个黑色节点,或者少两个黑色节点。于是首先易得出当黑白两色节点的个数差为偶数的时候一定有解,否则无解。
不妨设当前整棵树黑白两色节点个数差为 \(s\),则其实这两个节点的唯一作用就是尽量使得 \(|s|\) 变小,因为变大一定是不优的。所以这条边会被使用的次数显然就是 \(\frac{s}{2}\) 次,且会使得这条边两侧的节点子树内黑白两色节点的差都减去 \(\frac{s}{2}\)。于是我们将这条边删去之后,对整棵树再进行一遍树的做法,并在处理到被删除边两侧节点时将差减去 \(\frac{s}{2}\) 即可。
放一下代码吧,感觉结合代码还是会更好理解些。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
int n,m,le,ri,a[N],fa[N],p[N],ptot,g,ans;
bool vis[N],col[N],flag,huan[N];
struct node{
int head[N],tot,to[N<<1],next[N<<1];
void adde(int u,int v){
to[++tot]=v,next[tot]=head[u],head[u]=tot;
}
}S;
ll read(){
ll w=0,f=1;
char c=getchar();
while (c>'9'||c<'0'){
if (c=='-') f=-1;
c=getchar();
}
while (c>='0'&&c<='9'){
w=(w<<3)+(w<<1)+(c^48);
c=getchar();
}
return w*f;
}
void dfs(int u){
vis[u]=1;
col[u]=col[fa[u]]^1;
if (col[u]==1) a[u]=1;
else a[u]=-1;
for (int i=S.head[u];i;i=S.next[i]){
int v=S.to[i];
if (v!=fa[u]){
if (vis[v]){
if (col[v]==col[u]) flag=1;
le=v,ri=u;
}else{
fa[v]=u;
dfs(v);
a[u]+=a[v];
}
}
}
}
int solve(int u){
int sum=abs(a[u]);
vis[u]=1;
for (int i=S.head[u];i;i=S.next[i]){
int v=S.to[i];
if (v!=fa[u]&&!vis[v]){
sum+=solve(v);
}
}
return sum;
}
int dfs2(int u,int x){
int sum=a[u];
if (huan[u]) sum-=x;
sum=abs(sum);
vis[u]=1;
for (int i=S.head[u];i;i=S.next[i]){
int v=S.to[i];
if (v!=fa[u]&&!vis[v]){
sum+=dfs2(v,x);
}
}
return sum;
}
void solve2(){
int now=le;
while (now!=ri){
huan[now]=1;
p[++ptot]=a[now];
now=fa[now];
}
sort(p+1,p+1+ptot);
cout<<dfs2(1,p[ptot/2+1])+abs(p[ptot/2+1])<<'\n';
}
int solve3(int u,int x){
int now=0;
if (col[u]==1) now++;
else now--;
if (u==le||u==ri) now-=x;
vis[u]=1;
for (int i=S.head[u];i;i=S.next[i]){
int v=S.to[i];
if (v!=fa[u]&&!vis[v]){
now+=solve3(v,x);
}
}
ans+=abs(now);
return now;
}
void solve3(){
int x=g/2;solve3(1,x);
cout<<ans+abs(x)<<'\n';
}
signed main(){
n=read(),m=read();
for (int i=1;i<=m;i++){
int u=read(),v=read();
S.adde(u,v),S.adde(v,u);
}
dfs(1);
for (int i=1;i<=n;i++){
if (col[i]) ++g;
else --g;
}
if (g!=0&&!flag||abs(g)%2!=0&&flag){
puts("-1");
return 0;
}
memset(vis,0,sizeof(vis));
if (m==n-1) cout<<solve(1)<<'\n';
else if (!flag) solve2();
else solve3();
return 0;
}