Luogu P5659 树上的数

参考博客

前言

过了快半年才来补坑.考试的时候卡在第二题的时间过长了,所以这题只打了个10分暴力.
(那时候的自己实在太嫩了,现在也是 ).

花了大半天才弄懂题解,QAQ~~~

正题

首先,作一个简单的转化:

题目中说删除一条边 ( x , y ) (x,y) 即交换端点上的数,我们可以理解为

一个连续的行为: x x 上的数跑到 y , y y,y 上的数跑到 x x

之后,还有一个简单的性质:一个数的最终所在位置一定与开始的不同且最后一个位置只有一个数(废话)

所以我们可以定义:

f r o m [ x ] from[x] 表示最后 x x 上的数的来源边(一定是邻边)

t o [ x ] to[x] 表示初始时 x x 上的数往那边跑(一定也是邻边)

接着,我们来观察一下删边顺序所导致的结果.

对于度数为2的点:

img

如果先删右再左,可以发现有两个数右移,一个数左移.

实际上,通过这个反向移动的数的来源我们即可确定具体的删边顺序.

升级版:菊花图

img

我们可以发现:数的移动顺序 c 1 , c 2 , c 3 , c 4 c_1,c_2,c_3,c_4 ,确定了以下情况:

t o [ x ] = 1 ( x 1 ) , f r o m [ x ] = 5 ( 5 ) to[x]=1(x的数往1方向跑了),from[x]=5(5方向跑来一个数) ,删边顺序为(以邻接点来表示边) 1 , 2 , 3 , 4 , 5 1,2,3,4,5 .

这样如果用邻接点表示边,按删除顺序排序的话,则会出现如下情况:

t o [ x ] , . . . . , f r o m [ x ] to[x],....,from[x] .(我们称之为偏序链)

显然,如果这条链不能包含所有的邻边的话就不合法.(我们可以称之为提前自闭,这样不能再加入变了)

具体算法流程:

我们可以得到一个贪心算法:先把小的数挪到尽量小的位置,且保证不与前面的操作矛盾.

那么这个移动就会包括3类点:

  1. 起点 x x
  2. 中转点 t t
  3. 终点 y y

我们逐类讨论如何合法:

首先,对于路径上的所有有向边都不能重复经过.

具体要求:

  1. 起点
    1. 保证不提前自闭.
    2. 数还未选择末位置.(顺序每个数的位置即可保证)
  2. 中转点
    1. 保证不提前自闭.
    2. 数不能重复经过一条边.( x > y , y > x x->y,y->x ).
  3. 终点
    1. 保证不提前自闭
    2. 位置还未被占领 ( f r o m [ y ] = 0 ) (from[y]=0)

之后,我们用双向链表来维护偏序链.

定义:

s t [ x ] [ y ] st[x][y] 表示 ( x , y ) (x,y) 这条边所在偏序链的开头

e d [ x ] [ y ] ed[x][y] 表示 ( x , y ) (x,y) 这条边所在偏序链的结尾

初始化: s t [ x ] [ y ] = e d [ x ] [ y ] = y st[x][y]=ed[x][y]=y .(一开始都独立)

之所以这样定义是因为我们还不清楚所有的删除顺序,只能这样来存储信息及确定删除的相对顺序.

假如移动顺序是这样,那么 ( x , u ) , ( u , v ) , ( v , w ) , ( w , y ) (x,u),(u,v),(v,w),(w,y) 依次删除.

这样的话: s t [ v ] [ u ] e d [ v ] [ u ] st[v][u] \sim ed[v][u] 要接在 s t [ v ] [ w ] e d [ v ] [ w ] st[v][w]\sim ed[v][w] 之后.依次类推.

这样保证了不与前面操作的矛盾(相对位置不变),同时符合移动的要求.

具体看代码吧:(我帮不了你了):

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=2010;
void qr(int &x) {
	char c=getchar();x=0;
	while(!isdigit(c))c=getchar();
	while(isdigit(c))x=x*10+c-'0',c=getchar();
}
void qw(int x) {
	if(x>9) qw(x/10);
	putchar(x%10+'0');
}
void pr1(int x) {qw(x);putchar(' ');}

int T,n,pos[N],from[N],to[N],st[N][N],ed[N][N],d[N],edge[N][N];
/*一波定义:
pos[i]:i的位置 
from[i]:i上的数从哪边来.
to[i]:i上的数去哪儿
st[x][y],ed[x][y]:(x,y)所在偏序链的开头,结尾.
d[x]:x的剩余度数(一条边看作两条有向边)
edge[x][y]:(x,y)的移动情况: 两个方向都未走(-1),两个方向都走了(0),只从x到y(x),只从y到x(y).()内为具体的值 
*/
struct Edge{int y,next;}a[N<<1]; int len,last[N];
void ins(int x,int y) {
	a[++len]=(Edge){y,last[x]};last[x]=len;
	d[x]+=2; edge[x][y]=-1; st[x][y]=ed[x][y]=y;
}

int res,root,fa[N];//最后达到的位置(贪心取最小),根,father
void dfs(int x) {
	for(int k=last[x],y;k;k=a[k].next) {
		y=a[k].y; if(y==fa[x]) continue;
		fa[y]=x;
		if(edge[x][y]==x||edge[x][y]==0) continue; 
		if(x^root) { //作为中转点的要求
			if(ed[x][y]==from[x]&&st[x][fa[x]]==to[x]&&d[x]>2) continue;//如果走fa[x]~x~y,那么d[x]会缩小2. 这个判断是防止提前自闭
			if(ed[x][y]==fa[x]) continue;//偏序链中不能有重复.
		}
		else {
			if(from[x]) {
				if(ed[x][y]==from[x]&&d[x]>1) continue;
				//如果一定有了from[x],那么这条边必须最后访问(起点要求) 
			}
		}
		dfs(y);
	}
	if(x^root&&!from[x]) {//终点要求 
		if(to[x]&&st[x][fa[x]]==to[x]&&d[x]>1) ;//提前自闭
		else res=min(res,x);//更新选择
	}
}

int main() {
	qr(T); while(T--) {
		qr(n); len=0;
		for(int i=1;i<=n;i++) last[i]=from[i]=to[i]=d[i]=0,qr(pos[i]);
		for(int i=1,x,y;i<n;i++)
			qr(x),qr(y),ins(x,y),ins(y,x);
		for(int i=1;i<=n;i++) {
			int x,y,z;  root=x=pos[i];
			memset(fa+1,0,n<<2);
			res=N; dfs(x);
			from[y=res]=fa[res];//res上的数来自fa[res]
			pr1(y);
			while(fa[y]^x) {
				z=fa[y];
				d[y]--;
				d[z]--;
				if(edge[z][y]==-1)
					 edge[z][y]=edge[y][z]=z;
				else edge[z][y]=edge[y][z]=0;
				st[z][ed[z][y]]=st[z][fa[z]];
				ed[z][st[z][fa[z]]]=ed[z][y];
				y=z;
			}
			d[y]--;
			d[x]--;
			if(edge[x][y]==-1)
				 edge[x][y]=edge[y][x]=x;
			else edge[x][y]=edge[y][x]=0;
			
			to[x]=y;
		}
		puts("");
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_42886072/article/details/105294392