CSP2019 樹上的數 題解


題面
這是一道典型的部分分啟發正解的題。

所以我們先來看兩個部分分。

Part 1 菊花圖

這應該是除了暴力以外最好想的一檔部分分了。

如上圖(節點上的數字已省略),如果我們依次刪去邊(2)(1)(3)(4),那么操作完后2號點上的數字就會跑到1號點上,1號點數字會跑到3號點上,3號點數字跑到4號點上……依此累推。那么我們相當於把五個節點連成了一個環( 5 -> 2 -> 1 -> 3 -> 4 -> 5 ),每一個結點上的數字都會跑到環上的下一個結點上去,我們就是要求能使最終得到的排列字典序最小的環。那么我們逐位貪心,先由數字1所在的節點選擇它在環上的下一個點是哪一個,在由數字2所在節點選,依此類推。每次在合法的情況下選標號最小的節點即可。具體細節可以見代碼。

Part1 代碼:

#include<bits/stdc++.h>
using namespace std;
#define N 4007
#define mem(x) memset(x,0,sizeof(x))
int p[N],ans[N],vis[N];
int fa[N];
int find(int x)
{
    return fa[x]==x?x:fa[x]=find(fa[x]);
}
int main()
{
    int n,t;
    scanf("%d",&t);
    while(t--)
    {
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	    fa[i]=i;
	mem(vis);
	for(int i=1;i<=n;i++)
	    scanf("%d",&p[i]);
	int x,y;
	for(int i=1;i<n;i++)
	    scanf("%d%d",&x,&y);
	for(int i=1;i<=n;i++)
	{
	    int x=p[i];
	    for(int j=1;j<=n;j++)
	    {
		if(!vis[j]&&(i==n||find(x)!=find(j)))
		{
		    ans[i]=j;
		    fa[find(x)]=find(j);
		    vis[j]=1;
		    break;
		}
	    }
	}
	for(int i=1;i<=n;i++)
	    printf("%d ",ans[i]);
	printf("\n");
    }
    return 0;
}

Part2 鏈

這一部分和正解關系緊密,引入了拓撲序的模型來描述題目中的限制條件。

其中節點中括號里的是節點上的數字。
如果數字1想跑到1號節點上去要怎么辦?
那么我們可以依次刪去(2)(3)號邊,那么數字1就在1號點上了。
只要這樣就可以了嗎?
其實這里還隱含了兩個條件,就是邊(1)必須在(2)之后刪除,且(4)必須在(3)之前刪除,不然數字1就不在它應該在的地方了。
如果我們為每一條邊定一個優先級,優先級大的先刪,那么4條邊的優先級大小關系就是:(1) < (2) > (3) < (4)
這樣我們就可以保證數字1最終在1號點上了,此外因為我們要逐位貪心,所以在此基礎上我們還希望數字2能到2號節點上。
但這是否有可能呢?
發現這樣是不可能的,因為要使數字2到達2號點,那么必須滿足優先級(3) > (2) , 與數字1的條件是沖突的,所以不行。
所以我們得到了這部分的算法:
從數字1到數字n逐位貪心,每次選擇一個當前數字能到達的、不與之前條件沖突的、標號最小的節點,作為這個數字最終所在的節點。然后將新產生的條件加入。
具體的實現可以在每一個節點上維護一個標記值0,1,2,分別表示這個節點左右的兩條邊之間的優先級 沒有限制、左邊大於右邊、右邊大於左邊。然后每次從當前數字所在位置向左右兩邊找符合條件的點,再把新條件對應的標記值更新即可。具體見代碼。

Part2 代碼:

#include<bits/stdc++.h>
using namespace std;
#define N 4007
int hd[N],pre[N],to[N],num,tag[N],pos[N],id[N],p[N],d[N],cnt,ans[N];
void adde(int x,int y)
{
    num++;pre[num]=hd[x];hd[x]=num;to[num]=y;
}
void dfs(int v,int f)
{
    id[++cnt]=v,pos[v]=cnt;
    for(int i=hd[v];i;i=pre[i])
    {
	int u=to[i];
	if(u==f)continue;
	dfs(u,v);
    }
}
#define mem(x) memset(x,0,sizeof(x))
int main()
{
    int n;
    int t;
    scanf("%d",&t);
    while(t--)
    {
	scanf("%d",&n);
	mem(hd),mem(tag),mem(d);
	num=0,cnt=0;
	for(int i=1;i<=n;i++)
	    scanf("%d",&p[i]);
	for(int i=1;i<n;i++)
	{
	    int x,y;
	    scanf("%d%d",&x,&y);
	    adde(x,y),adde(y,x);
	    d[x]++,d[y]++;
	}
	int rt=0;
	for(int i=1;i<=n ;i++)
	    if(d[i]==1)rt=i;
	dfs(rt,0);
	for(int i=1;i<=n;i++)
	{
	    int x=p[i],b=pos[x];
	    int mi=n+1;
	    if(tag[b]!=1)
	    {
		for(int j=b+1;j<=n;j++)
		{
		    if(tag[j]!=1)mi=min(mi,id[j]);
		    if(tag[j]==2)break;
		}
	    }
	    if(tag[b]!=2)
	    {
		for(int j=b-1;j>=1;j--)
		{
		    if(tag[j]!=2)mi=min(mi,id[j]);
		    if(tag[j]==1)break;
		}
	    }
	    if(pos[mi]>b)
	    {
		for(int j=b+1;j<=pos[mi]-1;j++)tag[j]=1;
		tag[b]=tag[pos[mi]]=2;
	    }
	    else
	    {
		for(int j=pos[mi]+1;j<b;j++)tag[j]=2;
		tag[pos[mi]]=tag[b]=1;
	    }
	    tag[1]=tag[n]=0;
	    ans[i]=mi;
	}
	for(int i=1;i<=n;i++)
	    printf("%d ",ans[i]);
	printf("\n");
    }
    return 0;
}
	

