2024 Noip 做題記錄(二)

DaiRuiChen007發表於2024-09-23

\(\text{By DaiRuiChen007}\)



Round #5 - 2024.9.14

A. [P9906] Cover

Problem Link

題目大意

給定長度為 \(k\) 的序列,從一個點出發,每次向左或向右一步,共走 \(n\) 步,每個位置上顯示最後一次被經過的時刻,求能生成多少合法序列。

資料範圍:\(n,k\le 5000\)

思路分析

注意到能到達的點一定是一段區間,可以倒序 dp,設 \(f_{i,j}\) 表示最後 \(n\sim j\) 步經過的範圍是一個長度為 \(i\) 的區間,並且我們欽定第 \(j\) 步在某個位置上顯示。

那麼最後這一步一定在這個長度為 \(i\) 的區間的左端點或右端點上,因此 \(f_{i,j}\to f_{i+1,j-1},f_{i+1,j-i}\) 表示向哪個方向擴充一步。

但是我們可以走來回,因此 \(f_{i,j}\to f_{i+1,j'}\) 的轉移實際上轉移到了 \(f_{i+1,j'},f_{i+1,j'-2}\dots\),字尾和一下即可。

答案就是 \(\sum (k-i+1)f_{i,j}\),注意 \(i=1\) 時不能走來回。

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5005,MOD=1e9+7;
ll f[MAXN],g[MAXN];
inline void add(ll &x,ll y) { x=(x+y>=MOD)?x+y-MOD:x+y; }
signed main() {
	int n,k;
	scanf("%d%d",&n,&k);
	ll ans=0; f[n]=1;
	for(int i=1;i<k;++i) {
		memset(g,0,sizeof(g));
		for(int j=n;j>=1;--j) {
			add(g[j-1],f[j]);
			if(j>=i) add(g[j-i],f[j]);
		}
		memcpy(f,g,sizeof(f));
		for(int j=n;j>=1;--j) {
			if(i>1) add(f[j],f[j+2]);
			ans=(ans+f[j]*(k-i))%MOD;
		}
	}
	printf("%lld\n",ans);
	return 0;
}



B. [P10829] Tuple

Problem Link

題目大意

定義一個圖是好的,當且僅當圖上恰有一個點,或可以由三個大小相等的好圖各選出一個點連出三元環得到。

給定一個 \(n\) 個點 \(m\) 條邊的無向圖,判定該圖是否是好的。

資料範圍:\(n\le 2\times 10^5,m\le 3\times 10^5\)

思路分析

考慮如何刻畫好的圖,在無向圖上不好處理問題,注意到這張圖是邊仙人掌,可以建出圓方樹。

那麼原圖上的每個環對應一個方點,最特殊的顯然是最後一次加入的環,即某個方點刪去後整棵樹變成大小相同的三部分,且每部分都是好的。

那麼這個方點顯然就是圓方樹的重心,容易證明一張圖是好的當且僅當其圓方樹的點分樹是完美三叉樹。

實現的時候可以在建圓方樹時直接判斷每個邊雙聯通分量大小是否為 \(3\),點分治的時候要維護一下深度方便判定大小相等。

時間複雜度 \(\mathcal O(n+m)\)

程式碼呈現

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
int n,m,tot,dfn[MAXN],low[MAXN],dcnt,stk[MAXN],tp;
vector <int> G[MAXN],E[MAXN];
void link(int u,int v) { E[u].push_back(v),E[v].push_back(u); }
void tarjan(int u) {
	dfn[u]=low[u]=++dcnt,stk[++tp]=u;
	for(int v:G[u]) {
		if(!dfn[v]) {
			tarjan(v),low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]) {
				int k,c=1; link(u,++tot);
				do ++c,link(k=stk[tp--],tot); while(k^v);
				if(c!=3) puts("ne"),exit(0);
			}
		} else low[u]=min(low[u],dfn[v]);
	}
}
int qk(int x) {
	int c=0;
	for(;x>1;x/=3,++c) if(x%3) puts("ne"),exit(0);
	return c;
}
int siz[MAXN],cur[MAXN];
bool vis[MAXN];
bool dfs1(int u,int k) {
	int cnt=0; vis[u]=true;
	for(int v:E[u]) cnt+=!vis[v];
	if(cnt!=(k?3:0)) puts("ne"),exit(0);
	function<void(int,int)> dfs2=[&](int x,int fz) {
		siz[x]=1;
		for(int y:E[x]) if(!vis[y]&&y!=fz) dfs2(y,x),siz[x]+=siz[y];
	};
	dfs2(u,0);
	for(int v:E[u]) if(!vis[v]) {
		int rt=0,mx=siz[v];
		function<void(int,int)> dfs3=[&](int x,int fz) {
			cur[x]=mx-siz[x];
			for(int y:E[x]) if(!vis[y]&&y!=fz) {
				dfs3(y,x),cur[x]=max(cur[x],siz[y]);
			}
			if(!rt||cur[x]<cur[rt]) rt=x;
		};
		dfs3(v,u);
		if(!dfs1(rt,k-1)) puts("ne"),exit(0);
	}
	return true;
}
signed main() {
	scanf("%d%d",&n,&m),tot=n;
	for(int i=1,u,v;i<=m;++i) {
		scanf("%d%d",&u,&v);
		G[u].push_back(v),G[v].push_back(u);
	}
	for(int i=1;i<=n;++i) {
		sort(G[i].begin(),G[i].end());
		if(unique(G[i].begin(),G[i].end())!=G[i].end()) return puts("ne"),0;
	}
	tarjan(1);
	for(int i=1;i<=n;++i) if(!dfn[i]) return puts("ne"),0;
	int rt=0,mx=tot;
	function<void(int,int)> dfs3=[&](int x,int fz) {
		siz[x]=1;
		for(int y:E[x]) if(!vis[y]&&y!=fz) {
			dfs3(y,x),cur[x]=max(cur[x],siz[y]),siz[x]+=siz[y];
		}
		cur[x]=max(cur[x],mx-siz[x]);
		if(!rt||cur[x]<cur[rt]) rt=x;
	};
	dfs3(1,0);
	puts(dfs1(rt,qk(n))?"da":"ne");
	return 0;
}



C. [P10822] Subset

Problem Link

題目大意

給定 \(a_1\sim a_n\)\(q\) 次詢問 \([l,r]\) 有多少子區間本質不同顏色數為奇數。

資料範圍:\(n,q\le 5\times 10^5\)

思路分析

\(a_I\) 上一次出現為 \(pre_i\),那麼掃描線 \(i-1\to i\) 時就會把 \(l\in(pre_i,i]\) 範圍的區間顏色數 \(+1\)

維護奇偶性,我們要支援區間反轉區間歷史和。

類似區間加區間歷史和,構造一個歷史和標記,每個線段樹節點維護:每次打歷史標記時,當前節點的懶標記是處於反轉還是未反轉狀態,對這兩種情況分別記錄次數即可。

資訊合併和標記下傳都是容易的。

