【洛谷5659】[CSP-S2019] 树上的数(思维)

点此看题面

  • 给定一棵\(n\)个点的树,每个点上有一个数字。
  • 你可以按任意顺序删去树上的所有边,每当要删去一条边,这条边两端点上的数字将会交换。
  • 记录\(P_i\)表示最终\(i\)所在节点,求字典序最小的\(P\)。
  • 数据组数\(\le10,n\le2000\)

同一点的边

这道题的核心问题就在于删边顺序,因此我们首先要尝试简化问题,发现只需考虑同一点的边之间的删除顺序。

因为显然不同点的边删除顺序是无用信息,而知道了所有点各自边之间的删除顺序又一定能构造出一个符合条件的方案,所以这样做是完全可行的。

而要维护同一点的边的删除顺序,我们可以考虑用链表进行处理。

字典序问题的贪心

这种最小化字典序的问题有一个经典贪心,就是我们依次枚举每一位,尽可能填入能填的最小数。

所以我们的核心问题在于如何判定在当前情况下,是否可能让\(x\)上的数字最终留在\(y\)上。

找到\(x\)到\(y\)这条路径,它可以分为三部分:起点\(x\)、中间若干点、终点\(y\)。接下来就是分类讨论。

三部分的分类讨论

对于起点\(x\),考虑它连出的起始边。显然起始边必须是\(x\)的边当中第一条被删去的边(因此我们需要记录每条边是否是第一条被删去的,以及每个点第一条删去的是谁),而一条边可以被第一条删去需要满足它在链表中没有前驱。

对于终点\(y\),考虑它连出的终止边。显然终止边必须是\(y\)的边当中最后一条被删去的边(因此我们还需要记录每条边是否是最后一条被删去的,以及每个点最后一条删去的是谁),而一条边可以被最后一条删去需要满足它在链表中没有后继。

现在我们既需要记录第一条被删去的边\(H\),又需要记录最后一条被删去的边\(T\),这时候就会产生一个坑点:如果\(H\)和\(T\)在链表中的一条链上,则这条链长必须等于这个点的度数,因为所有边都必须被删去(因此我们需要在每条链的左端点上记录链长,并对于每个端点记下它的对面端点,方便判断这条链是否变成了以\(H\)开头以\(T\)结尾)。

所以想把一条边设为第一条边还需要注意若它所在链的右端点是最后一条边,则这条链长必须等于度数(想设成最后一条边同理需要类似判断)。

对于中间的点,考虑到达它的边\(p\)和离开它的边\(q\),那么我们相当于想要在链表上连接\(p,q\),因此需要满足\(p\)不是最后一条边、\(q\)不是第一条边、\(p\)没有后继、\(q\)没有前驱、\(p,q\)不在同一条链中,且如果\(p\)是第一条边、\(q\)是最后一条边还需要满足两链长之和恰好为度数。

一个点出发的答案

然后我们发现如果枚举\(x,y\)再判断\(x\)上的数能否留在\(y\)上,还需要一次\(O(n)\)的判断,会挂掉。

但实际上,从\(x\)出发到任意\(y\),中间点的讨论都是类似的。

因此,对于起点\(x\)我们选择可能的起始边出发,每到一个点先尝试把连向它的边作为终止边,若可以则更新答案。然后把这个点当作中间点,枚举出边尝试转移。

这样一来,对于每个起点只需遍历一次就可以了。

找到最优终点之后,一路往回跳更新沿途所有中间点,并特殊更新起点和终点即可。

代码:\(O(Tn^2)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 2000
#define add(x,y) (++d[x],e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y,s[ee]=node(ee))
using namespace std;
int n,p[N+5],d[N+5],H[N+5],T[N+5],ee,lnk[N+5];struct edge {int to,nxt;}e[N<<1];
struct node {int pre,nxt,H,T,L,R,C;I node(CI x=0) {pre=nxt=H=T=0,L=R=x,C=1;}}s[N<<1];
int ans,lst[N+5];I void dfs(CI x)
{
	RI p=lst[x]^1;(!T[x]||T[x]==p)&&!s[p].nxt&&(!s[s[p].L].H||s[s[p].L].C==d[x])&&(ans=min(ans,x));//尝试作为终点
	for(RI q=lnk[x];q;q=e[q].nxt) e[p].to^e[q].to&&T[x]^p&&H[x]^q&&!s[p].nxt&&!s[q].pre&&
		s[p].L^s[q].L&&(!s[s[p].L].H||!s[s[q].R].T||s[s[p].L].C+s[q].C==d[x])&&(lst[e[q].to]=q,dfs(e[q].to),0);//作为中间点,枚举边尝试转移
}
I void Mark(CI x,RI y)//让x上的数留在y上
{
	RI p,q=lst[y]^1;s[T[y]=q].T=1,y=e[q].to;//特殊更新终点
	W(x^y) p=lst[y]^1,q^=1,s[p].nxt=q,s[q].pre=p,//更新中间点
		s[s[p].L].C+=s[q].C,s[s[p].L].R=s[q].R,s[s[q].R].L=s[p].L,y=e[q=p].to;//合并链信息
	s[H[x]=q^1].H=1;//特殊更新起点
}
int main()
{
	RI Tt,i,x,y;scanf("%d",&Tt);W(Tt--)
	{
		for(scanf("%d",&n),ee=i=1;i<=n;++i) scanf("%d",p+i),lnk[i]=d[i]=H[i]=T[i]=0;//清空
		for(i=1;i^n;++i) scanf("%d%d",&x,&y),add(x,y),add(y,x);if(n==1) {puts("1");continue;}//特判n=1
		for(y=1;y<=n;printf("%d ",ans),Mark(x,ans),++y) for(ans=n,i=lnk[x=p[y]];i;i=e[i].nxt)
			(!H[x]||H[x]==i)&&!s[i].pre&&(!s[s[i].R].T||s[i].C==d[x])&&(lst[e[i].to]=i,dfs(e[i].to),0);//枚举起始边
		putchar('\n');
	}return 0;
}
上一篇:使用批处理文件命令行方式快速启动和停止IIS、SqlServer


下一篇:MongodDB用GridFS方式存取文件