字典樹專題

potential-star發表於2024-04-20

01 trie

找序列中任意兩數的最大異或和

int n, m;
int a[N];
int idx=0;
int ch[N*31][2];
void insert(int x){
	int p=0;
	for(int i=30;i>=0;i--){
		int u=(x>>i)&1;
		if(!ch[p][u])ch[p][u]=++idx;
		p=ch[p][u];
	}
}
int query(int x){
	int p=0;int ans=0;
	for(int i=30;i>=0;i--){
		int u=(x>>i)&1;
		if(ch[p][!u]){
			p=ch[p][!u];
			ans+=1<<i;
		}
		else p=ch[p][u];
		}
		return ans;
}
void solve(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		insert(a[i]);
	}
	int ans=0;
	for(int i=1;i<=n;i++){
		ans=max(ans,query(a[i]));
	}
	cout<<ans<<endl;
}

奶牛異或

題意:找出最大區間異或和,相同的時候取右端點最小的,再相同取長度最短的

Solution:利用異或的可逆性,先算區間異或字首和,問題轉化回上題,注意處理邊界,對於只有一個數的情況,直接賦初值解決

int n, m;



int a[N];
int idx=0;
int ch[N*31][2];
void insert(int x){
	int p=0;
	for(int i=20;i>=0;i--){
		int u=(x>>i)&1;
		if(!ch[p][u])ch[p][u]=++idx;
		p=ch[p][u];
	}
}
int query(int x){
	int p=0;int ans=0;
	for(int i=20;i>=0;i--){
		int u=(x>>i)&1;
		if(ch[p][!u]){
			p=ch[p][!u];
			ans+=1<<i;
		}
		else p=ch[p][u];
		}
		return ans;
}
void solve(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		
	}
	for(int i=1;i<=n;i++)a[i]^=a[i-1];

	int ans=a[1];
	map<int,int>mp;
	int ansl=1,ansr=1;
	mp[0]=0;
	for(int i=1;i<=n;i++){
		if(i>=2){
			int tmp=query(a[i]);
		if(ans<tmp){
			ans=tmp;
			ansr=i;
			ansl=mp[tmp^a[i]]+1;
		}
		}
		insert(a[i]);
		mp[a[i]]=i;
	}
	cout<<ans<<" "<<ansl<<" "<<ansr<<endl;
}

繼續深入這個快速找異或和最大值的演算法,這次將目標轉化到樹上 The XOR-longest Path

題意:給定一棵n個點的帶邊權樹,求樹上最長的異或和路徑。

Solution:首先考慮將問題轉化成點之間的問題,我們做從根開始的樹上異或字首和d[u],對於任意一條路徑u到v的異或和都恰好是$$d[u]\oplus d[v]$$,其中lca的部分由於異或特性恰好抵消。

int n, m;
int a[N];
int idx=0;
int ch[N*31][2];
void insert(int x){
	int p=0;
	for(int i=30;i>=0;i--){
		int u=(x>>i)&1;
		if(!ch[p][u])ch[p][u]=++idx;
		p=ch[p][u];
	}
}
int query(int x){
	int p=0;int ans=0;
	for(int i=30;i>=0;i--){
		int u=(x>>i)&1;
		if(ch[p][!u]){
			p=ch[p][!u];
			ans+=1<<i;
		}
		else p=ch[p][u];
		}
		return ans;
}
struct edge{int v,w;};
int d[N];
vector<edge>e[N];
void dfs(int u,int fa){

	for(auto [v,w]:e[u]){
		if(v==fa)continue;
		d[v]=d[u]^w;
		dfs(v,u);
		
	}
	
}
void solve(){
	cin>>n;
  for(int i=1;i<=n-1;i++){
  	int u,v,w;cin>>u>>v>>w;
  	e[u].push_back({v,w});
  	e[v].push_back({u,w});
  }
   dfs(1,0);
   	for(int i=1;i<=n;i++)insert(d[i]);
	int ans=0;
	for(int i=1;i<=n;i++){
		ans=max(ans,query(d[i]));
	}
	cout<<ans<<endl;
}

Vitya and Strange Lesson

https://codeforces.com/problemset/problem/842/D

問題描述

\(mex\) 是一個序列中沒有出現過的最小非負整數。

給出你一個長度為 \(n\) 的非負整數序列以及 \(m\) 個詢問,每次詢問先給你一個整數 \(x\) ,然後:

  • 把序列中所有數異或上 \(x\)
  • 輸出序列的 mex

注意,在每個詢問過後序列是發生變化的。

Solution:考慮異或的結合律

由於異或是對二進位制的每一位單獨進行操作,每一位之間相互不會影響,所以我們考慮每個數字的二進位制表示並對每一位進行思考。

對於詢問的一個數 x,在原來的字典樹上,假設此時本來應該向左兒子走,但是如果 x 的這一位是 1,即相當於進行左右子樹的交換操作,那麼我們就應該調轉方向,向右兒子走。

如果現在輸入的是 \(x_{i}\),那麼現在真正進行操作的 x 就應該是 \(x=x1⊕x2⊕x3…xi−1⊕xi\)

所以現在已經解決了 m 個查詢的問題,只需要思考如何在 01Tire 上查詢 mex,這比較簡單,只需要判斷當前子樹是否為滿二叉樹,如果是隻能調轉方向,否則儘量向著 0 的方向走,以保證 mex最小的性質。