時間複雜度:\(\mathcal O((n+q)\log n)\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5e5+5;
int n,q;
struct SegmentTree {
	int sum[MAXN<<2],len[MAXN<<2];
	bool tg[MAXN<<2];
	int t0[MAXN<<2],t1[MAXN<<2];
	ll hsum[MAXN<<2];
	void adt(int p,int z0,int z1,int rv) {
		hsum[p]+=1ll*z0*sum[p]+1ll*z1*(len[p]-sum[p]);
		if(tg[p]) t0[p]+=z1,t1[p]+=z0;
		else t0[p]+=z0,t1[p]+=z1;
		if(rv) tg[p]^=1,sum[p]=len[p]-sum[p];
	}
	void psd(int p) {
		adt(p<<1,t0[p],t1[p],tg[p]),adt(p<<1|1,t0[p],t1[p],tg[p]),t0[p]=t1[p]=tg[p]=0;
	}
	void psu(int p) { sum[p]=sum[p<<1]+sum[p<<1|1],hsum[p]=hsum[p<<1]+hsum[p<<1|1]; }
	void init(int l=1,int r=n,int p=1) {
		len[p]=r-l+1;
		if(l==r) return ;
		int mid=(l+r)>>1;
		init(l,mid,p<<1),init(mid+1,r,p<<1|1);
	}
	void upd(int ul,int ur,int l=1,int r=n,int p=1) {
		if(ul<=l&&r<=ur) return adt(p,0,0,1),void();
		int mid=(l+r)>>1; psd(p);
		if(ul<=mid) upd(ul,ur,l,mid,p<<1);
		if(mid<ur) upd(ul,ur,mid+1,r,p<<1|1);
		psu(p);
	}
	ll qry(int ul,int ur,int l=1,int r=n,int p=1) {
		if(ul<=l&&r<=ur) return hsum[p];
		int mid=(l+r)>>1; psd(p);
		if(ur<=mid) return qry(ul,ur,l,mid,p<<1);
		if(mid<ul) return qry(ul,ur,mid+1,r,p<<1|1);
		return qry(ul,ur,l,mid,p<<1)+qry(ul,ur,mid+1,r,p<<1|1);
	}
}	T;
ll ans[MAXN];
int a[MAXN],p[MAXN];
vector <array<int,2>> Q[MAXN];
signed main() {
	ios::sync_with_stdio(false);
	cin>>n,T.init();
	for(int i=1;i<=n;++i) cin>>a[i];
	cin>>q;
	for(int i=1,l,r;i<=q;++i) cin>>l>>r,Q[r].push_back({l,i});
	for(int i=1;i<=n;++i) {
		T.upd(p[a[i]]+1,i),p[a[i]]=i,T.adt(1,1,0,0);
		for(auto z:Q[i]) ans[z[1]]=T.qry(z[0],i);
	}
	for(int i=1;i<=q;++i) cout<<ans[i]<<"\n";
	return 0;
}



*D. [P10882] Triangle

Problem Link

題目大意

給定 \(a_1\sim a_n\)\(q\) 次詢問 \(l,r\),找出一組 \(l\le i<j<k\le r\) 滿足存在三角形以 \(a_i,a_j,a_k\) 為三邊邊長,並最小化 \(a_i+a_j+a_k\)

資料範圍:\(n\le2.5\times 10^5,q\le5\times 10^5,V\le 10^7\)

思路分析

考慮對值域倍增分塊。

對於每個塊 \(B_k=[2^k,2^{k+1})\),處理每個詢問 \(l,r\),如果區間 \([l,r]\) 中有 \(\ge 3\)\(B_k\) 中的元素,那麼取出任意三個都能構成三角形,我們只要取出最小的三個即可。

對每個 \([l,r]\) 我們找出最小的 \(k\),更新答案後彈出,如果其他情況想要更優,一定至少包含一個 \(<2^k\) 的元素。

我們發現,對於 \(i\in [0,k)\)\([l,r]\) 中至多有 \(2\)\(B_i\) 中的元素,因此 \([l,r]\)\(<2^k\) 的元素數量是 \(\mathcal O(\log V)\) 級別的。

我們考慮最終的三角形中有多少個元素 \(<2^k\)

如果有 \(\ge 2\) 個元素 \(<2^k\),那麼我們可以列舉中間的數,顯然最大的數沒有下界,一定是越小越好。

因此三角形最長的兩條邊排序後一定是相鄰的,因此最長邊的範圍也在 \([0,2^k)\) 間,或者是 \(B_k\)\([l,r]\) 中的最小元素。

那麼我們只要考慮 \(\mathcal O(\log V)\) 個元素,設他們從小到大排序之後為 \(t_1\sim t_m\),列舉 \(i\),找到最小的 \(t_j>t_{i+1}-t_i\),那麼 \((t_i,t_{i+1},t_j)\) 構成三角形。

注意到 \(i\) 變大時,若 \(t_{i+1}-t_i\) 也變大,那麼 \(t_j\) 也變大,三條邊都不優,所以我們只要考慮 \(t_{i+1}-t_i\) 的字首最小值,可以雙指標線性維護。

實現程式碼的時候我們可以按 \(k\) 從小到大掃描,字首和計算區間中有多少 \(\in B_k\) 的數,如果 \(\ge 3\) 個就要求區間前三小值,可以線段樹維護,由於每個區間詢問後直接彈出,這部分複雜度 \(\mathcal O(n\log V+q\log n)\)

這樣的數如果 \(\le 2\) 個,那麼我們要把他們全找出來,找出 \(\ge l\) 的最小的一個,和 \(\le r\) 最大的一個,一定能找出所有的數,先加入較小值,最終的序列就已經有序了。

這部分總的複雜度為 \(\mathcal O((n+q)\log V)\)

然後考慮 \(<2^k\) 的元素恰好有一個的情況,顯然此時這個元素是最小值。

不妨設這個元素為 \(a_x\),我們列舉這個 \(a_x\) ,反向考慮什麼樣的區間會列舉到 \(x\)

設 $a_x $屬於 \(B_i\),容易發現如果某個詢問區間 \([l,r]\) 經過 \(B_i\) 時沒有找到 \(\ge 3\) 個數,\(a_x\) 才會 \(<2^k\)

因此對每個 \(a_x\),找出他左邊和右邊第二個和 \(a_x\) 同屬 \(B_i\) 的元素 \(a_L,a_R\),一個詢問區間 \([l,r]\) 會列舉到 \(a_x\),一定有 \(L<l\le r<R\),所以對於 \(a_x\),只要考慮 \([L,R]\) 這個區間即可。

對每個塊 \(B_i\) 逐塊處理,容易發現對於同一個塊,每個 \(a_x\) 對應的區間大小總和是 \(\mathcal O(n)\) 級別的。

因此所有 \(a_x\) 對應的區間大小總和是 \(\mathcal O(n\log V)\) 級別的。

對於每個 \(a_x\),我們就是要在 \([L,R]\) 中選出兩個 \(>a_x\) 的數 \(a_i,a_j\),滿足 \(|a_i-a_j|<a_x\),然後把 \((i,j,x)\) 看成一組支配對,最後從左到右掃描線一遍維護每個詢問區間裡面的最小支配對即可。

那麼現在我們只需要將支配對數量最佳化到一個可以接受的量級,我們設 \(b_i=\left\lfloor\dfrac {a_i}{a_x}\right\rfloor\),那麼 \(|a_i-a_j|<a_x\) 的必要條件是 \(|b_i-b_j|\le 1\)

如果 \(b_i=b_j\),此時任意的一對 \(a_i,a_j\) 都是滿足限制的,相當於最小的 \(a_i+a_j\),這是經典支配對結論,我們對每個 \(b_i\) 維護字尾最小值單調棧。

考慮插入 \(a_i\) 時,棧內每個元素 \(a_j\) 是否會與 \(a_i\) 形成支配對,如果 \(a_j\ge a_i\),那麼 \(a_j\) 會被 \(a_i\) 彈出,可以把他們視為支配對,這樣的支配對總數和區間長度成線性。

然後考慮所有 \(a_j<a_i\) 的點,由於他們在單調棧上,因此 \(a_j<a_k<a_i\) 時也有 \(j<k<i\),那麼 \((i,j)\) 顯然不如 \((j,k)\)

因此只有 \(a_j<a_i\)\(j\) 最大的 \(a_j\) 可以與 \(a_i\) 形成支配對,這就是彈棧後剩餘的棧頂,這樣的支配對總數和區間長度也成線性。

然後考慮 \(|b_i-b_j|=1\) 的情況。

還是考慮支配對,不妨設 \(j<i\)\(b_j=b_i-1\),剩餘的情況可以翻轉區間 \([L,R]\) 做。

那麼如果有兩個 \(j,k\) 同時滿足 \(b_j=b_k=b_i-1\),那麼 \(a_j,a_k\) 一定合法且更優。

因此支配對一定是 \([L,i)\) 中第一個 \(b_j=b_i-1\)\(j\) 對應的 \(a_j\)

這部分支配對總數和區間長度依然成線性。

因此所有支配對數量的總和是 \(\mathcal O(n\log V)\) 級別的,樹狀陣列維護掃描線即可。

時間複雜度 \(\mathcal O(n\log V\log n+q\log V)\)

程式碼呈現

#include<bits/stdc++.h>
using namespace std;
typedef array<int,3> info;
const int MAXN=2.5e5+5,MAXQ=5e5+5,MAXV=1e7+5,inf=1e9;
const void chkmin(int &x,const int &y) { x=x<y?x:y; }
info operator +(info f,info g) {
	for(int x:f) for(int &y:g) if(y>x) swap(x,y);
	return g;
}
int n,q,a[MAXN],bl[MAXN],ql[MAXQ],qr[MAXQ],ans[MAXQ],cnt[MAXN],pre[MAXN],nxt[MAXN];
bool vis[MAXQ];
vector <int> arr[MAXQ];
struct zkwSegt {
	static const int N=1<<18;
	info tr[N<<1];
	void init(int k) {
		for(int i=0;i<N;++i) tr[i+N].fill(inf);
		for(int i=1;i<=n;++i) if(bl[i]==k) tr[i+N][0]=a[i];
		for(int i=N-1;i;--i) tr[i]=tr[i<<1]+tr[i<<1|1];
	}
	info qry(int l,int r) {
		info s{inf,inf,inf};
		for(l+=N-1,r+=N+1;l^r^1;l>>=1,r>>=1) {
			if(~l&1) s=s+tr[l^1];
			if(r&1) s=s+tr[r^1];
		}
		return s;
	}
}	S;
void calc(int &s,const vector<int>&w) {
	int k=w.size(),i=1;
	while(i+1<k&&w[i-1]+w[i]<=w[i+1]) ++i;
	if(i+1>=k) return ;
	int j=i-1,lst=w[i+1]-w[i];
	while(j>0&&w[j-1]>lst) --j;
	chkmin(s,w[j]+w[i]+w[i+1]);
	for(++i;i+1<k;++i) if(w[i+1]-w[i]<lst) {
		for(lst=w[i+1]-w[i];j>0&&w[j-1]>lst;--j);
		chkmin(s,w[j]+w[i]+w[i+1]);
	}
}
vector <array<int,2>> M[MAXN],Q[MAXN];
void ins(int x,int y,int z)	{ M[max({x,y,z})].push_back({min({x,y,z}),a[x]+a[y]+a[z]}); }
vector <int> stk[MAXV];
int b[MAXN],pos[MAXV];
void gen(int id,int l,int r) {
	int x=a[id];
	for(int i=l;i<=r;++i) if(a[i]>x) b[i]=a[i]/x,stk[b[i]].clear();
	for(int i=l;i<=r;++i) if(a[i]>x) {
		vector<int>&s=stk[b[i]];
		while(s.size()&&a[s.back()]>=a[i]) ins(s.back(),id,i),s.pop_back();
		if(s.size()) ins(s.back(),id,i);
		s.push_back(i);
	}
	for(int i=l;i<=r;++i) if(a[i]>x) pos[b[i]]=pos[b[i]-1]=0;
	for(int i=l;i<=r;++i) if(a[i]>x) {
		int j=pos[b[i]-1];
		if(j&&x+a[j]>a[i]) ins(id,j,i);
		pos[b[i]]=i;
	}
	for(int i=l;i<=r;++i) if(a[i]>x) pos[b[i]]=pos[b[i]-1]=0;
	for(int i=r;i>=l;--i) if(a[i]>x) {
		int j=pos[b[i]-1];
		if(j&&x+a[j]>a[i]) ins(id,j,i);
		pos[b[i]]=i;
	}
}
struct FenwickTree {
	int tr[MAXN],s;
	void init() { fill(tr,tr+n+1,inf); }
	void upd(int x,int v) { for(;x;x&=x-1) tr[x]=min(tr[x],v); }
	int qry(int x) { for(s=inf;x<=n;x+=x&-x) s=min(s,tr[x]); return s; }
}	T;
signed main() {
	ios::sync_with_stdio(false);
	cin>>n>>q;
	for(int i=1;i<=n;++i) cin>>a[i],bl[i]=__lg(a[i]);
	for(int i=1;i<=q;++i) cin>>ql[i]>>qr[i],ans[i]=inf;
	for(int k=0;k<24;++k) {
		vector <int> idx;
		S.init(k),memset(cnt,0,sizeof(cnt));
		for(int i=1;i<=n;++i) if(bl[i]==k) cnt[i]=1,idx.push_back(i);;
		for(int i=1;i<=n;++i) pre[i]=(bl[i]==k?i:pre[i-1]);
		for(int i=n;i>=1;--i) nxt[i]=(bl[i]==k?i:nxt[i+1]);
		for(int i=1;i<=n;++i) cnt[i]+=cnt[i-1];
		for(int i=1;i<=q;++i) if(!vis[i]) {
			int w=cnt[qr[i]]-cnt[ql[i]-1];
			if(w<3) {
				if(w>1) {
					int x=nxt[ql[i]],y=pre[qr[i]];
					if(a[x]>a[y]) swap(x,y);
					arr[i].push_back(a[x]),arr[i].push_back(a[y]);
				} else if(w) arr[i].push_back(a[nxt[ql[i]]]);
			} else {
				info z=S.qry(ql[i],qr[i]);
				arr[i].push_back(z[0]),vis[i]=true;
				chkmin(ans[i],z[0]+z[1]+z[2]);
			}
		}
		int m=idx.size();
		for(int i=0;i<m;++i) gen(idx[i],i>1?idx[i-2]+1:1,i+2<m?idx[i+2]-1:n);
	}
	for(int i=1;i<=q;++i) calc(ans[i],arr[i]),Q[qr[i]].push_back({ql[i],i});
	T.init();
	for(int i=1;i<=n;++i) {
		for(auto z:M[i]) T.upd(z[0],z[1]);
		for(auto z:Q[i]) chkmin(ans[z[1]],T.qry(z[0]));
	}
	for(int i=1;i<=q;++i) {
		if(ans[i]==inf) cout<<"yumi!\n";
		else cout<<ans[i]<<"\n";
	}
	return 0;
}




Round #6 - 2024.9.15

A. [P10884] Increase

Problem Link

題目大意

給定 \(n\) 個元素,每個元素有高度和權值,求有多少個高度單調不降的子序列滿足元素權值和 \(\ge k\)

資料範圍:\(n\le 40\)

思路分析

考慮折半搜尋,從前往後搜出在 \(x\) 處結尾的 LIS,從後往前搜出在 \(y\) 處開始的 LIS。

那麼查詢答案相當於在 \(a_x\le a_y\) 的 LIS 上查詢有多少權值 \(\ge k-w\) 的序列。

可以離線下來二維數點,但直接暴力排序二分也能透過。

時間複雜度 \(\mathcal O(n^22^{n/2})\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=45,inf=1e9;
int n,m,a[MAXN],w[MAXN];
ll k,ans=0;
vector <ll> s[MAXN];
void dfs1(int i,int x,ll c) {
	if(i>n/2) return s[x].push_back(c);
	dfs1(i+1,x,c);
	if(a[i]>=a[x]) dfs1(i+1,i,c+w[i]);
}
void dfs2(int i,int x,ll c) {
	if(i<=n/2) {
		for(int j=0;j<=n/2;++j) if(a[j]<=a[x]) {
			ans+=s[j].end()-lower_bound(s[j].begin(),s[j].end(),k-c);
		}
		return ;
	}
	dfs2(i-1,x,c);
	if(a[i]<=a[x]) dfs2(i-1,i,c+w[i]);
}
signed main() {
	scanf("%d%lld",&n,&k);
	for(int i=1;i<=n;++i) scanf("%d%d",&a[i],&w[i]);
	dfs1(1,0,0);
	for(int i=1;i<=n/2;++i) sort(s[i].begin(),s[i].end());
	a[n+1]=inf;
	dfs2(n,n+1,0);
	printf("%lld\n",ans);
	return 0;
}



B. [P10240] Backpack

Problem Link

題目大意

給定 \(n\) 個元素,每個元素有權重 \(a_i\),進行若干輪操作,每次選出最多的元素使得 \(\sum a_i\le m\),多種方案選字典序最大一組方案,選出的元素都刪除,求多少輪後所有元素被刪空。

資料範圍:\(n\le 50000\)

思路分析

首先考慮怎麼選出最多元素,顯然會按從小到大的順序貪心取出前 \(k\) 個元素。

然後考慮怎麼確定一組解,可以逐位貪心,即先最大化標號最小元素的位置,可以二分一個 \(x\),那麼我們就要求 \([x,n]\) 範圍內前 \(k\) 小元素和 \(\le m\)

由於我們要動態刪除元素,因此可以樹狀陣列套值域線段樹樹,求出一組解的複雜度 \(\mathcal O(k\log^3n)\)

由於 \(\sum k=n\),因此總複雜度 \(\mathcal O(n\log ^3n)\)

從小到大貪心求 \(k\) 可以直接 std::multiset 維護。

時間複雜度:\(\mathcal O(n\log^3n)\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=50005;
const ll inf=1e18;
int n,m,a[MAXN],id[MAXN],rk[MAXN],vals[MAXN];
struct Segt {
	static const int MAXS=MAXN*200;
	int ls[MAXS],rs[MAXS],siz[MAXS],tot;
	ll sum[MAXS];
	void ins(int u,int op,int l,int r,int &p) {
		if(!p) p=++tot;
		siz[p]+=op,sum[p]+=op*vals[u];
		if(l==r) return ;
		int mid=(l+r)>>1;
		if(u<=mid) ins(u,op,l,mid,ls[p]);
		else ins(u,op,mid+1,r,rs[p]);
	}
	ll qry(int k,int l,int r,vector<int>&P) {
		if(l==r) return vals[l];
		int mid=(l+r)>>1,c=0;
		for(int p:P) c+=siz[ls[p]];
		if(k<=c) {
			for(int&p:P) p=ls[p];
			return qry(k,l,mid,P);
		} else {
			ll s=0;
			for(int&p:P) s+=sum[ls[p]],p=rs[p];
			return qry(k-c,mid+1,r,P)+s;
		}
	}
	int rt[MAXN];
	void ins(int x,int u,int op) { for(;x;x&=x-1) ins(u,op,1,n,rt[x]); }
	ll qry(int k,int x) {
		int s=0; vector <int> P;
		for(;x<=n;x+=x&-x) s+=siz[rt[x]],P.push_back(rt[x]);
		if(s<k) return inf;
		return qry(k,1,n,P);
	}
}	T;
multiset <int> A;
int solve() {
	int s=0,c=0;
	for(int i:A) {
		if(s+i>m) return c;
		s+=i,++c;
	}
	return c;
}
signed main() {
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]),id[i]=i;
	sort(id+1,id+n+1,[&](int x,int y){ return a[x]<a[y]; });
	for(int i=1;i<=n;++i) rk[id[i]]=i,vals[i]=a[id[i]];
	for(int i=1;i<=n;++i) A.insert(a[i]),T.ins(i,rk[i],1);
	int cnt=0;
	for(;A.size();++cnt) {
		int k=solve(),rem=m;
		for(int i=1;i<=k;++i) {
			int l=1,r=n,p=0;
			while(l<=r) {
				int mid=(l+r)>>1;
				ll z=T.qry(k-i+1,mid);
				if(z<=rem) p=mid,l=mid+1;
				else r=mid-1;
			}
			A.erase(A.find(a[p])),T.ins(p,rk[p],-1),rem-=a[p];
		}
	}
	printf("%d\n",cnt);
	return 0;
}



C. [P10241] Subpath

Problem Link

題目大意

給定 \(n\) 個點的樹,點有權值,求出路徑上最長嚴格遞增子序列的長度。

資料範圍:\(n\le 10^5\)

思路分析

先考慮如何求 \(u\) 子樹內以 \(u\) 為結尾 / 開頭的 LIS 長度 \(f_u,g_u\)

可以考慮線段樹合併,對每個 \(v\),把 \(f_v/g_v\) 插在 \(a_v\) 上,線段樹維護值域區間 \(\max\),那麼 \(f_u\) 就是子樹 \([1,a_u)\) 範圍內最大 \(f\)\(+1\)\(g\) 可以同理維護。

然後考慮如何在路徑 \(\mathrm{LCA}\) 處維護最長 LIS。

假如 \(\mathrm{LCA}\) 並不在 LIS 上,相當於對每個 \(x\),然後取出 \([1,x]\) 中最大的 \(f\)\((x,\infty)\) 中最大的 \(g\) 加起來更新答案,可以看成 CDQ 分治的過程,那麼只要線上段樹合併時一邊取左子樹,一邊取右子樹更新即可。

如果 \(\mathrm{LCA}\) 在 LIS 上,那麼相當於在兩個子樹內各查詢出 \(f_u/g_u\) 再合併,這也是容易的。

時間複雜度 \(\mathcal O(n\log V)\)

程式碼呈現

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int ans=0;
struct SegmentTree {
	int tot,ls[MAXN*32],rs[MAXN*32],f[MAXN*32],g[MAXN*32];
	void ins(int u,int x,int y,int l,int r,int &p) {
		if(!p) p=++tot;
		f[p]=max(f[p],x),g[p]=max(g[p],y);
		if(l==r) return ;
		int mid=(l+r)>>1;
		u<=mid?ins(u,x,y,l,mid,ls[p]):ins(u,x,y,mid+1,r,rs[p]);
	}
	void merge(int l,int r,int q,int &p) {
		if(!q||!p) return p|=q,void();
		f[p]=max(f[p],f[q]),g[p]=max(g[p],g[q]);
		if(l==r) return ;
		ans=max({ans,f[ls[p]]+g[rs[q]],f[ls[q]]+g[rs[p]]});
		int mid=(l+r)>>1;
		merge(l,mid,ls[q],ls[p]),merge(mid+1,r,rs[q],rs[p]);
	}
	int qry(int ul,int ur,int op,int l,int r,int p) {
		if(ul<=l&&r<=ur) return op?g[p]:f[p];
		int mid=(l+r)>>1,s=0;
		if(ul<=mid) s=max(s,qry(ul,ur,op,l,mid,ls[p]));
		if(mid<ur) s=max(s,qry(ul,ur,op,mid+1,r,rs[p]));
		return s;
	}
}	T;
vector <int> G[MAXN];
int n,f[MAXN],g[MAXN],a[MAXN],rt[MAXN],V=1e9+1;
void dfs(int u,int fz) {
	f[u]=g[u]=1;
	for(int v:G[u]) if(v^fz) {
		dfs(v,u);
		int tf=T.qry(0,a[u]-1,0,0,V,rt[v])+1;
		int tg=T.qry(a[u]+1,V,1,0,V,rt[v])+1;
		ans=max({ans,tf+T.qry(a[u]+1,V,1,0,V,rt[u]),T.qry(0,a[u]-1,0,0,V,rt[u])+tg}); 
		f[u]=max(f[u],tf),g[u]=max(g[u],tg);
		T.merge(0,V,rt[v],rt[u]);
	}
	T.ins(a[u],f[u],g[u],0,V,rt[u]);
}
signed main() {
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]);
	for(int i=1,u,v;i<n;++i) {
		scanf("%d%d",&u,&v),G[u].push_back(v),G[v].push_back(u);
	}
	dfs(1,0);
	ans=max({ans,T.f[rt[1]],T.g[rt[1]]});
	printf("%d\n",ans);
	return 0;
}



*D. [P8340] Express

Problem Link

題目大意

\(\{1,2,3,\dots,n\}\) 有多少個子集能用其子集和表出 \(1\sim n\) 中所有數。

資料範圍:\(n\le 5\times 10^5\)

思路分析

首先考慮如何求出一個集合中最小不可表示的元素,這是經典問題,找到排序後第一個 \(s_{i}+1<a_{i+1}\) 的位置,其中 \(s_i\) 是字首和,那麼答案就是 \(s_i+1\)

因此我們可以考慮容斥,列舉 \(x\) 為第一次出現過這種情況的 \(s_i\),那麼相當於 \([1,x]\) 中元素的和為 \(x\),並且其他元素在 \([x+2,n]\) 中選擇,然後用一定的容斥使得 \(1\sim x\) 中不存在這樣的不合法 \(s_i\)

