題目連結
Luogu
Codeforces
題意簡述
某公司中有 \(n\) 名員工。為方便起見,將這些員工從 1 至 \(n\) 編號。起初,員工之間相互獨立。接下來,會有以下 \(m\) 次操作:
-
員工 \(y\) 成為員工 \(x\) 的上司。保證此前 \(x\) 沒有上司。
-
員工 \(x\) 拿到一份檔案並簽字,隨後交給他的上司。他的上司簽字後,再交給更上一級。依此類推,直到檔案傳遞到的那個人沒有上司為止。
-
詢問員工 \(x\) 是否在第 \(i\) 件檔案上籤過字。檔案編號為上一件檔案的編號再加 1,第一件檔案的編號為 1。如果是,輸出
YES
,否則輸出NO
。
解法說明
顯然,我們可以將員工之間的關係看作森林,將每個員工看作一個節點,其與上司的關係看作一條邊。之所以不是一棵樹,是因為在 \(m\) 次操作中,有些人可能並沒有被指定上司,所以員工之間的關係很可能並不是一棵樹而是森林。
透過觀察題面可以發現,一個員工在成為另一個員工的上司後,就不會再有更改了。由於線上操作過於麻煩,我們可以考慮離線。
具體離線方法如下:
-
對於操作 1,直接連邊即可,不過這裡還要線上維護一個並查集;
-
對於操作 2,分別記下第一個和最後一個對檔案簽字的員工,後者就是前者所在的連通塊的根,利用並查集查詢;
-
對於操作 3,分別記下員工編號及檔案編號,離線回答。
接下來分析如何回答詢問。可以發現,如果詢問的員工 \(x\) 在 最開始看到檔案 \(i\) 的員工與最後看到檔案 \(i\) 的員工之間的鏈上,那麼 \(x\) 就看過檔案。所以,問題就被轉化為了判斷 \(x\) 是否在這條鏈上。
考慮如何判斷。
設 \(st\) 為鏈的起始點,\(ed\) 為截止點,可推得如 \(x\) 在鏈上,則 \(\text{lca}(x,st) = x\) 且 \(\text{lca}(x,ed) = ed\),維護一個 LCA 即可求解。我這裡用的是樹剖求 LCA,倍增也可以。
還有一些細節需要注意。由於員工之間的關係是森林而非一棵樹,所以我們在預處理樹剖時應列舉每個點,如果該點是其所屬的連通塊的根,就對其進行一次預處理,且回答詢問時應首先判斷 \(x\) 與 \(st\)、\(ed\) 是否在同一連通塊內,如果不在直接輸出 NO
,否則再執行下一步操作。
剩餘細節詳見下面程式碼中的註釋。
透過程式碼
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define PII pair<int,int>
#define mp make_pair
const int N=1e5+10;
namespace IO{
//快讀
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
//快寫
inline void write(int x){
if(x<0){
putchar('-');
x=-x;
}
if(x>9){
write(x/10);
}
putchar(x%10+'0');
}
}
using namespace IO;
namespace code{
//鏈式前向星存圖
int head[N],tot;
struct node{
int ver,next;
}t[N<<1];
void add(int x,int y){
t[++tot].ver=y,t[tot].next=head[x],head[x]=tot;
}
//並查集
int fa[N];
int getfa(int x){
if(fa[x]==x){
return x;
}
return fa[x]=getfa(fa[x]);
}
//樹鏈剖分
int fat[N],size[N],son[N],deep[N],top[N];
void dfs1(int x){
size[x]=1;
int maxson=-1;
for(int i=head[x];i;i=t[i].next){
int y=t[i].ver;
if(y==fat[x]){
continue;
}
fat[y]=x;
deep[y]=deep[x]+1;
dfs1(y);
if(size[y]>maxson){
maxson=size[y];
son[x]=y;
}
size[x]+=size[y];
}
}
void dfs2(int x,int from){
top[x]=from;
if(!son[x]){
return;
}
dfs2(son[x],from);
for(int i=head[x];i;i=t[i].next){
int y=t[i].ver;
if(y==son[x]||y==fat[x]){
continue;
}
dfs2(y,y);
}
}
//求LCA
int lca(int x,int y){
while(top[x]!=top[y]){
if(deep[top[x]]<deep[top[y]]){
swap(x,y);
}
x=fat[top[x]];
}
if(deep[x]<deep[y]){
return x;
}
return y;
}
//主程式
int n,m,f_tot,q_tot;
PII file[N],query[N];
void solve(){
n=read(),m=read();
for(int i=1;i<=n;i++){//並查集預處理
fa[i]=i;
}
for(int i=1;i<=m;i++){//離線處理
int op=read();
if(op==1){
int x=read(),y=read();
add(y,x);//單向邊
fa[x]=getfa(y);//線上維護並查集
}else if(op==2){
int x=read();
file[++f_tot]=mp(x,getfa(x));
}else{
int x=read(),y=read();
query[++q_tot]=mp(x,y);
}
}
for(int i=1;i<=n;i++){//列舉所有點
if(getfa(i)==i){//判斷是否為所在連通塊的根
deep[i]=1;//樹剖預處理
fat[i]=i;
dfs1(i);
dfs2(i,i);
}
}
for(int i=1;i<=q_tot;i++){
int x=query[i].first,y=query[i].second,st=file[y].first,ed=file[y].second;
if(getfa(x)!=getfa(st)){//是否在同一個連通塊
printf("NO\n");
continue;
}
if(lca(x,st)==x&&lca(x,ed)==ed){//判斷
printf("YES\n");
}else{
printf("NO\n");
}
}
}
}
using namespace code;
signed main(){
solve();
return 0;
}