Part3 正解

其實從第二部分我們已經看到,形如數字x要到y號節點上所需滿足的條件可以描述為一系列邊的優先級的大小關系(這里的優先級實際上就是一種拓撲序),並且我們可以把這些條件放在節點上,表示與這個節點相鄰的所有邊之間的大小關系是怎樣的,也就是說只有與同一個節點相鄰的邊之間才會有大小關系的限制。那么我們如何把鏈上的算法拓展到一般的樹上呢?

如果有一個數字想從1號點到3號點,那么需要滿足的條件有兩種:

  1. (3)的優先級是與1號點相鄰的邊中最大的,(6)的優先級是與3號點相鄰的邊中最小的;
  2. (3)的優先級大於(6);

對於條件2,這樣還不夠充分,因為如果刪完(3)之后再刪(4)的話就不對了。也就是說刪完(3)之后要緊接着刪(6)
這個限制條件就相當於把與2號點相鄰的邊按照優先級大小排列,那么(3)和(6)必須是相鄰的,且(3)在(6)前面。
怎么做到這一點呢?發現這種要讓兩條邊相鄰的條件其實已經在第一部分的菊花圖中討論過了,一樣的用並查集判斷即可,只不過Part1中只用了一個並查集,而現在我們要維護每一個點周圍的邊的大小關系,所以要對每一個點開一個並查集。
而對於條件1,我的做法是對每一個並查集建了一個虛點,如果一條邊的優先級最大,那么由虛點向它連一條邊,如果一條邊優先級最小,則由它向虛點連一條邊,這樣就可以直接套Part1部分的代碼了。
然后貪心過程與Part2類似,每次從一個點出發遍歷整棵樹,每走一條邊就判斷一下,然后再對一條路徑進行修改即可。

Part3 代碼:

#include<bits/stdc++.h>
using namespace std;
#define N 20007
#define mem(x) memset(x,0,sizeof(x))
int hd[N],pre[N],to[N],num,fa[N],sz[N],d[N],p[N],ver;
bool in[N],out[N];
void adde(int x,int y)
{
    num++;pre[num]=hd[x];hd[x]=num;to[num]=y;
}
int find(int x)
{
    return fa[x]==x?x:fa[x]=find(fa[x]);
}
void merge(int x,int y)
{
    int u=find(x),v=find(y);
    fa[u]=v,sz[v]+=sz[u];
    out[x]=in[y]=1;
}
bool check(int x,int y,int l)
{
    if(in[y]||out[x])return false;
    int u=find(x),v=find(y);
    if(u==v&&sz[u]!=l)return false;
    return true;
}
void dfs1(int v,int f)
{
    if(f!=v&&check(f,v,d[v]+1))ver=min(ver,v);
    for(int i=hd[v];i;i=pre[i])
    {
	int u=to[i];
	if(i==f)continue;
	if(check(f,i,d[v]+1))
	{
	    dfs1(u,i^1);
	}
    }
}
bool dfs2(int v,int f,int p)
{
    if(v==p)
    {
	merge(f,v);
	return true;
    }
    for(int i=hd[v];i;i=pre[i])
    {
	int u=to[i];
	if(i==f)continue;
	if(dfs2(u,i^1,p))
	{
	    merge(f,i);
	    return true;
	}
    }
    return false;
}
int main()
{
    int t,n;
    scanf("%d",&t);
    while(t--)
    {
	scanf("%d",&n);
	mem(hd),mem(in),mem(out),mem(d);
	num=(n+1)/2*2+1;
	for(int i=1;i<=n;i++)
	    scanf("%d",&p[i]);
	for(int i=1;i<n;i++)
	{
	    int x,y;
	    scanf("%d%d",&x,&y);
	    d[x]++,d[y]++;
	    adde(x,y),adde(y,x);
	}
	for(int i=1;i<=num;i++)
	    fa[i]=i,sz[i]=1;
	for(int i=1;i<=n;i++)
	{
	    int v=p[i];
	    ver=n+1;
	    dfs1(v,v);
	    dfs2(v,v,ver);
	    printf("%d ",ver);
	}
	printf("\n");
    }
    return 0;
}

總結:通過這題大家可以發現,此題正解與部分分是緊密相連的,如果沒有對部分分的思考,很難直接想到正解。這啟發我們當無法直接想到正解時,可以先思考此題的部分分,通過部分分來得到正解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM