Codeforces Round 947 (Div. 1 + Div. 2) E. Chain Queries
本来决定开摆养生不打的,但11点半的时候点进去看到这个题是个疑似DS,写题的欲望瞬间高涨,然后就40min写了这个题
然而赛中并不能提交,只好等到第二天早上再交一发,没想到还WA了一发才过
首先这题如果我们能确定当前黑色点集的链的两个端点\(x,y\)的话,这个题就非常显然了
只需要求出\((x,y)\)路径上黑色点的数量,当这个值与\((x,y)\)路径的长度、以及黑色节点的总个数都相等时才合法
那么现在问题是如何确定这两个端点,一个很自然的观察是一定有一个端点是DFS序最大的点,因为根据DFS序的性质,在该点的子树内一定没有其它的黑色点
那另一个端点该取什么好呢,单纯的考虑DFS序最小、深度最小、深度次大这些限制都会有反例,因此需要往别的方向思考
考虑诸如树上莫队处理树上路径时,我们用到的是欧拉序,因此我们对于每个点额外维护它在欧拉序中最后一次出现的前后关系
那么根据欧拉序的性质,欧拉序最小的点的子树内也一定没有其它的黑色点,因此可以用来作为第二个端点
但这种情况有一个Corner Case,即样例中\(1,3,4\)为黑点的情况,两次得到的端点会是重合的
不过好在我们仔细分析一波后,会发现当且仅当这条链的两个端点有祖先关系时会发生这种情况,此时我们把另一个端点换成欧拉序最大的点(就是这条链最上方的节点)即可
用两个set
维护点集,同时用树剖+线段树维护黑点个数,总复杂度\(O(n\log^2n)\)
#include<cstdio>
#include<iostream>
#include<set>
#include<vector>
#include<algorithm>
#include<utility>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=200005;
int t,n,q,c[N],x,y; vector <int> v[N];
class Segment_Tree
{
private:
int sum[N<<2];
public:
#define TN CI now=1,CI l=1,CI r=n
#define LS now<<1,l,mid
#define RS now<<1|1,mid+1,r
inline void build(TN)
{
sum[now]=0; if (l==r) return;
int mid=l+r>>1; build(LS); build(RS);
}
inline void updata(CI pos,CI mv,TN)
{
sum[now]+=mv; if (l==r) return; int mid=l+r>>1;
if (pos<=mid) updata(pos,mv,LS); else updata(pos,mv,RS);
}
inline int query(CI beg,CI end,TN)
{
if (beg<=l&&r<=end) return sum[now]; int mid=l+r>>1,ret=0;
if (beg<=mid) ret+=query(beg,end,LS);
if (end>mid) ret+=query(beg,end,RS);
return ret;
}
#undef TN
#undef LS
#undef RS
}SEG;
namespace Heavy_Division
{
int idx,idxr,top[N],dep[N],sz[N],dfn[N],dfnr[N],anc[N],son[N];
inline void DFS1(CI now=1,CI fa=0)
{
son[now]=0; sz[now]=1; dep[now]=dep[fa]+1; anc[now]=fa;
for (int to:v[now]) if (to!=fa)
{
DFS1(to,now); sz[now]+=sz[to];
if (sz[to]>sz[son[now]]) son[now]=to;
}
}
inline void DFS2(CI now=1,CI tf=1)
{
dfn[now]=++idx; top[now]=tf;
if (son[now]) DFS2(son[now],tf);
for (int to:v[now]) if (to!=anc[now]&&to!=son[now]) DFS2(to,to);
dfnr[now]=++idxr;
}
inline int querysum(int x,int y)
{
int ret=0;
while (top[x]!=top[y])
{
if (dep[top[x]]<dep[top[y]]) swap(x,y);
ret+=SEG.query(dfn[top[x]],dfn[x]);
x=anc[top[x]];
}
if (dep[x]<dep[y]) swap(x,y);
ret+=SEG.query(dfn[y],dfn[x]);
return ret;
}
inline int LCA(int x,int y)
{
while (top[x]!=top[y])
{
if (dep[top[x]]<dep[top[y]]) swap(x,y);
x=anc[top[x]];
}
if (dep[x]<dep[y]) swap(x,y);
return y;
}
};
using namespace Heavy_Division;
int main()
{
//freopen("E.in","r",stdin); freopen("E.out","w",stdout);
for (scanf("%d",&t);t;--t)
{
RI i; for (scanf("%d%d",&n,&q),i=1;i<=n;++i) scanf("%d",&c[i]);
for (i=1;i<n;++i) scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
set <pi> FT,BK; DFS1(); DFS2(); SEG.build();
for (i=1;i<=n;++i) if (c[i]) SEG.updata(dfn[i],c[i]),FT.insert(pi(dfn[i],i)),BK.insert(pi(dfnr[i],i));
for (i=1;i<=q;++i)
{
scanf("%d",&x);
if (c[x]) FT.erase(pi(dfn[x],x)),BK.erase(pi(dfnr[x],x));
else FT.insert(pi(dfn[x],x)),BK.insert(pi(dfnr[x],x));
SEG.updata(dfn[x],(c[x]^1)-c[x]); c[x]^=1;
if (FT.empty()) { puts("No"); continue; }
int x=FT.rbegin()->second,y=BK.begin()->second;
if (x==y) y=BK.rbegin()->second;
int tmp=dep[x]+dep[y]-2*dep[LCA(x,y)]+1;
puts(tmp==querysum(x,y)&&querysum(x,y)&&tmp==FT.size()?"Yes":"No");
}
for (idx=idxr=0,i=1;i<=n;++i) v[i].clear();
}
return 0;
}
PS:后面看题解和祁神代码好像这题有个更简单的分类讨论做法
大体就是根据两种链的形态,根据每个黑色点相邻的黑色点数量来判断,但我感觉那种方法写起来还是细节挺多的,没有这种大力的方法想起来简单
辣鸡老年选手AFO在即