1. 程式人生 > 實用技巧 >CSP2019 樹上的樹 口胡

CSP2019 樹上的樹 口胡

拖了一年, 今天上午終於把這道題做出來了。


基本思路

題目要求字典序最小, 而字典序是有著天然的貪心性質的, 可以比較自然地想到要應用貪心演算法, 進一步地, 要思考 “某個數字最終停留在某個點上” 會對全域性的刪邊順序產生哪些限制。
比如有這樣一條路徑: \(s--^a--o--^b--o--^c--o--^d--t\), 如果初始時點 s 上的數字最終停留在點 t, 那麼一定滿足:

  1. 邊 a 是與點 s 相鄰的邊中第一個被刪去的邊
  2. 對於路徑上的點 o(們), 在其相鄰的邊中, b 一定緊接著 a 被刪去, c 一定緊接著 b 被刪去
  3. 邊 d 是與點 t 相鄰的邊中最後一個被刪去的邊

以上任意一條的任意部分不被滿足, 初始時點 s 上的數字都不會最終停留在點 t; 以上所有條件都滿足, 初始時點 s 上的數字最終就會停留在點 t。
發現以上的限制都是對於 "與某個點相鄰的邊" 之間的限制, 自然地認為要維護這個來輔助貪心演算法的判斷。


進一步的思路

有了基本的思路, 就可以思考出演算法大致的框架了。
首先從小到大列舉數字, 找出在滿足前面數字形成的限制下其可以到達的標號最小的節點, 然後把限制加上。
最樸素的找最小節點的思路就是列舉節點, 優化這個樸素思路的方法建立在如下事實上:

如果以當前數字的初始節點為根, 那麼對於任意非根節點, 其與其父親當作當前數字的最終節點所產生的限制是高度相似的

那麼就可以通過 dfs 來查詢當前列舉到的數字能夠停留的標號最小的節點。


最終思路

僅剩的問題是如何維護與一個點相鄰的邊之間的相對順序。
首先最終這些邊的順序一定是一個序列。
對於 “讓這條邊是這個點相鄰邊中 第一個/最後一個 被刪除的邊” 這種限制, 可以加哨兵, 這樣就把所有的限制都轉化成 “一個邊要緊接著另一個邊之後刪” 了。


程式碼以及註釋

(時間有限, 對於程式碼僅做了一些最基本的註釋 僅供觀賞

#include<bits/stdc++.h>
using namespace std;

const int N = 2003;

int n, fa[N], p[N], deg[N];

int ct, hd[N], nt[N<<1], vr[N<<1];
    void ad(int x,int y) {nt[++ct]=hd[x], hd[x]=ct, vr[ct]=y;  }

int f[N][N], t[N][N], siz[N][N]; // 用於維護刪邊序列(鏈)的並查集, 一個集合的代表元就是刪邊序列的頭部, 用 t 記錄尾部, siz 記錄序列長度
    int fid(int *F, int x) {return F[x]==x ? x: F[x]=fid(F,F[x]);  }
        void mg(int x, int a, int b) { a=fid(f[x],a),b=fid(f[x],b); f[x][a]=b; t[x][b]=t[x][a]; siz[x][b]+=siz[x][a]; }
        
int bst; //這個變數用來記錄當前數字能到達的標號最小的節點
void dfs(int x)
{   int ff=fid(f[x],fa[x]);
    	if(fa[x])
   		{
   			int ttt=fid(f[x],n+1);
    		if( !(t[x][ff]==0 && ttt==n+1 && siz[x][ff]+siz[x][ttt]!=deg[x]+2) )
            if(ff!=ttt && t[x][ttt]==n+1 && ff==fa[x]) bst=min(bst,x);
   		}
    for(int i=hd[x],y=vr[i];i;i=nt[i],y=vr[i]) if(y!=fa[x]) {
        int tt=fid(f[x],y);
        if(t[x][ff]==0 && tt==n+1 && siz[x][ff]+siz[x][tt]!=deg[x]+2) continue;
        if( ff!=tt && ff==fa[x] && t[x][tt]==y) fa[y]=x,dfs(y);
    }
}

int main()
{

    int T; scanf("%d",&T);
    while(T--)
    { scanf("%d",&n);
        for(int i=1;i<=n;++i)scanf("%d",&p[i]);
      if(n==1) //特判一下
      {   puts("1");
          continue;
      }
      ct=0;
      memset(hd,0,sizeof hd); memset(deg,0,sizeof deg);
        for(int i=1,x,y; i<n; ++i)scanf("%d%d",&x,&y), ad(x,y),ad(y,x), ++deg[x],++deg[y];
      for(int i=1;i<=n;++i)
        for(int j=0;j<=n+1;++j)
            f[i][j]=t[i][j]=j, siz[i][j]=1;
       		// 以上基本都是輸入初始化
      for(int i=1;i<=n;++i)
      { fa[p[i]]= 0; bst= n+2;
          dfs(p[i]);
            cout << bst << ' ';
        int y=bst, x=fa[bst], z=n+1;
        while(y)
        { mg(y,x,z);
          z=y, y=x, x=fa[x];
        }
      }
      putchar('\n');
    }

return 0;
}