先考慮如何算出總方案數 \(f_x\) 表示 \([1,x]\) 中元素和為 \(x\) 的子集數量。

由於每個元素都不能相同,因此子集大小至多是 \(\mathcal O(\sqrt x)\) 級別的。

我們考慮把一個方案對應成一個楊表,即從大到小排列,第 \(k\) 行的寬度就是第 \(k\) 大元素的值。

那麼這個楊表一共有 \(x\) 個網格,但至多 \(\mathcal O(\sqrt x)\) 行,那麼每列的高度都只有 \(\mathcal O(\sqrt x)\) 種可能。

從大到小列舉列高 \(h\),然後 \(f_i\to f_{i+kh}\),其中 \(k>0\),可以用完全揹包的方式處理,由於 \(h\)\(\mathcal O(\sqrt n)\) 級別,那麼總複雜度是 \(\mathcal O(n\sqrt n)\),可以在這個複雜度內求出 \(f_1\sim f_n\)

然後考慮如何容斥,即我們要欽定所有 \(<x\) 的數 \(y\) 都不滿足 \([1,y]\) 的和 \(=y\)\(y+1\) 未被選。

那麼對於 \(y<x\)\(f_y\to f_x\) 的容斥係數就是 \([y+2,x]\) 中的元素組出 \(x-y\) 的方案數,再乘以 \(-1\)

考慮類 CDQ 分治的過程維護貢獻,但我們發現 \(2y>x\)\(f_y\to f_x\) 的容斥係數一定為 \(0\)

因此 CDQ 分治時 \(mid+1\sim r\) 的元素內部不存在貢獻。

因此我們只要考慮 \([l,mid]\to (mid,r]\) 的轉移。

維護在 \([y+2,x]\) 中選數的方案數,可以做一個類似上面的 dp。

列舉 \(h\)\([y+2,x]\) 範圍內的元素數量,那麼加入的時候就是 \(g_{(y+1)h+y}\gets f_y\),表示將 \([y+2,x]\) 範圍內的元素都減去 \(y+1\),然後在 \(g\) 上做類似 dp 過程即可。

最後 \(x\in (mid,r]\)\(f_x\) 減去 \(g_x\) 就減去了這部分的貢獻。

最終的答案是 \(2^n-\sum_{i<n} 2^{n-i-1}f_i\)

時間複雜度 \(\mathcal O(n\sqrt n)\)

程式碼呈現

#include<bits/stdc++.h> 
using namespace std;
const int MAXN=5e5+5,B=1000;
int MOD,f[MAXN],g[MAXN],pw[MAXN];
inline void add(int &x,const int &y) { x=(x+y>=MOD)?x+y-MOD:x+y; }
void dp(int n) {
	if(n==1) return ;
	dp(n>>1),memset(g,0,sizeof(g));
	for(int i=B;i;--i) {
		for(int j=0;j+(j+1)*i<=n;++j) add(g[j+(j+1)*i],f[j]);
		for(int j=n;~j;--j) g[j]=(j>=i?g[j-i]:0);
		for(int j=i;j<=n;++j) add(g[j],g[j-i]);
	}
	for(int j=n/2+1;j<=n;++j) if(g[j]) add(f[j],MOD-g[j]);
}
signed main() {
	int n;
	scanf("%d%d",&n,&MOD);
	for(int i=pw[0]=1;i<=n;++i) pw[i]=pw[i-1]*2%MOD;
	for(int i=B;i;--i) {
		for(int j=n;j>=i;--j) f[j]=f[j-i];
		f[i]=1;
		for(int j=i;j<=n;++j) add(f[j],f[j-i]);
	}
	f[0]=1,dp(n);
	int ans=pw[n];
	for(int i=0;i<n;++i) ans=(ans-1ll*f[i]*pw[n-i-1])%MOD;
	printf("%d\n",(ans+MOD)%MOD);
	return 0;
}



E. [P10103] Permutation

Problem Link

題目大意

\(q\) 組詢問給定 \(n,m\),求有多少 \(n\) 階排列 \(p\) 滿足 \(p_1\sim p_m>m\) 且所有 \(p_i\ne i\)

資料範圍:\(n,m,q\le 2\times 10^5\)

思路分析

先在 \(m+1\sim n\) 中選出 \(m\) 個放到 \(p_1\sim p_m\) 上,變成一個子問題:剩下 \(n-m\) 個位置,\(n-2m\) 個元素不能放在自己對應的位置上,\(m\) 個元素沒有任何限制。

因此可以設計狀態 dp:\(f_{n,m}\) 表示 \(n\) 個不能放在自己對應位置上的元素,\(m\) 個無位置限制的元素,排列的方案數。

考慮分別消去一個有限制的元素和一個無限制的元素,根據組合意義處理。

先考慮一個無限制的元素如何填:

  • 如果填在自己對應的位置上,那麼轉移到 \(f_{n,m-1}\)
  • 否則相當於這個元素被欽定了“不能放在自己位置上”的限制,變成一個有限制的元素,轉移到 \(f_{n+1,m-1}\)