但我們還必須注意一個坑點:同一個數不能被插入兩次!因為這會導致子樹的大小發生變化,造成答案錯誤。

積累:mex具有二分性,性質是前mex-1個數是不是都出現過

實現方面值得一提的是query內部的邏輯,也是本題的核心。

  • 考慮當前二叉樹已經較小的一邊二叉樹已經滿了,我們走到相反方向,發現如果這個點不存在,那依然加上當前為貢獻,但mex是最小的,所以後面位全是0,直接return。
  • 另一方面,如果當前二叉樹沒滿,我們走入較小部分,如果也不存在這個節點,那隻可能說明這裡面一個數都沒有。反證法:若存在x在面,那建立\(trie\)的時候就會把這個節點建出來。所以說明當前以及後面位全是0才是正確的。
int n, m;
int a[N];
int idx=0;
int ch[N*31][2];
bool st[N];
int cnt[N*31];
void insert(int x){
	int p=0;
	for(int i=30;i>=0;i--){
		int u=(x>>i)&1;
		if(!ch[p][u])ch[p][u]=++idx;
		p=ch[p][u];cnt[p]++;
	}
}
//裡面一個數都沒有的時候就是答案找到的時候
int query(int x){
	int p=0;int ans=0;
	for(int i=30;i>=0;i--){
		int u=(x>>i)&1;
		int nx=ch[p][u];
		if(cnt[nx]<(1<<i)){
			p=ch[p][u];
			if(p==0)return ans;
		}
		else {
			p=ch[p][!u];
			ans|=(1<<i);
			if(p==0)return ans;
		}
	}
	return ans;	
}
void solve(){
	cin>>n;cin>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		if(st[a[i]]==0){
			st[a[i]]=true;
			insert(a[i]);
			}
	}
	
	int sum=0;
	for(int i=1;i<=m;i++){
		int x;cin>>x;
		sum^=x;
		int ans=query(sum);
		cout<<ans<<endl;
	}
}

VK Cup 2018 - Round 1C

題意:給出a,b兩個序列,要求調整b序列的順序,要求調整後a序列異或上b序列的結果字典序最小

Solution:考慮我們需要讓前面的儘可能小,所以我們先將b序列中的樹全部插入到trie裡,每次按順序為a中數找出異或後最小的數。值得注意的是每次確定當前位的策略的時候需要刪掉當前點的編號的出現次數。

nt n, m;
int a[N];
int cnt[N*31];
int idx=0;
int ch[N*31][2];
void insert(int x){
	int p=0;
	for(int i=30;i>=0;i--){
		int u=(x>>i)&1;
		if(!ch[p][u])ch[p][u]=++idx;
		p=ch[p][u];
		cnt[p]++;
	}
}
int query(int x){
	int p=0;int ans=0;
	for(int i=30;i>=0;i--){
		int u=(x>>i)&1;
		if(ch[p][u]&&cnt[ch[p][u]]){
			p=ch[p][u];
				cnt[p]--;
			//ans+=1<<i;
		}
	else{p=ch[p][!u];
	cnt[p]--;
	ans+=1<<i;
	};
		}
		return ans;
}
void solve(){
	cin>>n;
	for(int  i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++){
		int x;cin>>x;
		insert(x);
	}
	for(int i=1;i<=n;i++){
		cout<<query(a[i])<<" ";
	}
	
}

Xor-MST

最小異或生成樹:給出n個點,任意兩個點連邊的代價是兩個點權值異或的結果,求最小生成樹

Solution:感覺還是有點模糊。大概是字典樹上分治,我們考慮每個字典樹上有分叉的點,對於每個分叉的點兩邊想要連通必須在當前這位發出1<<d的代價,至於下面的位我們考慮貪心,在size小的一邊對每個數在另一邊尋找最適合(也就是結果最小)自己的匹配的數,對於這個過程由於每次都是從當前位的下一位開始迴圈,這裡給出一種遞迴的查詢寫法。

注意事項:不能全部開longlong,會mle,反思看哪些地方需要用longlong,還有就是如果字典樹封裝好的空間會最佳化當前

int n, m;
int a[N];
int ch[N*30][2];
int idx=0;
vector<int>e[N*30];
ll ans=0;
void insert(int x){
	int p=0;
	for(int i=29;i>=0;i--){
		int u=(x>>i)&1;
		if(!ch[p][u])ch[p][u]=++idx;
		p=ch[p][u];
		e[p].push_back(x);
	}
}
int query(int p,int d,int x){
	if(d<0)return 0;
	int u=(x>>d)&1;int res=0;
	if(ch[p][u]){
		p=ch[p][u];
		res+=query(p,d-1,x);
		}
	else {
		p=ch[p][!u];
		res+=1<<d;
		res+=query(p,d-1,x);
	}
	return res;
}
void mdiv(int p,int d){
	if(d<0)return ;
	int ls=ch[p][0],rs=ch[p][1];
	if(ls&&rs){
		int mn=inf;
		if(e[ls].size()>e[rs].size())swap(ls,rs);
		for(auto x:e[ls]){
			mn=min(mn,query(rs,d-1,x));
			
		}
		ans+=(ll)mn+(1LL<<d);
		
	}
	if(ls)mdiv(ls,d-1);
	if(rs)mdiv(rs,d-1);
}
void solve(){
	cin>>n;
	for(int i=1;i<=n;i++){
	int x;cin>>x;
	insert(x);
	}
	mdiv(0,29);
	cout<<ans<<endl;

}

相關文章