CF2042E Vertex Pairs 题解

赛时想出来了,赛后一遍过。好吧,被卡了,原因是写挂了,后面改过了。

感谢 @Akuto_urusu 提出可以优化的地方。

题意

给定一棵有 \(2\times n\) 个节点的树,每个节点都有颜色,颜色在 \([1,n]\) 之间,保证每个颜色出现 \(2\) 次。选第 \(i\) 个点的代价是 \(2^i\)

现在,你需要选出一个集合。你需要使得这个集合内的点互相能通过该集合内的点到达,并且每种颜色都至少在该集合中出现一次。代价为集合内所有点的代价之和。

你需要找出代价最小的集合并给出方案。

思路

先考虑这个代价最小的集合的大致求法。

有一个经典技巧,从大到小考虑,如果我们选了 \(i\),代价为 \(2^i\),而如果我们不选 \(i\),对于 \(j\in[1,i-1]\),无论我们是否选 \(j\),这个代价最多也是 \(\sum_{j=1}^{i-1}2^j=2^i-1\),比 \(2^i\) 小。

所以按节点编号从大到小考虑,如果能不取该节点就不取(换种说法,也就是删去),否则就取。

那么什么情况下该节点可以不取呢?

根据题目给出的定义,如果删去它后有一个连通块里包含所有颜色就可以不取。

所以我们需要判断一个连通块内是否包含所有颜色。

但是,这个问题有点难解决,至少我不会。

因为删掉节点 \(u\) 后,剩下的部分有两种,一种是以 \(u\) 的儿子为根的子树,一种是整棵树删去 \(u\) 子树后剩下的部分。两种一起处理太困难了。

那么再想想,我们如何做使得只用处理一种,也就是说,另一种一定被取或被删呢?。

\(col_i\)\(i\) 节点的颜色。

假设我们已经找到了一个必取(不可删)的节点 \(v\),并让该连通块变成以 \(v\) 为根的树。(至于为什么是连通块而不是整个图,后面会说)

那么这样我们再考虑删点时,惊奇地发现——因为树根 \(v\) 是不能删的,所以删去的只能是子树部分。那么删去点 \(u\) 就相当于删去了以 \(u\) 为根的这棵子树!

(图为示意图,隐去了节点颜色等信息)

然后如何判断一个节点是否能删呢?

如果直接去搜索删完后剩下的部分是否有所有颜色太慢了,所以考虑把所有不能删的节点全部标记,没被标记的就可以删。

如果节点 \(i,j\) 满足 \(col_i=col_j(i\neq j)\),那么 \(i,j\) 的所有公共祖先都不能删,因为如果删了,\(col_i\) 这种颜色就不存在于这个连通块中了。所以将 \(i,j\) 的所有公共祖先标记一下不能删。

但是有一些颜色初始时在这个连通块中就只有一个节点了,那么这个节点的所有祖先都不能删。

(节点黑色圆圈内为编号,圆圈外为节点的颜色)

处理完后,从大到小遍历所有还没被删的点,如果没被标记就把它删掉。但是删掉该子树后,有些颜色的出现次数变成 \(1\) 了,此时仅存的那个该颜色的点的祖先都不能取。所以暴力遍历要删的子树的所有节点,对于另一个同颜色的点,对其所有祖先进行标记。

按照从大到小的顺序一直进行下去,直到所有节点被删或被标记。

那么这个标记的维护,会有路径上的更改与单点查询,考虑用树链剖分实现。因为有 \(n\) 种颜色,每种颜色需要对路径标记一次,时间复杂度为 \(O(n\log ^2n)\)。而每个节点最多被删一次,每一次删需要一次路径标记,时间复杂度为 \(O(n\log^2n)\)。到这里时间复杂度 \(O(n\log ^2n)\)


现在找到一个必取点后的事情已经被我们解决了,但是如何去找一个必取点呢?

实际上,只用判断编号为 \(2n\) 的这个点是否能删就行了。

  • 如果可以删,那么删完后存在唯一的节点 \(v\) 满足 \(col_v=col_{2n}(v\neq 2n)\),而又要保证包含所有颜色,所以必须取 \(v\)。将 \(v\) 所在的连通块留下,将其他连通块删去,这也是为什么上文说的是连通块了。

  • 如果不能删,那么 \(2n\) 这个点就必取,上文说的连通块在此情况下其实就是整个图。