因此 \(f_{n,m}=f_{n,m-1}+f_{n+1,m-1}\),類似楊輝三角的遞推,考慮 \(f_{n',m-k}\to f_{n,m}\) 的轉移係數,我們得到:

\[f_{n,m}=\sum_{i=0}^k \binom kif_{n+i,m-k} \]

然後考慮一個有限制的元素如何填:

  • 填在一個無限制元素對應的位置上,那麼這個無限制元素依然可以任意填,轉移到 \(m\times f_{n-1,m}\)
  • 填在一個有限制元素對應的位置上,變成經典錯拍,考慮這個元素是否和當前元素互換位置,轉移到 \((n-1)(f_{n-1,m}+f_{n-2,m})\)

因此 \(f_{n,m}=(m+n-1)f_{n-1,m}+(n-1)f_{n-2,m}\)

那麼考慮對 \(m\) 分塊回答詢問,取塊長為 \(B\),對於 \(m=kB\),用第二個遞推式,求出整行的 \(f_{0,m}\sim f_{n,m}\)

然後對於一個 \(m=kB+r\) 其中 \(r\in[0,B)\) 的詢問,用第一個遞推式 \(\mathcal O(r)\) 計算答案。

時間複雜度 \(\mathcal O\left(qB+\dfrac{V^2}B\right)\),取 \(B=\dfrac{V}{\sqrt q}\) 時最優。

時間複雜度 \(\mathcal O(V\sqrt q)\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
namespace FastMod {
typedef unsigned long long ull;
typedef __uint128_t uLL;
ull b,m;
inline void init(ull B) { b=B,m=ull((uLL(1)<<64)/B); }
inline ull mod(ull a) {
	ull q=((uLL(m)*a)>>64),r=a-q*b;
	return r>=b?r-b:r;
}
};
#define o FastMod::mod
const int MAXN=2e5+5,B=450,MOD=998244353,N=2e5;
ll ksm(ll a,ll b=MOD-2) { ll s=1; for(;b;a=o(a*a),b>>=1) if(b&1) s=o(s*a); return s; }
ll fac[MAXN],ifac[MAXN],f[MAXN],C[B+5][B+5],ans[MAXN];
vector <array<int,3>> Q[MAXN];
signed main() {
	FastMod::init(MOD);
	for(int i=fac[0]=ifac[0]=1;i<=N;++i) ifac[i]=ksm(fac[i]=o(fac[i-1]*i));
	int q; scanf("%d",&q);
	for(int i=1,n,m;i<=q;++i) {
		scanf("%d%d",&n,&m);
		if(n-m<m) ans[i]=0;
		else ans[i]=o(fac[n-m]*ifac[n-2*m]),Q[m/B].push_back({n-2*m,m,i});
	}
	for(int i=0;i<=B;++i) for(int j=C[i][0]=1;j<=i;++j) C[i][j]=o(C[i-1][j]+C[i-1][j-1]);
	for(int x=0;x*B<=N;++x) if(Q[x].size()) {
		int M=x*B;
		f[0]=fac[M],f[1]=o(M*fac[M]);
		for(int n=2;n<=N;++n) f[n]=o((n+M-1)*f[n-1]+(n-1)*f[n-2]);
		for(auto z:Q[x]) {
			int n=z[0],m=z[1],k=m-M; ll s=0;
			for(int i=0;i<=k;++i) s=o(s+f[n+i]*C[k][i]);
			ans[z[2]]=o(ans[z[2]]*s)%MOD;
		}
	}
	for(int i=1;i<=q;++i) printf("%lld\n",ans[i]);
	return 0;
}



*F. [P10107] Distance

Problem Link

題目大意

給定 \(n\) 個點的有根樹,點有點權,\(q\) 次詢問 \(u,k\),求出所有 \(u\) 子樹內距離 \(u\) 不超過 \(k\) 的點 \(v\)\(a_v\oplus\mathrm{dis}(u,v)\) 的和。

資料範圍:\(n,q\le 10^6\)

思路分析

考慮刻畫子問題,注意到這個問題的子問題不太可能被一個子樹內的資訊描述,而是形如同層內一些節點的子樹資訊的合併。

又因為這題要處理的資訊和距離的二進位制異或有關,這啟示我們用倍增一類自帶二進位制結構的演算法刻畫資訊。

因此我們可以考慮 \(f(u,k)\) 表示所有和 \(u\) 同層且 dfn 序小於等於 \(u\) 的點的子樹中,深度 \(\le d_u+2^k\) 的點的答案和。

那麼 \(f(u,k)\gets f(u,k-1)+f(dw_{u,k-1},k-1)\),其中 \(dw_{u,k-1}\) 表示深度為 \(d_u+2^{k-1}\) 的節點中,dfn 序 \(<\mathrm{dfn}(u)+\mathrm{siz}(u)\) 的最後一個點,也可以簡單理解為 \(u\) 子樹內最“靠右”的節點。

由於 \(f(u,k)\) 自帶二進位制位上的資訊,因此我們只要處理距離的第 \(2^{k-1}\) 位上的變化量,即給每個 \(f(dw_{u,k-1},k-1)\) 裡的元素貢獻異或上 \(2^{k-1}\)

那麼我們要計數 \(f(dw_{u,k-1},k-1)\) 對應的這個範圍內,有多少個 \(a_v\)\(2^{k-1}\) 位為 \(0\),多少個為 \(1\),事實上我們只關心這兩種元素的數量差。

如果暴力設 \(G(u,k,d)\) 表示 \(f(u,k)\) 對應範圍內,\(a_v\) 的第 \(d\) 個二進位制位為 \(0\) 的元素數量減去為 \(1\) 的元素數量,這可以類似倍增轉移,但此時資訊總量是 \(\mathcal O(n\log^2n)\) 級別的。

考慮最佳化,很顯然 \(u,d\) 三維是必須記錄的,那麼考慮去除 \(k\) 一維的影響。

\(g(u,d)=G(u,\infty,d)\),即和 \(u\) 同層且 dfn 序不超過 \(u\) 的每個點的整個子樹中,\(a_v\) 的第 \(d\) 位等於 \(0\) 的數量減去等於 \(1\) 的數量。

那麼原本的 \(G(u,k,d)=g(u,d)-g(dw_{u,k},d)\),即一個類似差分的過程。

那麼 \(g\) 的轉移可以看成一個類似二維字首和的過程,記 \(pre_u\) 表示和 \(u\) 同層的點中最後一個 dfn 序小於 \(u\) 的。

那麼不難得到轉移:\(g(u,d)=g(dw_{u,0},d)+g(pre_u,d)-g(dw_{pre_u,0},d)+[a_u\operatorname{AND} 2^d=z]\),即分別從 \(u\) 的“下方”和“右方”轉移,然後容斥。

查詢答案可以做一些類似的過程,每次考慮最高位的影響,實際上每個最高位都只會影響深度的一段字尾,也可以用 \(g\) 算出其貢獻。

時間複雜度 \(\mathcal O((n+q)\log n)\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5;
vector <int> G[MAXN];
int n,q,a[MAXN],lst[MAXN],dep[MAXN],pre[MAXN],dw[MAXN][20],g[MAXN][20];
ll f[MAXN][20];
void dfs(int u,int fz) {
	dep[u]=dep[fz]+1,pre[u]=lst[dep[u]],lst[dep[u]]=u;
	dw[u][0]=dw[pre[u]][0];
	for(int v:G[u]) if(v^fz) dfs(v,u),dw[u][0]=v;
	for(int k=1;k<20;++k) dw[u][k]=dw[dw[u][k-1]][k-1];
	for(int k=0;k<20;++k) {
		g[u][k]=g[dw[u][0]][k]+g[pre[u]][k]-g[dw[pre[u]][0]][k]+(a[u]>>k&1?-1:1);
	}
	f[u][0]=a[u]+f[pre[u]][0];
	for(int k=1;k<20;++k) {
		int v=dw[u][k-1];
		f[u][k]=f[u][k-1]+f[v][k-1]+(1ll<<(k-1))*(g[v][k-1]-g[dw[u][k]][k-1]);
	}
}
ll qf(int u,int z) {
	int v=u; ll s=0; ++z;
	for(int k=19;~k;--k) if(z>>k&1) v=dw[v][k];
	for(int k=19;~k;--k) if(z>>k&1) {
		s+=f[u][k]+(1ll<<k)*(g[dw[u][k]][k]-g[v][k]),u=dw[u][k];
	}
	return s;
}
signed main() {
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i];
	for(int u=2,v;u<=n;++u) cin>>v,G[u].push_back(v),G[v].push_back(u);
	dfs(1,0),cin>>q;
	for(int x,k;q--;) cin>>x>>k,cout<<qf(x,k)-qf(pre[x],k)<<"\n";
	return 0;
}




Round #7 - 2024.9.16

A. [P10812] Factor

Problem Link

題目大意

給定 \(1\sim n\) 數軸,每個位置至多經過一次,不可超出 \([1,n]\),每次可以從 \(u\) 走到 \(u-1,u+1\)\(u\) 的因子,求有多少 \(n\to 1\) 的路徑。

資料範圍:\(n\le 5000\)

思路分析

如果不存在 \(u\to u+1\) 的操作,那麼直接 dp 並沒有後效性。

如果存在這種操作,我們就要將若干操作合併,我們以每次 \(u\) 能使減少的操作為狀態分段進行 dp。

\(f_{u,v}\) 表示當前路徑上一步是 \(v\to u\),並且 \(v>u\) 的方案數。

那麼轉移要麼直接向 \(u-1\) 移動成 \(f_{u-1,u}\),要麼列舉 \(x\in[u,v)\) 並轉移到 \(x\) 的因數上。

我們可以對所有 \(u\) 做字尾和,那麼每個從 \(x\) 向因數移動的所有轉移都可以一次性處理掉,那麼總轉移數是均攤調和的。

時間複雜度 \(\mathcal O(n^2\log n)\)

程式碼呈現

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5005;
vector <int> fac[MAXN];
int n,MOD,f[MAXN][MAXN];
inline void add(int &x,const int &y) { x=(x+y>=MOD)?x+y-MOD:x+y; }
signed main() {
	scanf("%d%d",&n,&MOD);
	for(int i=1;i<=n;++i) for(int j=i;j<=n;j+=i) fac[j].push_back(i);
	f[n][n+1]=1;
	for(int i=n;i>1;--i) {
		int s=0;
		for(int j=n;j>=i;--j) {
			add(s,f[i][j+1]);
			for(int k:fac[j]) if(k<i) add(f[k][i],s);
		}
		add(f[i-1][i],s);
	}
	int s=0;
	for(int i=1;i<=n+1;++i) add(s,f[1][i]);
	printf("%d\n",s);
	return 0;
}



B. [P10674] Subgraph

Problem Link

題目大意

給定 \(n\) 個點 \(m\) 條邊的無向圖,求有多少點集對 \((S,T)\) 使得每個 \(S\) 中元素都在某兩個 \(T\) 中元素的某條簡單路徑上。

資料範圍:$n\le 5\times 10^{5} ,m\le10^6 $。

思路分析

對每個 \(T\),求出其內部所有路徑並構成的點集 \(V(T)\),那麼 \(S\) 的選法就是 \(2^{|V(T)|}\) 種方案。

考慮刻畫 \(f(T)\),容易發現建出原圖的圓方樹,那麼所有 \(T\) 中節點所在的方點生成的斯坦納樹上的點都在 \(V(T)\) 中,如果是方點,那麼其對應的所有圓點都在 \(V(T)\) 中。

考慮在方點處統計權值,對於一個大小為 \(C\) 的點雙連通分量,設其權值為 \(2^{C-1}\),即圓方樹上兒子個數。

那麼對於斯坦納樹的根,如果其是圓點,那麼沒有將這個點考慮在 \(V(T)\) 中,否則沒有將其父親對應的圓點考慮進 \(V(T)\) 中,因此答案最後 \(\times 2\) 再加上 \(T=\varnothing\) 的情況即可。

dp 時設 \(f_u\) 表示 \(u\) 子樹內至少有一個點被選入 \(T\) 時的權值和,即欽定 \(u\) 子樹外選點後的答案。

但是我們在統計點 \(u\) 作為樹根的權值的時候要欽定至少兩棵子樹被選,做一個簡單揹包即可。

時間複雜度 \(\mathcal O(n+m)\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5,MOD=998244353;
ll dp[MAXN],ans,pw[MAXN];
vector <int> G[MAXN],E[MAXN];
void link(int x,int y) { E[x].push_back(y),E[y].push_back(x); }
int n,m,k,low[MAXN],dfn[MAXN],dcnt,stk[MAXN],tp,w[MAXN];
bool ins[MAXN];
void tarjan(int u) {
	ins[stk[++tp]=u]=true,dfn[u]=low[u]=++dcnt;
	for(int v:G[u]) {
		if(!dfn[v]) {
			tarjan(v),low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]) {
				link(++k,u);
				while(ins[v]) link(stk[tp],k),++w[k],ins[stk[tp--]]=false;
			}
		} else low[u]=min(low[u],dfn[v]);
	}
}
void dfs(int u,int fz) {
	if(E[u].size()==1&&fz!=0) return ++ans,dp[u]=1,void();
	ll g[3]={1,0,0};
	for(int v:E[u]) if(v^fz) {
		dfs(v,u),g[2]=(g[2]*(dp[v]+1)+g[1]*dp[v])%MOD,g[1]=(g[1]+g[0]*dp[v])%MOD;
	}
	if(u>n) ans=(ans+g[2]*pw[w[u]])%MOD,dp[u]=(g[2]+g[1])*pw[w[u]]%MOD;
	else ans=(ans+2*g[2]+g[1]+g[0])%MOD,dp[u]=(2*g[2]+2*g[1]+g[0])%MOD;
}
signed main() {
	scanf("%d%d",&n,&m),k=n;
	for(int i=pw[0]=1;i<=n;++i) pw[i]=pw[i-1]*2%MOD;
	for(int i=1,u,v;i<=m;++i) {
		scanf("%d%d",&u,&v),G[u].push_back(v),G[v].push_back(u);
	}
	tarjan(1),dfs(1,0);
	printf("%lld\n",(2*ans+1)%MOD);
	return 0;
}



C. [P10682] Xor

Problem Link

題目大意

給定 \(n\) 個點 \(m\) 條邊的 DAG,求有多少種給每條邊賦 \(0/1\) 權值的方式使得任意兩條起終點相同的路徑權值和 \(\bmod 2\) 都同餘。

資料範圍:\(n,m\le 400\)

思路分析

很顯然題目的要求就是對於任意兩條 \(u\to v\) 的路徑,路徑上所有邊權異或和為 \(0\),可以用高斯消元求解。

由於本題全填 \(0\) 肯定是一組解,因此可以直接維護,不需要特判一些 Corner Case。

那麼我們只要最佳化限制組數即可。

對每個起點 \(u\) 分別考慮,根據經典結論,先求出一棵以 \(u\) 為根的外向 dfs 樹,設樹上 \(u\to v\) 的路徑為 \(T_u\),那麼只要考慮恰經過一條非樹邊的環。

即對於一條非樹邊 \(x\to y\),我們只要求 \(T_x\oplus T_y\oplus w(x\to y)=0\),並且不難證明這是充分的。

對於任意一條路徑,找到其中的第一條非樹邊 \(x\to y\),根據限制,可以把到 \(y\) 的路徑等效成 \(T_y\),遞迴進行此過程即可證明該路徑合法。

那麼此時總共只有 \(\mathcal O(nm)\) 條限制,暴力插入並用 bitset 最佳化高斯消元維護。

時間複雜度 \(\mathcal O\left(\dfrac{nm^3}\omega\right)\)

程式碼呈現

#include<bits/stdc++.h>
using namespace std;
const int MAXN=405,MOD=1e9+7;
int n,m;
struct Edge { int v,id; };
vector <Edge> G[MAXN];
bool vis[MAXN];
bitset <MAXN> w,d[MAXN],x[MAXN];
void ins() {
	for(int i=1;i<=m;++i) if(w[i]) {
		if(!x[i].any()) return x[i]=w,void();
		else w^=x[i];
	}
}
void dfs(int u) {
	vis[u]=true;
	for(auto e:G[u]) {
		if(!vis[e.v]) d[e.v]=d[u],d[e.v].set(e.id),dfs(e.v);
		else w=d[u],w^=d[e.v],w.flip(e.id),ins();
	}
}
signed main() {
	scanf("%d%d",&n,&m);
	for(int i=1,u,v;i<=m;++i) scanf("%d%d",&u,&v),G[u].push_back({v,i});
	for(int i=1;i<=n;++i) {
		for(int u=1;u<=n;++u) d[u].reset(),vis[u]=false;
		dfs(i);
	}
	int ans=1;
	for(int i=1;i<=m;++i) if(!x[i].any()) ans=ans*2%MOD;
	printf("%d\n",ans);
	return 0;
}



*D. [P11051] Range

Problem Link

題目大意

給定 \(n\) 個點的樹,每個點有點權 \(w_i\)\(q\) 次詢問 \(L,R\),構造一組 \(c_i\) 使得每個子樹內 \(c_i\) 的和都在 \([0,1]\) 之間,最小化 \(\sum|c_i|w_i\)

資料範圍;\(n\le 2\times 10^5,q\le 10^5\)

思路分析

先從 \(w_i=1\) 的情況開始分析,此時在哪裡填 \(c\) 沒有區別,只關心總和。

自下而上地開始填 \(c_i\),首先在每個葉子上都有 \(c_i=L\),在此之後,每個點的子樹和都 \(\ge L\),我們只要給一些非葉子結點的 \(c\) 設為負數以保證其總和 \(\le R\),容易證明減到 \(<L\) 是不優的。

觀察根節點處的變化量,設原有 \(k\) 個葉子,那麼要讓根節點合法,整棵樹的變化量至少為 \(\max(0,kL-R)\)

不難證明這個界是可以取到的,可以每個 \(>R\) 的節點處減到 \(R\),可以證明這樣不會有冗餘操作。

然後考慮 \(w_i\in\{0,1\}\) 的情況,容易發現此時我們能在 \(w_i=0\) 的點上任意操作,因此我們一定能在每個 \(w_i=0\) 的點上把子樹和調整到 \(L\)

這相當於把每個 \(w_i=0\) 的點看成葉子,然後對每個連通塊分別求解答案再求和。

設有 \(k\) 個葉子的連通塊有 \(f_k\) 個,答案就是 \(\sum_k f_k\times \max(0,kL-R)\),求出第一個 \(kL>R\) 的位置維護 \(f_k\)\(k\times f_k\) 的字尾和即可快速計算答案。

然後考慮一般的情況,由於我們已經會解決 \(w_i\in\{0,1\}\) 的情況了,因此不妨猜測更一般的情況可以向這種情況規約。

對每個 \(x\),將 \(w_i>x\) 的點看成 \(1\)\(w_i\le x\) 的點看成 \(0\),然後對每個 \(x\) 求出答案再相加,可以根據本題的直接貪心過程證明之。

依然考慮維護 \(\sum f_k\),設 \(w_1\sim w_n\) 是遞增的,那麼我們就要依次刪除 \(1\sim n\),刪除 \(1\sim i\) 後的一個葉子數為 \(k\) 的連通塊對 \(f_k\) 的貢獻就是 \(w_{i+1}-w_i\)

首先我們肯定轉成倒序插入節點,可以用並查集維護產生和刪除的每個連通塊。

並且可以考慮差分,即一個連通塊在插入 \(x\) 時刻生成,就對 \(f_k\) 產生 \(+w_x\) 貢獻,在插入 \(y\) 時刻消失,就對 \(f_k\) 產生 \(-w_y\) 貢獻。

那麼這樣就可以維護出所有 \(f_k\) 並計算答案,最終答案記得加上 \(L\) 倍葉子權值的和。

時間複雜度 \(\mathcal O(n\log n+q)\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
vector <int> G[MAXN];
int n,w[MAXN],fa[MAXN],dsu[MAXN],siz[MAXN];
bool vis[MAXN];
ll cnt[MAXN],s1[MAXN],s2[MAXN],clf;
int find(int x) { return x^dsu[x]?dsu[x]=find(dsu[x]):x; }
void add(int x,int c,int v) { cnt[siz[x]]-=v,cnt[siz[x]+=c]+=v; }
void merge(int x,int y,int v) { dsu[y]=x,cnt[siz[y]]-=v,add(x,siz[y],v); }
void ins(int x) {
	vis[x]=true;
	if(fa[x]&&vis[fa[x]]) add(find(fa[x]),-1,w[x]);
	for(int y:G[x]) {
		if(!vis[y]) add(x,1,w[x]);
		else merge(x,y,w[x]);
	}
	if(vis[fa[x]]) merge(find(fa[x]),x,w[x]);
}
void init(vector <int> P,vector <int> W) {
	n=W.size(); vector <int> ord;
	for(int i=1;i<=n;++i) w[i]=W[i-1],ord.push_back(i),dsu[i]=i;
	for(int i=2;i<=n;++i) G[fa[i]=P[i-1]+1].push_back(i);
	for(int i=1;i<=n;++i) if(G[i].empty()) G[i].push_back(0),clf+=w[i];
	sort(ord.begin(),ord.end(),[&](int x,int y){ return w[x]>w[y]; });
	for(int u:ord) ins(u);
	for(int i=n;i>=1;--i) s1[i]=s1[i+1]+cnt[i],s2[i]=s2[i+1]+cnt[i]*i;
}
ll query(int L,int R) {
	int x=min(n,R/L)+1;
	return clf*L+s2[x]*L-s1[x]*R;
}



*E. [P11054] Connect

Problem Link

題目大意

給定一張 \(n\) 個點 \(m\) 條邊的無向圖,互動器中每個點有一個 \([0,n)\) 的顏色。

每次互動時,你可以把若干個點染成 \([0,n]\) 中的任意顏色,互動器會告訴你新圖中的同色連通塊數量。

請在 \(2750\) 次互動之內確定每個點的顏色。

資料範圍:\(n\le 250\)

思路分析

先從鏈入手。

將所有奇數下標的點全部染成某種顏色 \(c\),如果此時得到的連通塊數 \(<n\),說明下標為偶數的點中有顏色為 \(c\) 的,否則說明沒有。

以此為依據二分,可以求出每個顏色為 \(c\) 的點,對每種顏色進行此過程即可還原下標為偶數的點,對於下標為奇數的點也做一遍即可求解,操作次數 \(2n+n\log n\)

然後考慮推廣,我們可以對鏈上所有下標為計數的點一次性檢驗,那麼在圖上我們可以對一個獨立集狀物一次性檢驗。

具體來說,選定一個獨立集 \(S\),將 \(\overline S\) 中的點染成 \(c\),如果返回值小於 \(|S|\) 加上 \(\overline{S}\) 匯出子圖的連通塊數,那麼說明 \(S\) 中存在顏色 \(c\),可以 \(n+|S|\log n\) 還原。

考慮進一步最佳化,觀察我們用到了獨立集的什麼性質。

首先要求 \(S\) 內部的連通塊數量為 \(S\),也即 \(S\) 中沒有同色點相連,那麼我們可以將同色且相鄰的點縮成一個連通塊。

其次要求每個 \(S\) 中的點都至少和一個 \(\overline S\) 中的點相連,這樣才能在一個點顏色為 \(c\) 的時候減少連通塊數量。

這是容易的,取出一棵生成樹並黑白染色得到兩個集合分別作為 \(S\) 求解即可。

最終我們只要求出每個同色連通塊即可,也就是本題 \(50\%\) 分數的子任務。

這個不難,考慮增量法構造,依次加入每個點 \(u\) 並求出已加入的點中哪些與其同色。

\(u\) 的鄰域和 \(u\) 自己保留原先顏色,其他點染顏色 \(n\),設保留原顏色的點集是 \(V\) 那麼 \(u\) 的鄰域中有與 \(u\) 同色的點當且僅當實際同色連通塊數小於 \(|V|\)\(\overline V\) 匯出子圖中的連通塊數量。

注意到每次二分實際上都減少了一個點(和其他點併成同色連通塊,或確定一個連通塊的顏色),那麼我們在 \(3n+n\log n\) 次詢問內解決了此問題。

實際上由於元素數的不斷減少,詢問次數不超過 \(3n+\sum_{i=1}^n\log_2i\),可以透過。

注意特判全部點顏色相同的 Corner Case。

時間複雜度 \(\mathcal O(n^2\log n)\)

程式碼呈現

#include<bits/stdc++.h>
using namespace std;
int perform_experiment(vector<int>E);
const int MAXN=255;
vector <int> G[MAXN],E[MAXN],R[MAXN];
int n;
struct DSU {
	int dsu[MAXN];
	void init() { iota(dsu,dsu+n,0); }
	int find(int x) { return x^dsu[x]?dsu[x]=find(dsu[x]):x; }
	bool merge(int x,int y) {
		x=find(x),y=find(y),dsu[x]=y;
		return x^y;
	}
}	F,T;
int count(const vector<int>&V) {
	static bitset<MAXN> inq;
	inq.reset(),T.init();
	for(int i:V) inq.set(i);
	int s=V.size();
	for(int i:V) for(int j:G[i]) if(inq[j]) s-=T.merge(i,j);
	return s;
}
int col[MAXN];
void solve(vector<int> S) {
	for(int c=0;c<n;++c) {
		vector <int> X;
		while(S.size()) {
			auto chk=[&](int k) {
				vector <int> q(n,c),r;
				for(int i=0;i<=k;++i) for(int u:R[S[i]]) q[u]=-1;
				for(int i=0;i<n;++i) if(~q[i]) r.push_back(i);
				int z=perform_experiment(q);
				return z<count(r)+k+1;
			};
			int l=0,r=S.size()-2,x=S.size()-1;
			if(!chk(x)) {
				X.insert(X.end(),S.begin(),S.end());
				break;
			}
			while(l<=r) {
				int mid=(l+r)>>1;
				if(chk(mid)) x=mid,r=mid-1;
				else l=mid+1;
			}
			col[S[x]]=c;
			X.insert(X.end(),S.begin(),S.begin()+x);
			S.erase(S.begin(),S.begin()+x+1);
		}
		S.swap(X);
	}
}
vector<int> find_colours(int N,vector<int>X,vector<int>Y) {
	n=N,F.init();
	for(int i=0;i<(int)X.size();++i) G[X[i]].push_back(Y[i]),G[Y[i]].push_back(X[i]);
	for(int u=0;u<n;++u) {
		static bitset <MAXN> vis;
		vis.reset();
		vector <int> Ne,C;
		for(int v:G[u]) if(v<u&&!vis[F.find(v)]) {
			Ne.push_back(F.find(v)),vis.set(F.find(v));
		}
		while(Ne.size()) {
			auto chk=[&](int k) { //qry Ne[0,k]
				vector <int> q(n,n),r;
				vis.reset(),q[u]=-1;
				for(int i=0;i<=k;++i) vis.set(Ne[i]);
				for(int i=0;i<u;++i) if(vis[F.find(i)]) q[i]=-1;
				for(int i=0;i<n;++i) if(~q[i]) r.push_back(i);
				int z=perform_experiment(q);
				return count(r)+k+2>z;
			};
			int l=0,r=Ne.size()-2,x=Ne.size()-1;
			if(!chk(x)) break;
			while(l<=r) {
				int mid=(l+r)>>1;
				if(chk(mid)) x=mid,r=mid-1;
				else l=mid+1;
			}
			C.push_back(Ne[x]);
			Ne.erase(Ne.begin(),Ne.begin()+x+1);
		}
		for(int v:C) F.merge(u,v);
	}
	vector <int> bl(n);
	for(int i=0;i<n;++i) R[bl[i]=F.find(i)].push_back(i);
	F.init();
	for(int i=0;i<n;++i) for(int j:G[i]) if(F.merge(bl[i],bl[j])) {
		E[bl[i]].push_back(bl[j]),E[bl[j]].push_back(bl[i]);
	}
	vector <int> S[2];
	function<void(int,int,int)> dfs=[&](int u,int fz,int c) {
		S[c].push_back(u);
		for(int v:E[u]) if(v^fz) dfs(v,u,c^1);
	};
	dfs(bl[0],-1,0);
	if(S[1].empty()) {
		vector <int> q(n,-1);
		for(q[0]=0;q[0]<n;++q[0]) if(perform_experiment(q)==1) {
			return vector<int>(n,q[0]);
		}
	}
	solve(S[0]),solve(S[1]);
	vector <int> cols(n);
	for(int i=0;i<n;++i) cols[i]=col[bl[i]];
	return cols;
}




Round #8 - 2024.9.19

A. [P11053] Grid

Problem Link

題目大意

給定 \(n\times n\) 的 01 矩陣 \(A\) 的第一行和第一列,定義 \(A_{i,j}=1-A_{i-1,j}\times A_{i,j-1}\)\(q\) 次詢問 \(A\) 的某個子矩陣的元素和。

資料範圍:\(n,q\le 2\times 10^5\)

思路分析

觀察這個矩陣,發現如果 \(A_{i,j}=1\) 那麼 \(A_{i+1,j},A_{i,j+1}=0\),從而 \(A_{i+1,j+1}=1\),因此所有的 \(1\) 構成若干向右下方的射線。

並且我們發現如果 \(A_{i,j}=1,A_{i-1,j-1}=0\),那麼可以推出 \(A_{i,j}\) 左上角的矩形一定形如 \(\begin{bmatrix}x&y&1\\z&0&0\\1&0&1\end{bmatrix}\)

此時 \(y,z\) 中至少有一個 \(1\),又因為連續的兩個 \(1\) 顯然不能出現在第一行或第一列以外的地方。

因此這種情況只能出現在 \(\min(i,j)\le 3\) 的位置,也就是前三行或前三列,那麼暴力求出第三行和第三列,其中的每個 \(1\) 都對應一條向右下方的射線,且不存在其他的 \(1\)

對子矩形詢問差分成一個以 \((1,1)\) 為左上角的詢問 \((x,y)\),對於一條射線的起點 \((i,j)\),其對詢問的貢獻就是 \(\max(0,\min(x-i,y-j))\)

先求出 \(\sum \min(x-i,y-j)\)\(\min(x-i,y-j)<0\) 的點一定是 \(A_{3,j+1\sim n}\)\(A_{i+1\sim n,3}\) 範圍內的點,字尾和即可。

時間複雜度 \(\mathcal O((n+q)\log n)\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
typedef vector<int> vi;
const int MAXN=2e5+5;
int n,q,a[4][MAXN],b[MAXN][4],cl[MAXN],cr[MAXN];
ll dl[MAXN],dr[MAXN];
bool cmp(array<int,2> i,array<int,2> j) { return i[0]-i[1]<j[0]-j[1]; }
vector<ll> mosaic(vi X,vi Y,vi T,vi B,vi L,vi R) {
	n=X.size(),q=T.size();
	if(n<=3) {
		vector <vi> M(n,vi(n));
		M[0]=X;
		for(int i=1;i<n;++i) {
			M[i][0]=Y[i];
			for(int j=1;j<n;++j) M[i][j]=(M[i-1][j]|M[i][j-1])^1;
		}
		for(int i=0;i<n;++i) for(int j=1;j<n;++j) M[i][j]+=M[i][j-1];
		for(int i=1;i<n;++i) for(int j=0;j<n;++j) M[i][j]+=M[i-1][j];
		vector <ll> ans(q);
		for(int i=0;i<q;++i) {
			ans[i]=M[B[i]][R[i]];
			if(T[i]) ans[i]-=M[T[i]-1][R[i]];
			if(L[i]) ans[i]-=M[B[i]][L[i]-1];
			if(T[i]&&L[i]) ans[i]+=M[T[i]-1][L[i]-1];
		}
		return ans;
	}
	for(int i=1;i<=n;++i) a[1][i]=X[i-1],b[i][1]=Y[i-1];
	a[2][1]=Y[1],a[3][1]=Y[2],b[1][2]=X[1],b[1][3]=X[2];
	for(int o:{2,3}) for(int i=2;i<=n;++i) {
		a[o][i]=(a[o-1][i]|a[o][i-1])^1;
		b[i][o]=(b[i][o-1]|b[i-1][o])^1;
	}
	vector <array<int,2>> Z;
	for(int i=3;i<=n;++i) if(a[3][i]) Z.push_back({3,i}),++cl[i],dl[i]+=i;
	for(int i=4;i<=n;++i) if(b[i][3]) Z.push_back({i,3}),++cr[i],dr[i]+=i;
	for(int i=n;i>=1;--i) cl[i]+=cl[i+1],dl[i]+=dl[i+1],cr[i]+=cr[i+1],dr[i]+=dr[i+1];
	sort(Z.begin(),Z.end(),cmp);
	int k=Z.size();
	vector <ll> sl(k),sr(k);
	if(k) {
		sl[0]=Z[0][1];
		for(int i=1;i<k;++i) sl[i]=sl[i-1]+Z[i][1];
		sr[k-1]=Z[k-1][0];
		for(int i=k-2;~i;--i) sr[i]=sr[i+1]+Z[i][0];
	}
	for(int i=1;i<=3;++i) for(int j=1;j<=n;++j) a[i][j]+=a[i][j-1]+a[i-1][j]-a[i-1][j-1];
	for(int i=1;i<=n;++i) for(int j=1;j<=3;++j) b[i][j]+=b[i][j-1]+b[i-1][j]-b[i-1][j-1];
	auto qry=[&](int x,int y) -> ll {
		if(x<=3) return a[x][y];
		if(y<=3) return b[x][y];
		ll s=a[3][y]+b[x][3]-a[3][3];
		int i=upper_bound(Z.begin(),Z.end(),array<int,2>{x,y},cmp)-Z.begin();
		if(i>0) s+=1ll*i*y-sl[i-1];
		if(i<k) s+=1ll*(k-i)*x-sr[i];
		s+=dl[y]-1ll*y*cl[y];
		s+=dr[x]-1ll*x*cr[x];
		return s;
	};
	vector <ll> ans(q);
	for(int i=0;i<q;++i) {
		ans[i]=qry(B[i]+1,R[i]+1)-qry(T[i],R[i]+1)-qry(B[i]+1,L[i])+qry(T[i],L[i]);
	}
	return ans;
}



B. [P10303] Function

Problem Link

題目大意

給定 \(n\) 個函式和初值 \(x_0\),每個函式 \(F_i(x)\) 形如 \(a_i|x|+b_ix+c_i\),求一個排列 \(p_1\sim p_n\) 使得 \(F_{p_1}(F_{p_2}(\cdots F_{p_n}(x_0)))\) 最大。

資料範圍:\(n,|x_0|,|a_i|,|b_i|,|c_i|\le 15\)

思路分析

暴力 dp \(f_{s,v}\) 表示使用 \(s\) 中函式後能否得到 \(v\),但 \(v\) 值域過大,需要最佳化狀態。

先考慮 \(c_i=0\) 的情況,那麼 \(F_i(x)\) 只會根據 \(x\) 的符號不同形成兩種情況,並且兩個同號的人操作後依然同號。

那麼我們顯然只關心絕對值最大和最小的正數和負數,此時只要對每個 \(f_s\) 記錄這四種狀態即可。

回到 \(c_i\ne 0\) 的情況,\(F_i(x)\) 可能會把一些絕對值較小的元素透過 \(c_i\) 變號,不難發現這樣的變號若發生,\(x\) 初值一定 \(\le nV\),其中 \(V\) 是值域,那麼對每個 \(f_s\) 特殊記錄 \([-nV,nV]\) 範圍內的數能否得到即可。

時間複雜度 \(\mathcal O(2^nn^2V)\)

程式碼呈現

#include<bits/stdc++.h>
#define LL __int128
using namespace std;
const int V=250;
const LL inf=1e36;
struct info {
	LL xl,xr,yl,yr;
	bitset <505> q;
	info() { xl=0,xr=-inf,yl=inf,yr=0; }
	void ins(LL x) {
		if(-V<=x&&x<=V) q.set(x+V);
		if(x<0) xl=min(xl,x),xr=max(xr,x);
		else yl=min(yl,x),yr=max(yr,x);
	}
	void gen(vector <LL> &v) {
		if(xl<=xr) v.push_back(xl),v.push_back(xr);
		if(yl<=yr) v.push_back(yl),v.push_back(yr);
		for(int i=-V;i<=V;++i) if(q[i+V]) v.push_back(i);
	}
}	f[1<<15];
int n,o,a[15],b[15],c[15];
LL val(LL x,int i) {
	return (x<0?b[i]-a[i]:b[i]+a[i])*x+c[i];
}
void write(LL x) {
	if(x<0) putchar('-'),x=-x;
	if(x>9) write(x/10);
	putchar(x%10+'0');
}
signed main() {
	scanf("%d%d",&n,&o),f[0].ins(o);
	for(int i=0;i<n;++i) scanf("%d%d%d",&a[i],&b[i],&c[i]);
	for(int s=0;s<(1<<n);++s) {
		vector <LL> z; f[s].gen(z);
		for(LL x:z) for(int i=0;i<n;++i) if(!(s>>i&1)) {
			f[s|1<<i].ins(val(x,i));
		}
		if(s==(1<<n)-1) write(*max_element(z.begin(),z.end()));
	}
	puts("");
	return 0;
}



C. [P10304] Delete

Problem Link

題目大意

給定一張 \(n\) 個點 \(m\) 條邊的 DAG,求出以 \(1\) 為根的 dfs 生成樹 \(T\)\(q\) 次詢問給定 \(a,b\),其中 \(a\)\(T\) 中是 \(b\) 的祖先,查詢若刪 $a\to b $ 在 \(T\) 上路徑的邊後,\(b\)\(T\) 上的子樹中有多少個點不能從 \(1\) 出發到達。

資料範圍:\(n,q\le 10^5,m\le 1.5\times 10^5\)

思路分析

先考慮 \(b\) 是否可達,這個事情顯然對 \(a\) 的深度有單調性,因此我們求出 \(f_b\) 表示如果存在 \(1\to b\) 路徑,\(dep_a\) 至少是多少。

初始 \(f_b=dep_b\),轉移時逆拓撲序考慮每條非樹邊,對於非樹邊 \(u\to v\),那麼就會把 \(f_v\)\(u\to\mathrm{LCA}(u,v)\) 路徑上最小的 \(f\) 更新,可以倍增維護。

然後考慮 \(b\) 子樹內的某個點 \(c\),容易發現 \(c\) 能到達的條件就是 \(b\to c\) 路徑上存在一個 \(f_u\le dep_a\)

離線下來維護 \(v\) 到當前節點 \(u\) 的路徑最小 \(f\),更新就是求出 \((f_u,n]\) 內的節點數加到 \(f_u\) 上並清空 \((f_u,n]\),查詢就是字尾求和,不難用值域線段樹合併維護。

時間複雜度 \(\mathcal O((n+m+q)\log n)\)

程式碼呈現

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int n,m,dep[MAXN],dfn[MAXN],dcnt,st[MAXN][20],deg[MAXN],up[MAXN][20];
vector <int> G[MAXN],E[MAXN],ord,O[MAXN];
void dfs0(int u,int fz) {
	dep[u]=dep[fz]+1,dfn[u]=++dcnt,st[dcnt][0]=fz,up[u][0]=fz;
	for(int k=1;k<20;++k) up[u][k]=up[up[u][k-1]][k-1];
	for(int v:G[u]) {
		if(!dfn[v]) E[u].push_back(v),dfs0(v,u);
		else O[u].push_back(v);
	}
}
int bit(int x) { return 1<<x; }
int cmp(int x,int y) { return dfn[x]<dfn[y]?x:y; }
int LCA(int x,int y) {
	if(x==y) return x;
	int l=min(dfn[x],dfn[y])+1,r=max(dfn[x],dfn[y]),k=__lg(r-l+1);
	return cmp(st[l][k],st[r-bit(k)+1][k]);
}
int f[MAXN],mn[MAXN][20];
int qry(int x,int r) {
	int s=f[r];
	for(int k=19;~k;--k) if(dep[up[x][k]]>=dep[r]) s=min(s,mn[x][k]),x=up[x][k];
	return s;
}
struct SegmentTree {
	int tot,tr[MAXN*20],ls[MAXN*20],rs[MAXN*20];
	void ins(int u,int x,int l,int r,int &p) {
		if(!p) p=++tot;
		tr[p]+=x;
		if(l==r) return ;
		int mid=(l+r)>>1;
		u<=mid?ins(u,x,l,mid,ls[p]):ins(u,x,mid+1,r,rs[p]);
	}
	void merge(int l,int r,int q,int &p) {
		if(!q||!p) return p|=q,void();
		tr[p]+=tr[q];
		if(l==r) return ;
		int mid=(l+r)>>1;
		merge(l,mid,ls[q],ls[p]),merge(mid+1,r,rs[q],rs[p]);
	}
	int qry(int ul,int ur,int l,int r,int p) {
		if(ul<=l&&r<=ur) return tr[p];
		int mid=(l+r)>>1,s=0;
		if(ul<=mid) s+=qry(ul,ur,l,mid,ls[p]);
		if(mid<ur) s+=qry(ul,ur,mid+1,r,rs[p]);
		return s;
	}
	void del(int ul,int ur,int l,int r,int &p) {
		if(ul<=l&&r<=ur) return tr[p]=0,p=0,void();
		int mid=(l+r)>>1;
		if(ul<=mid) del(ul,ur,l,mid,ls[p]);
		if(mid<ur) del(ul,ur,mid+1,r,rs[p]);
		tr[p]=tr[ls[p]]+tr[rs[p]];
	}
}	T;
int rt[MAXN],ans[MAXN];
vector <array<int,2>> qys[MAXN];
void dfs1(int u) {
	T.ins(f[u],1,1,n,rt[u]);
	for(int v:E[u]) dfs1(v),T.merge(1,n,rt[v],rt[u]);
	if(f[u]<n) {
		int w=T.qry(f[u]+1,n,1,n,rt[u]);
		T.ins(f[u],w,1,n,rt[u]),T.del(f[u]+1,n,1,n,rt[u]);
	}
	for(auto z:qys[u]) if(z[0]<n) ans[z[1]]=T.qry(z[0]+1,n,1,n,rt[u]);
}
signed main() {
	int q;
	scanf("%d%d%d",&n,&m,&q);
	for(int i=1,u,v;i<=m;++i) scanf("%d%d",&u,&v),G[u].push_back(v),++deg[v];
	for(int i=1;i<=n;++i) sort(G[i].begin(),G[i].end());
	queue <int> Q;
	for(int i=1;i<=n;++i) if(!deg[i]) Q.push(i);
	while(Q.size()) {
		int u=Q.front(); Q.pop(),ord.push_back(u);
		for(int v:G[u]) if(!--deg[v]) Q.push(v);
	}
	dfs0(1,0);
	for(int k=1;k<20;++k) for(int i=1;i+bit(k)-1<=dcnt;++i) {
		st[i][k]=cmp(st[i][k-1],st[i+bit(k-1)][k-1]);
	}
	for(int i=1;i<=n;++i) if(dfn[i]) f[i]=dep[i];
	for(int u:ord) if(dfn[u]) {
		mn[u][0]=f[u];
		for(int k=1;k<20;++k) mn[u][k]=min(mn[u][k-1],mn[up[u][k-1]][k-1]);
		for(int v:O[u]) if(dfn[v]) f[v]=min(f[v],qry(u,LCA(u,v)));
	}
	for(int i=1,a,b;i<=q;++i) scanf("%d%d",&a,&b),qys[b].push_back({dep[a],i});
	dfs1(1);
	for(int i=1;i<=q;++i) printf("%d\n",ans[i]);
	return 0;
}



D. [P8499] Equal

Problem Link

題目大意

給定兩棵有根樹 \(G,H\),記 \(n=|G|,k=|H|-|G|\),求是否能在 \(H\) 上刪除 \(k\) 個節點使得 \(G,H\) 同構。

資料範圍:\(n\le 5\times 10^5,k\le 5\)

思路分析

直接 dp 狀態難以接受,考慮自上而下地搜尋設 \(f_{i,j}\) 表示能否使得 \(G\)\(i\) 子樹和 \(H\)\(j\) 子樹同構。

用樹雜湊判斷子樹同構,當 \(H\) 是葉子時返回,否則相當於對 \(i,j\) 的所有兒子對應子樹求完美匹配。

我們發現如果有兩個子樹 \(x,y\) 已經同構,那麼直接匹配這兩個子樹肯定可以,否則我們可以用 \(y\) 匹配的子樹匹配上 \(x\) 匹配的子樹從而調整出一組 \(x,y\) 匹配的解。

那麼 \(i\) 剩下未匹配的子樹 \(\le k\) 棵,\(\mathcal O(k!)\) 爆搜每種匹配並記搜最佳化即可。

可以證明被訪問到的 \(f_{i,j}\) 總量是 \(\mathcal O(n2^k)\) 的。

時間複雜度 \(\mathcal O(n2^kk!)\)

程式碼呈現

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int MAXN=1e5+5;
struct Tree {
	int n,rt,fa[MAXN],siz[MAXN];
	vector <int> E[MAXN];
	ull f[MAXN];
	ull P(ull x) {
		x^=x<<13,x^=x>>7,x^=x<<17;
		return x*x*29+x*13+37;
	}
	void dfs(int u) {
		f[u]=siz[u]=1;
		for(int v:E[u]) dfs(v),siz[u]+=siz[v],f[u]+=P(f[v]);
		sort(E[u].begin(),E[u].end(),[&](int x,int y){ return f[x]<f[y]; });
	}
	void init() {
		cin>>n;
		for(int i=1;i<=n;++i) E[i].clear(),siz[i]=f[i]=0;
		for(int i=1;i<=n;++i) {
			cin>>fa[i];
			if(~fa[i]) E[fa[i]].push_back(i);
			else rt=i;
		}
		dfs(rt);
	}
}	G,H;
int k;
map <array<int,2>,bool> DP;
bool dp(int x,int y) {
	if(H.siz[y]==1) return true;
	if(DP.count({x,y})) return DP[{x,y}];
	if(G.siz[x]<H.siz[y]) return false;
	vector <int> &g=G.E[x],&h=H.E[y],s,t;
	auto ig=g.begin(),ih=h.begin();
	while(ig!=g.end()&&ih!=h.end()) {
		if(G.f[*ig]==H.f[*ih]) ++ig,++ih;
		else if(G.f[*ig]<H.f[*ih]) s.push_back(*ig++);
		else t.push_back(*ih++);
	}
	s.insert(s.begin(),ig,g.end()),t.insert(t.begin(),ih,h.end());
	if(s.size()<t.size()||s.size()>k) return false;
	int z=s.size();
	vector <int> p(z);
	iota(p.begin(),p.end(),0),t.resize(z,0);
	do {
	    bool ok=true;
		for(int i=0;i<z;++i) {
			ok&=dp(s[i],t[p[i]]);
			if(!ok) break;
		}
		if(ok) return DP[{x,y}]=true;
	} while(next_permutation(p.begin(),p.end()));
	return DP[{x,y}]=false;
}
void solve() { DP.clear(),G.init(),H.init(),cout<<(dp(G.rt,H.rt)?"Yes\n":"No\n"); }
signed main() {
	ios::sync_with_stdio(false);
	int T,C; cin>>C>>T>>k;
	while(T--) solve();
	return 0;
}



E. [P9481] Edge

Problem Link

題目大意

給定 \(2^n-1\) 個節點的完全二叉樹,每個點向父親連一條帶權有向邊,除此之外還有 \(m\) 條從祖先到後代的額外邊,求所有點對之間的最短路長度之和。

資料範圍:\(n\le 18,m\le 2^{18}\)

思路分析

考慮把所有邊方向取反,此時 \(u\to v\) 的路徑必然過 \(\mathrm{LCA}(u,v)\),那麼對每個 \(u\),到其子樹的最短路容易求,只要對每個祖先求出最短路,到該祖先的其他子樹的最短路就能算出了。

對每個祖先求最短路,可以先預處理祖先之間的最短路,都當成邊連線,然後把 \(u\) 的祖先和後代全部拿出來跑 Dijkstra。

注意到樹高 \(\mathcal O(n)\),因此每個點和邊只會被計算 \(\mathcal O(n)\) 次。

時間複雜度 \(\mathcal O(mn\log m)\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=(1<<18)+5,MOD=998244353;
const ll inf=1e18;
struct Edge { int v; ll w; };
vector <Edge> G[MAXN];
int n,m,d[MAXN],to[MAXN];
ll a[MAXN],f[MAXN],dis[MAXN],ans;
bool vis[MAXN];
void dfs(int u) {
	if(d[u]==1) return ;
	for(int v:{u<<1,u<<1|1}) d[v]=d[u]>>1,dfs(v),f[u]=(f[u]+f[v]+a[v]*d[v])%MOD;
}
void init(int x) {
	vis[x]=false,dis[x]=inf;
	if(d[x]>1) init(x<<1),init(x<<1|1);
}
void solve(int s) {
	for(int x=s;x;x>>=1) to[x>>1]=x,vis[x]=false,dis[x]=inf;
	init(s);
	priority_queue <array<ll,2>,vector<array<ll,2>>,greater<array<ll,2>>> Q;
	Q.push({dis[s]=0,s});
	while(Q.size()) {
		int u=Q.top()[1]; Q.pop();
		if(vis[u]) continue;
		vis[u]=true,ans=(ans+dis[u])%MOD;
		for(auto e:G[u]) if(dis[e.v]>dis[u]+e.w) Q.push({dis[e.v]=dis[u]+e.w,e.v});
		auto ext=[&](int v) {
			if(dis[v]>dis[u]+a[v]) Q.push({dis[v]=dis[u]+a[v],v});
		};
		if(u<s) ext(to[u]);
		else if(d[u]>1) ext(u<<1),ext(u<<1|1);
	}
	G[s].clear();
	for(int x=s>>1;x;x>>=1) if(vis[x]) {
		G[s].push_back({x,dis[x]});
		int y=to[x]^1;
		ans=(ans+f[y]+(dis[x]+a[y])*d[y])%MOD;
	}
}
signed main() {
	scanf("%d%d",&n,&m);
	for(int i=2;i<(1<<n);++i) scanf("%lld",&a[i]);
	for(int i=1,u,v,w;i<=m;++i) scanf("%d%d%d",&u,&v,&w),G[v].push_back({u,w});
	d[1]=(1<<n)-1,dfs(1);
	for(int u=1;u<(1<<n);++u) solve(u);
	printf("%lld\n",ans);
	return 0;
}



F. [P9482] String

Problem Link

題目大意

給定長度為 \(n\) 的字串 \(S\)\(q\) 次詢問給定 \(x,k\),求有多少 \(i\in[1,k]\) 滿足 \(S[x,x+i-1]<\mathrm{rev}(S[x+i,x+2i-1])\)\(<\) 表示字典序比較。

資料範圍:\(n,q\le 10^5\)

思路分析

考慮如何判定一組 \((x,i)\) 合法,首先可以比較字尾 \(S[x,n]\) 和字首 \(S[1,x+2i-1]\),可以對 \(S+\mathrm{rev}(S)\) 建字尾陣列處理出每個字尾 \(S[i,n]\) 的排名 \(R_i\) 和字首的排名 \(L_{x+2i-1}\)

那麼一組 \((x,i)\) 合法當且僅當 \(R_x<L_{x+2i-1}\) 並且 \(S[x,x+2i-1]\) 不是迴文串。

先考慮怎麼對滿足第一個條件的點計數,對 \(L\) 降序掃描線,相當於求 \([x,x+2k)\) 中有多少被已插入的元素和 \(x\) 奇偶性不同,對奇數和偶數分別建樹狀陣列維護即可。

然後我們要去掉 \(R_x<L_{x+2i-1}\)\(S[x,x+2i-1]\) 迴文的情況,設以 \((i,i+1)\) 為迴文中心的最長迴文半徑為 \(d_i\),那麼第二個條件就是 \(d_{i+x-1}\ge i\)

第一個條件不好處理,但我們發現 \(S[x,x+i-1]=S[x+i,x+2i-1]\) 時一定有 \([R_x<L_{x+2i-1}]=[R_{i+x}<L_{i+x-1}]\),事實上就是給兩個串的開頭刪去相等的一段字元。

那麼我們只要把不滿足 \(R_{i+1}<L_i\)\(d_i\) 設成 \(-\infty\),然後只要數 \(i\in [x,x+k-1]\) 中有多少 \(i-d_i+1\le x\),注意到 \(i< x\) 的時候只要 \(d_i\ne-\infty\) 恆成立,因此可以預處理字首和解決一半。

時間複雜度 \(\mathcal O((n+q)\log n)\)

程式碼呈現

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int MAXN=2e5+5;
mt19937_64 rnd(time(0));
char str[MAXN];
int sa[MAXN],rk[MAXN],wt[MAXN],len[MAXN],ht[MAXN][20];
int bit(int x) { return 1<<x; }
void init(int n) {
	iota(sa+1,sa+n+1,1);
	sort(sa+1,sa+n+1,[&](int x,int y){ return str[x]<str[y]; });
	for(int i=1,j;i<=n;) {
		for(j=i;j<n&&str[sa[j+1]]==str[sa[i]];++j);
		len[i]=j-i+1;
		while(i<=j) rk[sa[i++]]=j;
	}
	for(int k=1;k<n;k<<=1) {
		for(int l=1,r;l<=n;++l) if(len[l]>1) {
			r=l+len[l]-1;
			for(int i=l;i<=r;++i) wt[sa[i]]=(sa[i]+k>n?0:rk[sa[i]+k]);
			sort(sa+l,sa+r+1,[&](int x,int y){ return wt[x]<wt[y]; });
			for(int i=l,j;i<=r;) {
				for(j=i;j<r&&wt[sa[j+1]]==wt[sa[i]];++j);
				len[i]=j-i+1;
				while(i<=j) rk[sa[i++]]=j;
			}
			l=r;
		}
	}
	for(int i=1,k=0;i<=n;++i) {
		k=max(k-1,0);
		while(str[i+k]==str[sa[rk[i]-1]+k]) ++k;
		ht[rk[i]][0]=k;
	}
	for(int k=1;k<20;++k) for(int i=1;i+bit(k)-1<=n;++i) {
		ht[i][k]=min(ht[i][k-1],ht[i+bit(k-1)][k-1]);
	}
}
int lcp(int x,int y) {
	int l=min(rk[x],rk[y])+1,r=max(rk[x],rk[y]),k=__lg(r-l+1);
	return min(ht[l][k],ht[r-bit(k)+1][k]);
}
int n,q,L[MAXN],R[MAXN],id[MAXN],ans[MAXN],d[MAXN],cnt[MAXN];
bool mk[MAXN];
vector <array<int,3>> Q1[MAXN],Q2[MAXN];
struct FenwickTree {
	int tr[MAXN],s;
	void init() { memset(tr,0,sizeof(tr)); }
	void add(int x) { for(;x<=n;x+=x&-x) ++tr[x]; }
	int qry(int x) { for(s=0;x;x&=x-1) s+=tr[x]; return s; }
}	T[2];
void solve() {
	scanf("%d%d%s",&n,&q,str+1);
	str[n+1]='#',str[2*n+2]='|';
	for(int i=1;i<=n;++i) str[2*n+2-i]=str[i];
	init(2*n+2);
	for(int i=1;i<=n;++i) R[i]=rk[i],L[i]=rk[2*n+2-i];
	for(int i=1;i<n;++i) {
		d[i]=lcp(i+1,2*n+2-i);
		mk[i]=(R[i+1]<L[i]),cnt[i]=cnt[i-1]+mk[i];
	}
	iota(id+1,id+n+1,1);
	sort(id+1,id+n+1,[&](int x,int y){ return L[x]<L[y]; });
	for(int i=1,x,k;i<=q;++i) {
		scanf("%d%d",&x,&k),ans[i]=0;
		int l=1,r=n,p=n+1;
		while(l<=r) {
			int m=(l+r)>>1;
			if(R[x]<L[id[m]]) p=m,r=m-1;
			else l=m+1;
		}
		if(p<=n) {
			Q1[p].push_back({x,k,i});
			ans[i]+=cnt[x-1];
			Q2[x+k-1].push_back({x,k,i});
		}
	}
	T[0].init(),T[1].init();
	for(int i=n;i>=1;--i) {
		T[id[i]&1].add(id[i]);
		for(auto z:Q1[i]) {
			int x=z[0],k=z[1],r=(x^1)&1;
			ans[z[2]]+=T[r].qry(x+2*k-1)-T[r].qry(x-1);
		}
	}
	T[0].init();
	for(int i=1;i<=n;++i) {
		if(mk[i]) T[0].add(i-d[i]+1);
		for(auto z:Q2[i]) ans[z[2]]-=T[0].qry(z[0]);
	}
	for(int i=1;i<=q;++i) printf("%d\n",ans[i]);
	for(int i=1;i<=n;++i) Q1[i].clear(),Q2[i].clear();
}
signed main() {
	int C,O; scanf("%d%d",&C,&O);
	while(O--) solve();
	return 0;
}



G. [P9479] Tree

Problem Link

題目大意

給定 \(n\) 個節點的樹,保證父親節點編號小於兒子,求有多少 \(n+m\) 個節點的樹(根節點為 \(1\)),使得:

  • 對於所有 \(i,j\in[1,n]\),在兩棵樹上 \(\mathrm{LCA}(i,j)\) 的標號相同。
  • 對於所有 $ i,j$,在新樹上 \(\mathrm{LCA}(i,j)\le \max(i,j)+k\)

資料範圍:\(n\le 3\times 10^4,m\le 3000,k\le 10\)

思路分析

考慮 \(k=0,m=1\) 如何做,不難發現第一個條件等價於 \(1\sim n\) 的虛樹不變,因此只需要在原樹上插入一個點,可以插在邊中間或者掛在節點下面,方案數 \(2n-1\)

對於 \(k=0\) 的一般情況,此時要求每個字首的虛樹都不包含更大的節點,從 \(n+1\sim n+m\) 依次插入每個節點,方案數 \(\prod_{i=n}^{n+m-1}(2i-1)\)

對於原問題,我們依然考慮依次插入 \(i=n+1\sim n+m\),但是插入 \(i\)\(i\) 可以不在 \(1\sim i-1\) 的虛樹上,而是透過一個 \(\mathrm{LCA}\) 掛上去,而這個 \(\mathrm{LCA}\) 一定在 \([i+1,i+k]\) 範圍內。

因此對於每個 \(i\),要麼直接將插入樹上,有 \(2(i-1)-1\) 種方案,要麼在 \([i+1,i+k]\) 中另選擇一個節點插在某條邊中間,然後把 \(i\) 掛在該節點下面。

不難設計出一個 dp,\(f_{i,s}\) 表示當前已經插入 \(1\sim i\)\([i+1,i+k]\) 中已經被插入的元素是集合 \(s\),轉移時如果 \(0\in s\) 就跳過,否則按上述過程轉移,注意此時樹的大小是 \(i+|s|\)

時間複雜度:\(\mathcal O(n+mk2^k)\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MOD=1e9+7;
int n,m,k,f[1<<10],pc[1<<10];
ll g[1<<10];
void solve() {
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1;i<n;++i) scanf("%*d");
	memset(f,0,sizeof(f)),memset(g,0,sizeof(g));
	if(!k) {
		ll s=1;
		for(int i=n;i<n+m;++i) s=s*(2*i-1)%MOD;
		return printf("%lld\n",s),void();
	}
	for(int i=1;i<(1<<k);++i) pc[i]=pc[i>>1]+(i&1);
	f[0]=1;
	for(int i=n;i<n+m;++i) {
		for(int s=0;s<(1<<k);++s) {
			if(s&1) g[s>>1]+=f[s];
			else {
				int z=i+pc[s],t=s>>1;
				g[t]+=1ll*f[s]*(2*z-1);
				for(int j=0;j<k;++j) if(!(t>>j&1)) g[t|(1<<j)]+=1ll*f[s]*(z-1);
			}
		}
		for(int s=0;s<(1<<k);++s) f[s]=g[s]%MOD,g[s]=0;
	}
	printf("%d\n",f[0]);
}
signed main() {
	int c,T; scanf("%d%d",&c,&T);
	while(T--) solve();
	return 0;
}



*H. [P9483] Merge

iProblem Link

題目大意

給定 \(n\) 個元素,每個元素有 \(w,d\) 兩個屬性,初始第 \(i\) 個元素為 \((w_i,0)\),合併元素 \(i\to j\) 的代價為 \(w_i+d_i+d_j\),合併後形成新元素 \((w_i+w_j,2\max(d_i,d_j)+1)\),求把所有元素合併成一個的最小代價。

資料範圍:\(n\le 100\)

思路分析

考慮把元素的合併看成一棵二叉樹,記每個點 \(u\) 子樹的最大深度為 \(d(u)\),那麼所有 \(d\) 的貢獻就是所有非根節點的 \(\sum 2^{d(u)}-1\)

\(w\) 的貢獻可以看成二叉樹上每個點選一個子樹係數 \(+1\),每個葉子的係數就表示該葉子對應 \(w_i\) 對答案的貢獻係數。

容易發現每個葉子的具體排列不重要,確定二叉樹結構後把權值最大的放到係數最小的位置上即可。

因此我們只關心 \(n\) 個係數構成的每一種可重集 \(C\)

可以暴力 dp,\(f_{d,C}\) 表示子樹內最大深度為 \(d\),構成係數可重集為 \(C\) 時,\(\sum 2^{d(u)}-1\) 貢獻的最小值。

轉移時列舉兩個狀態合併,但這樣複雜度太高,無法透過。

我們考慮自上而下地維護這棵二叉樹:即從根節點開始,每次把樹上的一個葉子分裂出兩個兒子節點。

但是每次分裂的時候會影響樹上原有節點的 \(d(u)\),這就需要記錄一些和樹形態有關的資訊,這是完全不能接受的。

考慮最佳化,注意到一個子樹最大深度為 \(d(u)\) 的點,我們可以欽定他在倒數第 \(d(u)\) 次操作時才進行第一次分裂,那麼此後的每一次分裂他的最大深度都 \(+1\),很顯然這個過程不改變最優解。

因此存在一種分裂的方式,使得每次分裂後每個非葉節點的最大深度都 \(+1\),那麼 \(\sum 2^{d(u)}\) 就會翻倍,也容易求出分裂後的 \(\sum 2^{d(u)}-1\)

但是這麼做還不足以透過,首先發現答案不超過 \(1.7\times 10^{11}\),可以用來最佳化 \(\sum 2^{d(u)}-1\) 的上界。

其次我們發現按照上述欽定的過程分裂,前一次分裂過的節點的兩個兒子中至少有一個這次操作也會分裂,因此每次分裂的節點數單調不降,記錄上一次分裂的節點個數即可。

時間複雜度 \(\mathcal O(n^2Q_n+nS_n)\),其中 \(Q\) 表示葉子數 \(1\sim n\) 時的總狀態數,\(S\) 表示葉子數 \(=n\) 時的總狀態數,\(n=100\)\(Q_n=44039,S_n=1745\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll inf=1.7e11;
typedef vector<int> vi;
map <vi,array<ll,2>> f[105];
//[tree size][leaf coef] {min val,low}
ll a[105];
void solve() {
	int n; scanf("%d",&n);
	for(int i=0;i<n;++i) scanf("%lld",&a[i]);
	sort(a,a+n,greater<ll>());
	ll ans=inf;
	for(auto &it:f[n]) {
		const vi&c=it.first;
		ll s=it.second[0];
		for(int i=0;i<n;++i) s+=c[i]*a[i];
		ans=min(ans,s);
	}
	printf("%lld\n",ans);
}
signed main() {
	const int n=100;
	f[1][{0}]={0,1};
	for(int i=1;i<=n;++i) for(auto &it:f[i]) {
		const vi&c=it.first;
		vi d=it.first;
		ll w=it.second[0],lim=it.second[1];
		for(int j=1;j<=i&&i+j<=n;++j) {
			d.insert(upper_bound(d.begin(),d.end(),c[j-1]+1),c[j-1]+1);
			if(j>=lim) {
				ll nw=2*w+(i+j-2);
				if(nw>inf) break;
				if(!f[i+j].count(d)) f[i+j][d]={nw,j};
				else {
					auto &z=f[i+j][d];
					z=min(z,array<ll,2>{nw,j});
				}
			}
		}
	}
	int T; scanf("%d",&T);
	while(T--) solve();
	return 0;
}



*I. [P8500] Inverse

Problem Link

題目大意

給定序列 \(a_1\sim a_n\),有 \(m\) 個限制,形如 \(\min a[l,r]=V\),求一個滿足條件的序列 \(a\) 最小化逆序對數。

資料範圍:\(n,m\le 10^6\)

思路分析

首先發現我們填的所有數一定是某個 \(V\),否則換成小於等於當前數的一個 \(V\) 一定嚴格更優。

先從所有 \(l=r\) 開始,此時相當於一個被確定了若干位的序列,很顯然可以任意填的位置一定是單調不降的,否則排序後肯定更優。

從前往後確定每個位置,我們只需要最小化未填元素和已填元素之間的逆序對,這個問題和未填元素之間的取值無關,只需要每個未填元素填貪心最優解即可(多解取最小)。

容易發現越靠後的位置填的數一定更大,因為代價函式上填更小的數代價會嚴格變大。

然後考慮所有 \([l,r]\) 都不交的情況,很顯然我們要在每個 \([l,r]\) 中選一個位置填 \(V\),我們可以對每個區間內部升序排列,不影響對外部的逆序對。

因此注意到區間所有位置填的都 \(\ge V\),那麼最小值 \(V\) 一定恰好落在 \(a_l\) 上。

然後就是要解決一個有下界限制的問題,依然考慮貪心,從前往後動態維護每個位置上填數的代價函式,每個位置都貪心取最優解,如果有多個則取最小值。

很顯然我們不會選一個比貪心解 \(x\) 更大的值,因為更大的值對後續代價函式的增加量也更大,我們要說明取的數 \(y\) 不會小於貪心解。

如果 \(y<x\),那麼直接把值域在 \([y,x]\) 中的元素推平成 \(x\),這些元素內部的逆序對數會減少,而這些元素和已確定元素之間的逆序對數也會變少,因為每個位置的最優解一定 \(\ge x\)

因此我們可以依然可以用貪心解決這個問題。

接下來回到原問題,我們要考慮如何確定每個區間中填 \(V\) 的數的位置,然後又轉化為前一個特殊性質的貪心問題。

倒序掃描 \(V\),先去除所有有包含關係的限制,然後我們要貪心使得填 \(V\) 的數越靠左越好。

從後往前掃描即可,每個區間如果未滿足就把 \(V\) 放在左端點,容易證明此時對後續區間的選取也是最優的。

注意刪去被標記為 \(\ge V\) 的位置,用線段樹維護剩下的貪心即可。

時間複雜度 \(\mathcal O((n+m)\log n)\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
typedef array<int,2> pii;
const int MAXN=1e6+5;
int n,m,q;
struct SegmentTree {
	pii tr[MAXN<<2];
	int tg[MAXN<<2];
	void adt(int p,int k) { tr[p][0]+=k,tg[p]+=k; }
	void psd(int p) { adt(p<<1,tg[p]),adt(p<<1|1,tg[p]),tg[p]=0; }
	void psu(int p) { tr[p]=min(tr[p<<1],tr[p<<1|1]); }
	void init(int l=1,int r=q,int p=1) {
		tr[p]={0,l},tg[p]=0;
		if(l==r) return ;
		int mid=(l+r)>>1;
		init(l,mid,p<<1),init(mid+1,r,p<<1|1);
	}
	void add(int ul,int ur,int k,int l=1,int r=q,int p=1) {
		if(ul>ur) return ;
		if(ul<=l&&r<=ur) return adt(p,k);
		int mid=(l+r)>>1; psd(p);
		if(ul<=mid) add(ul,ur,k,l,mid,p<<1);
		if(mid<ur) add(ul,ur,k,mid+1,r,p<<1|1);
		psu(p);
	}
	pii qry(int ul,int ur,int l=1,int r=q,int p=1)  {
		if(ul<=l&&r<=ur) return tr[p];
		int mid=(l+r)>>1; psd(p);
		if(ur<=mid) return qry(ul,ur,l,mid,p<<1);
		if(mid<ul) return qry(ul,ur,mid+1,r,p<<1|1);
		return min(qry(ul,ur,l,mid,p<<1),qry(ul,ur,mid+1,r,p<<1|1));
	}
}	T;
struct info { int l,r,v; };
vector <info> I[MAXN];
int a[MAXN],dsu[MAXN];
bool up[MAXN];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
void solve() {
	scanf("%d%d",&n,&m);
	vector <info> lims(m);
	vector <int> vals;
	for(auto &e:lims) scanf("%d%d%d",&e.l,&e.r,&e.v),vals.push_back(e.v);
	sort(vals.begin(),vals.end());
	vals.erase(unique(vals.begin(),vals.end()),vals.end());
	q=vals.size(),T.init();
	for(auto &e:lims){
		e.v=lower_bound(vals.begin(),vals.end(),e.v)-vals.begin()+1;
		I[e.v].push_back(e);
	}
	iota(dsu+1,dsu+n+2,1);
	for(int i=q;i>=1;--i) {
		vector <int> idx;
		for(auto o:I[i]) for(int x=find(o.l);x<=o.r;x=find(x)) {
			idx.push_back(x),a[x]=i,dsu[x]=x+1;
		}
		sort(idx.begin(),idx.end()),idx.push_back(n+1);
		sort(I[i].begin(),I[i].end(),[&](info x,info y){ return x.l^y.l?x.l>y.l:x.r<y.r; });
		int lst=n+1;
		vector <info> J;
		for(auto o:I[i]) if(o.r<lst) J.push_back(o),lst=o.r;
		sort(J.begin(),J.end(),[&](info x,info y){ return x.l>y.l; }),lst=n+1;
		for(auto o:J) if(lst>o.r) {
			lst=*lower_bound(idx.begin(),idx.end(),o.l);
			if(lst<=o.r) up[lst]=true;
			else return puts("-1"),void();
		}
	}
	ll ans=0;
	for(int i=n;i;--i) if(up[i]) ans+=T.qry(a[i],a[i])[0],T.add(a[i]+1,q,1);
	for(int i=1;i<=n;++i) {
		if(up[i]) T.add(a[i]+1,q,-1),T.add(1,a[i]-1,1);
		else {
			auto z=T.qry(max(a[i],1),q,1);
			ans+=z[0],T.add(1,z[1]-1,1);
		}
	}
	printf("%lld\n",ans);
}
signed main() {
	int cs; scanf("%d",&cs);
	while(cs--) {
		solve();
		for(int i=1;i<=n;++i) a[i]=up[i]=0;
		for(int i=1;i<=q;++i) I[i].clear();
	}
	return 0;
}



*J. [P10881] Cycle

Problem Link

題目大意

給定 \(n\) 個點的完全圖,每個點有點權 \(a_i\)\(i,j\) 之間的邊權為 \(a_i+a_j\),求圖上所有生成的基環樹的邊權乘積之和。

資料範圍:\(n\le 1000\)

思路分析

先從樹上邊權乘積之和開始,這顯然是一個矩陣樹定理,我們所求就是 \(\det(D-G)\),其中 \(D_{i,i}=A+na_i\),其中 \(A=\sum a_i\)\(G_{i,j}=a_i+a_j\)

暴力帶入得到:\(\det(D-G)=\sum_p(-1)^{\mathrm{inv}(p)}\prod(D_{i,p_i}-G_{i,p_i})\)

考慮暴力拆開後面的括號,即把原矩陣分成兩部分分別求出 \(D\) 上和 \(G\) 上的行列式,又因為 \(\mathrm{rank}(G)\le 2\),因此我們只要考慮選擇 \(\le 2\)\(G\) 中行的情況。

又因為 \(D\) 是對角線矩陣,因此在 \(D\) 上的行列式只有全取對角線時有貢獻,那麼原式可以化成:

\[\det(D-G)=\prod_{i}D_{i,i}-\sum_iG_{i,i}\prod_{j\ne i}D_{j,j}+\sum_{i,j}(G_{i,i}G_{j,j}-G_{i,j}G_{j,i})\prod_{k\ne i,k\ne j} D_{k,k} \]

\(A_i=A+na_i\),進一步化簡就是:

\[\det(D-G)=\prod_{i}A_i-2\sum_ia_i\prod_{j\ne i}A_j+\sum_{i,j}(2a_ia_j-a_i^2-a_j^2)\prod_{k\ne i,k\ne j} A_k \]

此時我們就有了一個較好的表示式來計算矩陣行列式。

然後考慮基環樹的情況,我們可以暴力列舉一個環,然後把環縮成一個點後求矩陣樹定理的行列式。

先考慮如果確定一個環上的點集 \(S\) 後,如何求出每種情況下環上邊權乘積之和,即 \(\sum_p\prod_i (a_i+a_{p_i})\)

考慮經典做法,拆係數,即把答案表示成 \(\prod a_i^{c_i}\) 的線性組合,對每組 \(\{c_i\}\) 算出對應的係數,查詢時只需要求出每種給 \(a_i\) 分配 \(c_i\) 的方案權值和即可。

容易發現這個式子中每個 \(a_i\) 出現 \(2\) 次,那麼 \(c_i\in\{0,1,2\}\)\(\sum c_i=|S|\),則 \(c_i=0\) 的元素和 \(c_i=2\) 的元素一樣多。

不妨列舉有 \(x\)\(c_i=0\) 的元素,\(y\)\(c_i=1\) 的元素,我們要求的就是分配方案數,可以把這個問題看成給環定向,\(c_i\) 表示出度。

我們可以先插入 \(c_i=0\)\(c_i=2\) 的點,這部分方案數 \(\dfrac{x!^2}{x}\),然後插入 \(c_i=1\) 的點,可以插在當前環上任意兩點中間,會被兩側的點自然定向,方案數 \((2x)^{\overline y}\),總方案數 \(\dfrac{x!^2\times (2x+y-1)!}{(2x)!}\)

然後考慮原問題,我們現在要處理的就是 \(\det(D-G)\) 這部分的貢獻。

首先矩陣樹定理要在原矩陣上去掉一行一列,自然的想法就是把環縮成的虛點對應的行列給刪掉。

然後觀察 \(\det(D-G)\) 的形式,可以用類似的手法考慮拆係數,再一次發現這部分每個 \(a_i\) 最終的係數 \(\in\{0,1,2\}\),列舉是這幾種情況中的哪一種,也不難算出係數。

那麼我們可以把兩部分合起來,綜合算出 \(f_{x,y}\) 表示 \(x\)\(c_i=0\)\(y\)\(c_i=1\)\(n-x-y\)\(c_i=2\) 時對答案的貢獻係數。

具體我們依然先列舉環上 \(c_i=0,1\) 的點數 \(x,y\),剩餘不在環上的點只有 \(n-2x-y\) 個,分討屬於上式的哪個部分,需要列舉的只有後面的 \(\prod\) 號上取了幾個 \(A\) 和個 \(na_i\),總的列舉量是三次方的。

時間複雜度 \(\mathcal O(n^3)\)

程式碼呈現

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1005,MOD=998244353;
inline void add(ll&x,const ll&y) { x=(x+y)%MOD; }
ll ksm(ll a,ll b=MOD-2) { ll s=1; for(;b;a=a*a%MOD,b>>=1) if(b&1) s=s*a%MOD; return s; }
int n;
ll a[N],A,pwn[N],pwA[N],fac[N],ifac[N],C[N][N],rC[N][N];
ll f[N][N],g[N][N];
//f: coef, g: dp
signed main() {
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%lld",&a[i]),A=(A+a[i])%MOD;
	pwn[0]=pwA[0]=ifac[0]=fac[0]=1;
	for(int i=1;i<=n;++i) {
		pwn[i]=pwn[i-1]*n%MOD,pwA[i]=pwA[i-1]*A%MOD;
		ifac[i]=ksm(fac[i]=fac[i-1]*i%MOD);
	}
	for(int i=0;i<=n;++i) {
		for(int j=C[i][0]=1;j<=i;++j) {
			C[i][j]=(C[i-1][j-1]+C[i-1][j])%MOD;
		}
		for(int j=0;j<=i;++j) rC[j][i]=C[i][j];
	}
	for(int i=0;i<=n;++i) for(int j=max(3-2*i,0);2*i+j<=n;++j) {
		//cycle with i a^0/a^2 and j a^1
		int c=n-2*i-j; ll *bi=rC[i],*bj=rC[j];
		ll w=fac[i]*fac[i]%MOD*ifac[2*i]%MOD*fac[2*i+j-1]%MOD;
		for(int x=0,y=c;y>=0;++x,--y) { //|S|=0, x a^0, y a^1
			add(f[i+x][j+y],w*bi[i+x]%MOD*bj[j+y]%MOD*pwA[x]%MOD*pwn[y]);
		}
		for(int x=0,y=c-1;y>=0;++x,--y) { //|S|=1, x a^0, y a^1 in Prod
			ll z=w*(j+y+1)%MOD*bi[i+x]%MOD*bj[j+y]%MOD; 
			add(f[i+x][j+y+1],2*(MOD-z)*pwA[x]%MOD*pwn[y]%MOD);
		}
		for(int x=0,y=c-2;y>=0;++x,--y) { //|S|=2, x a^0, y a^1 in Prod
			ll z=w*bi[i+x]%MOD*bj[j+y]%MOD*pwA[x]%MOD*pwn[y]%MOD;
			add(f[i+x][j+y+2],z*(j+y+2)*(j+y+1));
			add(f[i+x+1][j+y],(MOD-z)*(i+x+1)*(i+1));
		}
	}
	g[0][0]=1;
	for(int o=1;o<=n;++o) {
		ll w1=a[o],w2=a[o]*a[o]%MOD;
		for(int i=o;i>=0;--i) for(int j=o-i;j>=0;--j) {
			g[i][j]=(g[i][j]*w2+(i?g[i-1][j]:0)+(j?g[i][j-1]*w1:0))%MOD;
		}
	}
	ll ans=0;
	for(int i=0;i<=n;++i) for(int j=0;i+j<=n;++j) ans=(ans+f[i][j]*g[i][j])%MOD;
	printf("%lld\n",ans);
	return 0;
}



*K. [P9371] Medium

Problem Link

題目大意

給定 \(a_1\sim a_n\),求一個區間 \([l,r]\) 最大化其中位數的出現次數(長度為偶數可以在中間兩個元素中任選一個)。

資料範圍:\(n\le 5\times 10^5\)

思路分析

考慮刻畫中位數 \(x\),要求 \(\ge x\) 的元素比 \(<x\) 的元素多,\(\le x\) 的元素比 \(>x\) 的元素多。

那麼設 \(X_i\) 表示 \(2[a_i\ge x]-1\) 的字首和,\(Y_i\) 表示 \(2[a_i\le x]-1\) 的字首和。

那麼一個區間 \([l,r]\) 中位數為 \(x\) 就要求 \(Y_r\ge Y_{l-1},X_r\ge X_{l-1}\),這是一個二維偏序問題。

然後考慮對於每個 \(x\) 都解決此問題。

把所有 \((X_i,Y_i)\) 連成線,遇到 \(a_i=x\) 會往右上移動,\(a_i<x\) 往左上移動,\(a_i>x\) 往右上移動。

觀察構成折線的結構,發現是若干連續的斜率為 \(-1\) 的線段,兩條線段中間夾的一段就是一個 \(a_i=x\) 的點。

那麼我們就是要求出距離最遠的兩條線段,使得在右邊的線段上可以選出一個點包含左邊線段上至少一個點。

設右側線段 \(x,y\) 的最大值為 \(R_x,R_y\),左邊線段 \(x,y\) 的最小值為 \(L_x,L_y\),那麼我們可以證明這兩條線段合法當且僅當 \(R_x\ge L_x,R_y\ge L_y\)

那麼這又變成了一個二維偏序問題,按 \(x\) 掃描樹狀陣列維護字首最大值即可。

由於線段樹和當前 \(x\) 的出現次數成線性,因此二維數點的總點數是 \(\mathcal O(n)\) 的。

預處理每條線段的 \(L_x,L_y,R_x,R_y\) 可以直接線段樹維護區間最大最小字首和。

時間複雜度 \(\mathcal O(n\log n)\)

程式碼呈現

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5,inf=1e9,MAXV=2e6+5,V=1e6;
typedef array<int,2> pii;
pii operator +(pii x,pii y) { return {min(x[0],y[0]),max(x[1],y[1])}; }
int n,a[MAXN];
struct SegmentTree {
	pii tr[MAXN<<2]; int tg[MAXN<<2];
	void adt(int p,int k) { tr[p][0]+=k,tr[p][1]+=k,tg[p]+=k; }
	void psd(int p) { if(tg[p]) adt(p<<1,tg[p]),adt(p<<1|1,tg[p]),tg[p]=0; }
	void psu(int p) { tr[p]=tr[p<<1]+tr[p<<1|1]; }
	void add(int ul,int ur,int k,int l=0,int r=n,int p=1) {
		if(ul<=l&&r<=ur) return adt(p,k);
		int mid=(l+r)>>1; psd(p);
		if(ul<=mid) add(ul,ur,k,l,mid,p<<1);
		if(mid<ur) add(ul,ur,k,mid+1,r,p<<1|1);
		psu(p);
	}
	pii qry(int ul,int ur,int l=0,int r=n,int p=1) {
		if(ul<=l&&r<=ur) return tr[p];
		int mid=(l+r)>>1; psd(p);
		if(ur<=mid) return qry(ul,ur,l,mid,p<<1);
		if(mid<ul) return qry(ul,ur,mid+1,r,p<<1|1);
		return qry(ul,ur,l,mid,p<<1)+qry(ul,ur,mid+1,r,p<<1|1);
	}
}	X,Y; //X: >=k?1:-1, Y: <=k?1:-1
vector <int> ps[MAXN];
struct FenwickTree {
	int tr[MAXV]; vector <int> e;
	void build() { fill(tr,tr+MAXV,-inf); }
	void upd(int x,int w) { for(e.push_back(x);x<MAXV;x+=x&-x) tr[x]=max(tr[x],w); }
	int qry(int x) { int s=-inf; for(;x;x&=x-1) s=max(s,tr[x]); return s; }
	void init() { for(int x:e) for(;x<MAXV;x+=x&-x) tr[x]=-inf; e.clear(); }
}	T;
int sequence(int N,vector<int>A) {
	n=N,T.build();
	for(int i=1;i<=n;++i) {
		ps[a[i]=A[i-1]].push_back(i);
		X.add(i,n,1),Y.add(i,n,-1);
	}
	int ans=0;
	for(int o=1;o<=n;++o) if(ps[o].size()) {
		vector <int> p{0};
		for(int z:ps[o]) p.push_back(z),Y.add(z,n,2);
		p.push_back(n+1);
		vector <array<int,3>> Q;
		for(int i=1;i<(int)p.size();++i) {
			pii x=X.qry(p[i-1],p[i]-1),y=Y.qry(p[i-1],p[i]-1);
			Q.push_back({x[0],y[0],-i});
			Q.push_back({x[1],y[1],i});
		}
		sort(Q.begin(),Q.end()),T.init();
		for(int i=0,j;i<(int)Q.size();i=j) {
			for(j=i;j<(int)Q.size()&&Q[i][0]==Q[j][0];++j) {
				if(Q[j][2]<0) T.upd(Q[j][1]+V,Q[j][2]);
				else ans=max(ans,Q[j][2]+T.qry(Q[j][1]+V));
			}
		}
		for(int z:ps[o]) X.add(z,n,-2);
	}
	return ans;
}



*L. [P10302] Distance

Problem Link

題目大意

給定 \(n\) 個點的樹和引數 \(X\),把樹上距離 \(\le X\) 的點全連起來,\(q\) 次詢問標號 \([l,r]\) 的點的匯出子圖中有多少連通塊。

資料範圍:\(n\le 3\times 10^5,q\le 6\times10^5\)

思路分析

這種複雜連通塊計數問題考慮找代表元。

一個很精妙的構造是取每個連通塊中 bfs 序最小的點作為代表元,我們有一個優美的性質:一個點是代表元當且僅當其不存在 bfs 序小於其的鄰居。

我們發現對於 bfs 序遞增的三個節點 \(i,j,k\),分討他們的 \(\mathrm{LCA}\) 形狀可以證明:\(\mathrm{dist}(i,k)\le X,\mathrm{dist}(j,k)\le X\) 時一定有 \(\mathrm{dist}(i,j)\le X\)

根據這個結論,我們知道對於新圖上任意一條路徑 \(p_1\to p_2\to\cdots\to p_k\),如果 \(p_i\) 的 bfs 序大於 \(p_{i-1}\)\(p_{i+1}\),那麼 \(\mathrm{dist}(p_{i-1},p_{i+1})\le X\),可以直接連邊 \(p_{i-1}\to p_{i+1}\)

因此我們能把任意一條路徑規約成 bfs 序先降後升的路徑,那麼如果一個點是代表元,bfs 序小於他的點一定至少對應該點的一條終點 bfs 序小於代表元的出邊,這和假設不符。

因此我們證明了一個點 \(u\) 是代表元當且僅當點集內不存在 bfs 序小於 \(u\) 的點 \(v\) 滿足 \(\mathrm{dist}(u,v)\le X\)

那麼我們只要求出每個點距離 \(\le X\) 的所有點中標號的前驅後繼 \(L_u,R_u\)

距離的限制較為棘手,考慮點分治轉成 \(d_u+d_v\le X\),容易發現這個條件是充分的。

那麼我們按 bfs 序從小到大加,找到 \(d_v\le X-d_u\) 的前驅後繼,可以用線段樹上二分維護。

最終答案就變成數 \(l\in(L_u,u],r\in[u,R_u)\)\(u\) 個數,直接二維數點。

時間複雜度 \(\mathcal O(n\log^2n+q\log n)\)

程式碼呈現

#include<bits/stdc++.h>
using namespace std;
const int MAXN=3e5+5,MAXQ=6e5+5,inf=1e9;
int n,m,X,bfn[MAXN],dep[MAXN],siz[MAXN],cur[MAXN],L[MAXN],R[MAXN];
bool vis[MAXN];
vector <int> G[MAXN];
struct SegmentTree {
	static const int N=1<<19;
	int tr[N<<1];
	void init() {
		fill(tr,tr+(N<<1),inf);
		for(int i=N;i;i>>=1) tr[i]=0;
		for(int i=N+n+1;i;i>>=1) tr[i]=0;
	}
	void upd(int x,int v) { for(x+=N;x;x>>=1) tr[x]=min(tr[x],v); }
	void clr(int x) { for(x+=N;tr[x]<inf&&x;x>>=1) tr[x]=inf; }
	int qpre(int x,int v) {
		for(x+=N;x^1;x>>=1) if((x&1)&&tr[x^1]<=v) {
			for(x^=1;x<=N;x=x<<1|(tr[x<<1|1]<=v));
			return x-N;
		}
		return 0;
	}
	int qsuf(int x,int v) {
		for(x+=N;x^1;x>>=1) if((~x&1)&&tr[x^1]<=v) {
			for(x^=1;x<=N;x=x<<1|(tr[x<<1]>v));
			return x-N;
		}
		return n+1;
	}
}	T;
void solve(int u) {
	vector <int> a;
	function<void(int,int)> dfs2=[&](int x,int fz) {
		siz[x]=1;
		if(dep[x]<=X) a.push_back(x);
		for(int y:G[x]) if(!vis[y]&&y!=fz) {
			dep[y]=dep[x]+1,dfs2(y,x),siz[x]+=siz[y];
		}
	};
	dep[u]=0,dfs2(u,0);
	sort(a.begin(),a.end(),[&](int x,int y){ return bfn[x]<bfn[y]; });
	for(int x:a) {
		L[x]=max(L[x],T.qpre(x,X-dep[x]));
		R[x]=min(R[x],T.qsuf(x,X-dep[x]));
		T.upd(x,dep[x]);
	}
	for(int x:a) T.clr(x);
}
void dfs1(int u) {
	vis[u]=true,solve(u);
	for(int v:G[u]) if(!vis[v]) {
		int rt=0,tot=siz[v];
		function<void(int,int)> dfs3=[&](int x,int fz) {
			cur[x]=tot-siz[x];
			for(int y:G[x]) if(!vis[y]&&y!=fz) {
				dfs3(y,x),cur[x]=max(cur[x],siz[y]);
			}
			if(!rt||cur[x]<cur[rt]) rt=x;
		};
		dfs3(v,u),dfs1(rt);
	}
}
struct FenwickTree {
	int tr[MAXN],s;
	void add(int x,int v) { for(;x;x&=x-1) tr[x]+=v; }
	int qry(int x) { for(s=0;x<=n;x+=x&-x) s+=tr[x]; return s; }
}	F;
vector <array<int,2>> Q[MAXN],M[MAXN];
int ans[MAXQ];
signed main() {
	scanf("%d%d%d",&n,&m,&X);
	for(int u=2,v;u<=n;++u) {
		scanf("%d",&v),G[u].push_back(v),G[v].push_back(u);
	}
	for(int i=1;i<=n;++i) L[i]=0,R[i]=n+1;
	queue <int> q; q.push(1);
	for(int o=1;o<=n;++o) {
		int u=q.front(); q.pop(),bfn[u]=o;
		for(int v:G[u]) if(!bfn[v]) q.push(v);
	}
	T.init(),dfs1(1);
	for(int i=1;i<=n;++i) {
		M[i].push_back({L[i],-1}),M[i].push_back({i,1});
		M[R[i]].push_back({L[i],1}),M[R[i]].push_back({i,-1});
	}
	for(int i=1,l,r;i<=m;++i) scanf("%d%d",&l,&r),Q[r].push_back({l,i});
	for(int i=1;i<=n;++i) {
		for(auto z:M[i]) F.add(z[0],z[1]);
		for(auto z:Q[i]) ans[z[1]]=F.qry(z[0]);
	}
	for(int i=1;i<=m;++i) printf("%d\n",ans[i]);
	return 0;
}