[CF613D]Kingdom and its cities

做题时间:2022.7.8

\(【题目描述】\)

给你一棵 \(N(N\leq 10^5)\) 个点的树,有 \(q(q\leq 10^5)\) 次询问,每次询问给出 \(k_i(\sum k_i \leq 10^5)\) 个关键点,询问最少删除树上多少个节点可以使得关键点两两不连通,若不能达成目的输出-1。

\(【输入格式】\)

第一行一个整数 \(N\)
接下来 \(N-1\) 行每行两个整数表示树上的一条边
\(2N\) 行一个整数 \(q\)
接下来 \(q\) 行每行第一个整数 \(k_i\) ,接下来 \(k_i\) 个整数表示本次询问的关键点

\(【输出格式】\)

\(q\) 行,每行一个整数表示答案

\(【考点】\)

虚树、树形DP

\(【做法】\)

非常明显的套路——树上决策、贪心无法解决、每棵子树的答案不和其它点相关,就是树形DP,并且有 \(\sum k_i\leq 10^5\) ,同消耗战,非关键点对于答案的影响可以很容易被处理掉,考虑建立虚树进行DP

首先,若有两个关键点 在原树 相邻,则无法达成目的,这里的判断我们留在确定了关键点的DP中进行。

其次,考虑DP,定义 \(f_u\) 表示以 \(u\) 为根的子树的答案。若当前点 \(u\) 为关键点,那么 \(u\) 就会和其子树中所有当前未拦截的点联通,因此 \(f_u\) 要加上子树中未拦截的点的数量;若当前点不为关键点,其子树中当前未拦截的点可以通过 \(u\) 和另一子树中当前未拦截的点联通,因此要考虑两种情况:

  1. \(u\) 的子树中仅有一棵中含有当前未拦截的点,这时其实可以不用拦截,因为它不会和任何点产生联通(若拦截了反而不是最优解);
  2. \(u\) 的子树中有多棵中含有当前未拦截的点,这是就必须要拦截了,答案加1即可。

最后加上各个子树的答案即可,转移方程:

\[f_u=\sum\limits_{v\in son_u}f_v+\left\{ \begin{array}{rcl} \ 0 & & u不是关键点且满足情况1 \\ \ 1& & u不是关键点且满足情况2\\ \sum\limits_{v\in son_u} cnt_v & & u是关键点 \end{array} \right. \]

最后就是解决虚树与原树在边权上的差异,但实际上除了判断是否能达成目的时要储存下虚树上相邻两点在原数中是否相邻之外,对于DP并没有影响。

#include<cstdio>
#include<iomanip>
#include<algorithm>
using namespace std;
const int N=1e6+50;
struct edge{
	int to,nxt,val;
}a[N*10];
int head[N],fa[N],id[N],st[N],top;
int dp[N],dep[N],f[N][25],h[N],cnt;
int n,tot,q,Can,k;
bool Is_h[N],tag[N];
//Is_h[u]表示节点 u 是否为关键节点 
//tag[u]表示以 u 为根的子树是否含有未被拦截的关键点 
void add(int u,int v,int w)
{
	cnt++;
	a[cnt].to=v;
	a[cnt].val=w;
	a[cnt].nxt=head[u];
	head[u]=cnt;
}
void DP(int u,int fa)
{
	int sum=0;//sum记录u有多少个子树中有未拦截的关键节点 
	dp[u]=0;
	for(int i=head[u];i;i=a[i].nxt){
		int v=a[i].to;
		if(v!=fa){
			if(Is_h[u]&&Is_h[v]&&a[i].val==1) Can=false;
			DP(v,u),dp[u]+=dp[v];
			
			if(tag[v]) tag[u]=true,sum++,tag[v]=false;
			//若当前儿子v中含有未拦截的关键节点,则记录,顺带清除v的标记 
		}
	}
	if(Is_h[u]) dp[u]+=sum,tag[u]=true;//若u是关键点,则要在它将所有包含未拦截关键节点的儿子全部清除 
	else{
		if(sum>1) dp[u]++,tag[u]=false;//若u不是关键点,清除u 
	}
}
bool cmp(int a,int b){return id[a]<id[b];}
void Swap(int &a,int &b){int t=a;a=b;b=t;}
int LCA(int x,int y)
{
	if(dep[x]<dep[y]) Swap(x,y);
	for(int i=20;i>=0;i--){
		if(dep[f[x][i]]>=dep[y]) x=f[x][i];
	}
	if(x==y) return x;
	for(int i=20;i>=0;i--){
		if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i]; 
	}
	return f[x][0];
}
void DFS(int u,int fat)
{
	fa[u]=fat;
	id[u]=++tot,dep[u]=dep[fat]+1,f[u][0]=fat;
	for(int i=1;i<=20;i++) f[u][i]=f[f[u][i-1]][i-1];
	
	for(int i=head[u];i;i=a[i].nxt){
		int v=a[i].to;
		if(v!=fat) DFS(v,u);
	}
}
void Edge(int x,int y)
{
	int w=2;
	if(fa[x]==y||fa[y]==x) w=1;//若原树中两点相邻 
	add(x,y,w),add(y,x,w);
}
bool Work()//建立虚树 
{
	sort(h+1,h+1+k,cmp);
	st[top=1]=1,head[1]=0;
	for(int i=1;i<=k;i++){
		if(h[i]!=1){
			int l=LCA(h[i],st[top]);
			if(id[l]!=id[st[top]]){
				while(id[l]<id[st[top-1]]){
					Edge(st[top-1],st[top]);
					top--;
				}
				if(id[l]>id[st[top-1]]){
					head[l]=0;
					Edge(l,st[top]);
					st[top]=l;
				}
				else Edge(l,st[top--]);
			}
			head[h[i]]=0,st[++top]=h[i];
		}
	}
	for(int i=1;i<top;i++) Edge(st[i],st[i+1]);
	top=0;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n-1;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		add(u,v,1),add(v,u,1);
	}
	scanf("%d",&q);
	DFS(1,0);
	while(q--){
		Can=true;
		scanf("%d",&k);
		for(int i=1;i<=k;i++){
			scanf("%d",&h[i]);
			Is_h[h[i]]=true;
		}
		Work();
		DP(1,0);
		if(!Can) dp[1]=-1;
		
		tag[1]=false,tot=0;
		for(int i=1;i<=k;i++) Is_h[h[i]]=false;
		printf("%d\n",dp[1]);
	}
	return 0;
}
posted @ 2022-07-08 14:47  lxzy  阅读(30)  评论(0)    收藏  举报