那么如何判断编号为 \(2n\) 的点是否能删呢?

我们再来看看刚才那个图。

根据刚才说的,剩下的连通块分为两种,太麻烦了,所以初始建树时,我们钦定 \(2n\) 为树的根,这样只用讨论以 \(2n\) 的儿子为根的子树是否合法就行了。

记录 \(sum_u\) 为以 \(u\) 为根的子树内颜色种数。然后判断是否有 \(sum_v=n,v\in son_{2n}\)

预处理 \(sum\) 数组,对于节点 \(i,j\),满足 \(col_i=col_j(i\neq j)\),将所有 \(i,j\) 的祖先的 \(sum\) 值加 \(1\)。这里可以用树上差分统计,将 \(sum_i\)\(1\)\(sum_j\)\(1\)\(sum_{\operatorname{lca(i,j)}}\)\(1\)

处理完后,遍历一遍树统计差分数组,然后最近公共祖先用树剖实现(因为上面已经用了树剖)。这里时间复杂度 \(O(n\log n)\)

那么到此这道题就做完了。

所以整个做法就是:判断一下编号为 \(2n\) 的点是否能删,如果能删,将与它颜色相同的另一个点定为新树根;否则定它为新树根。然后用树链剖分处理每个点是否能删,能删则删,删后再更新标记。最后把没被删的点输出。

时间复杂度 \(O(n\log ^2n)\),常数小,跑得挺快的。因为是树剖,所以码量可能有点大。

代码

#include<bits/stdc++.h>
using namespace std;
#pragma GCC optimize(3)
#pragma GCC optimize(2)
#pragma GCC optimize("Os")
const int N=1e6+5;
int head[N],cnt,n;//建图 
int top[N],id[N],sz[N],son[N],d[N],f[N],cnta;
//树剖数组,top链顶,id[u]为u在序列中的下表,son重儿子,d深度,f为父亲 
int col[N];//每个点颜色 
int pos1[N],pos2[N];
//pos1[i],pos2[i]为颜色为i的两个点的编号 
int sum[N];
//原树内以u为根的子树内的颜色个数
int root;
//新树根节点 
bool del[N];//该点是否删去
vector<int> ans;//存答案 
struct edge
{
	int v,nxt;
}a[N<<1];//链式前向星建图 
void add(int u,int v) 
{
	a[++cnt].v=v;
	a[cnt].nxt=head[u];
	head[u]=cnt;
}
struct seg
{
	bool assign,val;//区间赋值,单点查询线段树
	//assign为lazy标记,val为值 
}s[N<<2];
int read()//快读 
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
void pushdown(int rt)//下传 
{
	if(s[rt].assign)
	{
		s[rt<<1].assign|=1;
		s[rt<<1|1].assign|=1;
		s[rt<<1].val|=1;
		s[rt<<1|1].val|=1;
		s[rt].assign=0;
	}
}
void update(int l,int r,int rt,int L,int R)
{//区间加 
	if(L<=l&&R>=r)
	{
		s[rt].val|=1;
		s[rt].assign|=1;
		return;
	}
	pushdown(rt);
	int mid=(l+r)>>1;
	if(L<=mid) update(l,mid,rt<<1,L,R);
	if(R>mid) update(mid+1,r,rt<<1|1,L,R);
}
int query(int l,int r,int rt,int pos)
{//单点查询 
	if(l==r) return s[rt].val;
	int mid=(l+r)>>1;
	pushdown(rt);
	if(pos<=mid) return query(l,mid,rt<<1,pos);
	return query(mid+1,r,rt<<1|1,pos);
}
void dfs(int u,int fa)
{//预处理深度,子树大小,重儿子 
	d[u]=d[fa]+1,sz[u]=1,f[u]=fa;
	for(int i=head[u];i!=0;i=a[i].nxt)
	{
		int v=a[i].v;
		if(v==fa||del[v]) continue;
		dfs(v,u);
		sz[u]+=sz[v];
		if(sz[v]>sz[son[u]]) son[u]=v;
	}
}
void dfs2(int u,int t)
{//预处理链顶,在序列中的下标 
	top[u]=t,id[u]=++cnta;
	if(!son[u]) return;
	dfs2(son[u],t);
	for(int i=head[u];i!=0;i=a[i].nxt)
	{
		int v=a[i].v;
		if(v==f[u]||v==son[u]||del[v]) continue;
		dfs2(v,v); 
	}
}
int lca(int u,int v)
{//树剖求LCA 
	while(top[u]!=top[v])
	{
		if(d[top[u]]>d[top[v]]) swap(u,v);
		v=f[top[v]];
	}
	if(d[u]>d[v]) swap(u,v);
	return u;
}
void path_update(int u,int v)
{//树剖对路径进行赋值 
	while(top[u]!=top[v])
	{
		if(d[top[u]]>d[top[v]]) swap(u,v);
		update(1,n,1,id[top[v]],id[v]);
		v=f[top[v]];
	}
	if(d[u]>d[v]) swap(u,v);
	update(1,n,1,id[u],id[v]);
}
void get_sum(int u,int fa)//统计差分数组 
{
	for(int i=head[u];i!=0;i=a[i].nxt)
	{
		int v=a[i].v;
		if(v==fa) continue;
		get_sum(v,u);
		sum[u]+=sum[v];
	}
}
void assign(int u,int fa,int end)//在原树上删点 
{//删去原树除去 以end为根的子树 的所有点 
	del[u]=1;
	for(int i=head[u];i!=0;i=a[i].nxt)
	{
		int v=a[i].v;
		if(v==end||v==fa) continue;
		assign(v,u,end);
	}
}
bool check(int u)//判断是否删掉2n这个点 
{
	for(int i=head[u];i!=0;i=a[i].nxt)
	{
		int v=a[i].v;
		if(v==f[u]) continue;
		if(sz[v]>=n/2)
		{//判断其儿子的子树是否合法 
			if(sum[v]==n/2) 
			{
				assign(n,0,v);//删点 
				return true;
			}
			return false;
		}
	}
	return false;
}
void modify(int u,int fa)//新树上删点 
{
	int pos=0;
	if(pos1[col[u]]==u) pos=pos2[col[u]];
	else pos=pos1[col[u]];
	path_update(root,pos);//更新另一个点 
	del[u]=1;
	for(int i=head[u];i!=0;i=a[i].nxt)
	{
		int v=a[i].v;
		if(v==fa||del[v]) continue;
		modify(v,u);
	}
}
int main()
{
	n=read()*2;
	for(int i=1;i<=n;i++) 
	{
		col[i]=read();
		if(pos1[col[i]]) pos2[col[i]]=i;
		else pos1[col[i]]=i;
	}
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		add(u,v),add(v,u);
	}
	dfs(n,0);//以2n这个点为根 
	dfs2(n,0);
	for(int i=1;i<=n;i++)
	{
		int l=lca(pos1[i],pos2[i]);
		sum[pos1[i]]++,sum[pos2[i]]++,sum[l]--;
	}
	get_sum(n,0);
	if(check(n))//如果可以删2n这个点 
	{
		if(pos1[col[n]]==n) root=pos2[col[n]];
		else root=pos1[col[n]];
		cnta=0;
		memset(son,0,sizeof(son));
		dfs(root,0);//以root为根重构一下树 
		dfs2(root,0);
	}
	else root=n;//树根没变,可以不用重构 
	for(int i=1,l;i<=n;i++)
	{
		if(del[pos1[i]]) l=pos2[i];//如果只有一个该颜色的点未删,则它的祖先都不能删 
		else if(del[pos2[i]]) l=pos1[i];
		else l=lca(pos1[i],pos2[i]);//如果有两个同颜色的节点未被删,则它们的公共祖先都不能删 
		//更新哪些节点不能删 
		path_update(root,l);
	}
	for(int u=n;u>=1;u--)
	{
		if(del[u]) continue;//已经被删过 
		if(!query(1,n,1,id[u])) modify(u,f[u]);//如果能删 
		else ans.push_back(u);
	}
	printf("%d\n",ans.size());
	for(int i=ans.size()-1;i>=0;i--) printf("%d ",ans[i]);
	return 0;
}

(制作不易,若有错误请指出,如果对您有帮助,可以点个赞么,谢谢!)

posted @ 2024-12-03 22:22  Twilight_star  阅读(35)  评论(0)    收藏  举报