赛前训练 12 extra 树上差分&倍增

A

树上差分板子.

B

每个点只有一条出边的有向图可以看作树

基于上述结论,我们直接倍增维护 \(\min,\operatorname{sum}\) 即可.

实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
#define int long long
using namespace std;

const int N=1e5+5,M=48;
int n,k;
vector<int> G[N];
int dp[N][M],mn[N][M],sum[N][M];

void solve(int x){
	int m=k,cur=x,ansmn=1e18,anss=0;
	for(int j=34;j>=0;j--){
		if((1ll<<j)<=m){
			ansmn=min(ansmn,mn[cur][j]);
			anss+=sum[cur][j];
			cur=dp[cur][j];
			m-=(1ll<<j);
		}
	}
	cout<<anss<<' '<<ansmn<<'\n';
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>k;
	memset(mn,0x3f,sizeof mn);
	for(int i=1,u;i<=n;i++){
		cin>>u,u++;
		dp[i][0]=u;
	}
	for(int i=1,w;i<=n;i++){
		cin>>w;
		mn[i][0]=sum[i][0]=w;
	}
	for(int j=1;j<=34;j++){
		for(int i=1;i<=n;i++){
			dp[i][j]=dp[dp[i][j-1]][j-1];
			mn[i][j]=min(mn[i][j-1],mn[dp[i][j-1]][j-1]);
			sum[i][j]=sum[i][j-1]+sum[dp[i][j-1]][j-1];
		}
	}
	for(int i=1;i<=n;i++)
		solve(i);
	return 0;
}

C

容易发现每个点前面第一个和它不互质的数组成的子串必须得分成一段,这个很显然可以双指针维护.

然后对于区间 \([l,r]\),我们可以暴力跳出有多少个区间,但这样是 \(\mathcal{O}(n)\) 的,考虑倍增加速即可.

实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
#define int long long
using namespace std;

const int N=1e5+5,V=1e5,M=48;
int n,tot,q;
int a[N],p[N],st[N][M];
bool vis[N],ok[N];
vector<int> s[N];

void shai(){
	for(int i=2;i<=V;i++){
		if(!vis[i]){
			p[++tot]=i;
			for(int j=i*2;j<=V;j+=i)
				vis[j]=1;
		}
	}
	for(int i=1;i<=tot;i++)
		for(int j=p[i];j<=V;j+=p[i])
			s[j].emplace_back(i);
}
int qry(int l,int r){
	int res=0;
	for(int j=34;j>=0;j--){
		if(st[l][j]<=r){
			l=st[l][j];
			res+=(1ll<<j);
		}
	}
	res++;
	return res;
}
bool add(int x){
	for(int i:s[x])
		if(ok[i])
			return 0;
	for(int i:s[x])
		ok[i]=1;
	return 1;
}
void del(int x){
	if(!x)
		return;
	for(int i:s[x])
		ok[i]=0;
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>q;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	shai();
	for(int i=1,j=0;i<=n;i++){
		del(a[i-1]);
		while(j<n&&add(a[j+1]))
			j++;
		st[i][0]=j+1;
	}
	st[n+1][0]=n+1;
	for(int j=1;j<=34;j++){
		st[n+1][j]=n+1;
		for(int i=1;i<=n;i++)
			st[i][j]=st[st[i][j-1]][j-1];
	}
	while(q--){
		int l,r;
		cin>>l>>r;
		cout<<qry(l,r)<<'\n';
	}
	return 0;
}

D

对于一个节点,考虑其子树内和子树外的情形.

如果子树内有和当前节点相同的种类,则子树外的全部都得 ban,那个节点的兄弟们也得 ban,只有它自己子树内的不用 ban.

子树外的呢?画个图可以知道,只有那个子树外的节点到当前节点路径上中间的要 ban,结合上一种情形,我们发现只需要把当前节点的子树 ban 掉即可.

上述操作可以把树拍成序列后完成,搞完后做一遍前缀和即可.

实现
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <stdlib.h>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <map>
#include <set>
#define int long long
using namespace std;

const int N=2e5+5;
int n,ans,all;
int a[N],b[N],siz[N],cnt[N],tot[N],dfn[N],out[N],d[N],s[N];
vector<int> G[N];

void dfs(int cur,int fa){
	siz[cur]=1;
	dfn[cur]=++all;
	int pre=cnt[a[cur]];
	cnt[a[cur]]++;
	for(int i:G[cur]){
		if(i==fa)
			continue;
		int tmp=cnt[a[cur]];
		dfs(i,cur);
		siz[cur]+=siz[i];
		if(tmp!=cnt[a[cur]]){
			d[1]++;
			d[dfn[i]]--;
			d[out[i]+1]++;
		}
	}
	out[cur]=all;
	if(cnt[a[cur]]-pre<tot[a[cur]]){
		d[dfn[cur]]++;
		d[out[cur]+1]--;
	}
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i],b[i]=a[i];
	sort(b+1,b+n+1);
	int len=unique(b+1,b+n+1)-b-1;
	for(int i=1;i<=n;i++){
		a[i]=lower_bound(b+1,b+len+1,a[i])-b;
		tot[a[i]]++;
	}
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs(1,0);
	int ans=0;
	for(int i=1;i<=n;i++){
		s[i]=s[i-1]+d[i];
		if(!s[i])
			ans++;
	}
	cout<<ans;
	return 0;
}

总结

  • 两个小技巧:

    双指针:维护子串信息

    倍增:加速跳跃过程

  • 树上问题:

    与图的转化(单出边有向图)

    与序列的转化(dfn 序)

    画图、考虑子树外和子树内

以上.

posted @ 2025-10-19 21:28  _KidA  阅读(6)  评论(0)    收藏  举报