20260905noip模拟赛1

A 项目分配

题目描述

\(n\) 个区间,\(m\) 个人,每个人可以拿单个区间或一对交集为空的区间,每个区间只能分配给一个人,输出一种分配方案使得总被分配的区间个数最多。

\(1\leq n,m\leq 3\times 10^5,m\leq n\)

做法

这一题的做法是反悔贪心:维护两个区间池,第一个存失配的区间(暂时不能配对),第二个存已经配对的区间。先按区间左端点排序,每当加入一个新的区间 \(A\) 时,选一个第一个池子内右端点最小的区间,若能配对,将这两个区间配对(定义左端点小的为这一对区间的左区间),移动到第二个池子;否则检查第二个池子中右区间的右端点最小的区间,如果比区间 \(A\) 的右端点小,那么用区间 \(A\) 替换这对区间的右区间,将原本的右区间扔到第一个池子;否则将区间 \(A\) 扔到第一个池。最后得到最多能匹配几对,就做完了。

这样做是对的,最优的待匹配区间(第一个池)保证了当前可匹配的区间最大,每次能配就配就是贪心地最大化匹配对数。反悔贪心就是这样,先贪心能选就选,再调整已经选好的部分。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
const int N=3e5+5;
int n,m,tot,ans[N];
struct node{
	int l,r,id;
}a[N];
bool cmp(node a,node b){
	return a.l<b.l;
}
priority_queue<pair<int,int> >q1;
struct Node{
	int a,b,v;
	bool operator<(Node a)const{
		return v>a.v;
	}
};
priority_queue<Node>q2;
int main(){
	freopen("project.in","r",stdin);
	freopen("project.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i].l>>a[i].r;
		a[i].id=i;
	}
	sort(a+1,a+n+1,cmp);
	for(int i=1;i<=n;i++){
		if(!q1.empty()&&-q1.top().first<a[i].l){
			q2.push((Node){q1.top().second,i,a[i].r});
			q1.pop();
		}else if(!q2.empty()&&q2.top().v<a[i].r){
			int x=q2.top().b,A=q2.top().a;q2.pop();
			q2.push((Node){A,i,a[i].r});
			q1.push(make_pair(-a[x].r,x));
		}else q1.push(make_pair(-a[i].r,i));
	}
	while(!q2.empty()){
		if(tot==m)break;
		ans[a[q2.top().a].id]=ans[a[q2.top().b].id]=++tot;
		q2.pop();
	}
	for(int i=1;i<=n;i++)if(!ans[i]&&tot<m)ans[i]=++tot;
	for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
	return 0;
}

B 四向联通

题目描述

\(n\times m\) 的网格,每个点有一个代价,可以花费这个代价标记该点,要求标记的点连通且上下左右边界都有被标记的点,输出最小代价。

\(v\) 表示每个点花费的最大值。\(1\leq n,m\leq 10^3,v\leq 36\)

做法

建立四个 \(0\) 代价的虚电连接边界,就转化为了关键点个数为 \(4\)斯坦纳树,然而斯坦纳树直接做的时间复杂度很极限,再加上优先队列的常数巨大,无法通过所有测试点。瓶颈在于 dij 太慢,发现每个点的点权最大不超过 \(36\),那么 \(f_{u,S}\) 也不会超过 \(36\times (2n-1)\),扔掉优先队列,从小到大枚举 \(v\),用 \(f_{u,S}=v\) 且未松弛过的点进行松弛,单个 dij 的时间复杂度就变成了 \(\mathcal O(V+n^2)\)\(V\) 是最大可能费用。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#define ppc(x) __builtin_popcount(x)
using namespace std;
typedef long long ll;
const int N=1e3+5,M=1<<4;
const int xx[4]={0,0,1,-1};
const int yy[4]={1,-1,0,0};
int n,m,a[N*N];
char ch;
int f[N*N][M];
bool vis[N*N];
queue<int>q[72*N];
vector<int>h[4];
void dij(int S){
	memset(vis,0,sizeof vis);
	for(int i=1;i<=n*m+4;i++)if(f[i][S]<1061109567)q[f[i][S]].push(i);
	for(int j=0;j<=36*(2*n-1);j++){
		while(!q[j].empty()){
			int u=q[j].front();q[j].pop();
			if(vis[u])continue;
			vis[u]=1;
			if(u>n*m){
				int x=u-n*m-1;
				for(int i=0;i<h[x].size();i++){
					int v=h[x][i];
					if(f[v][S]>f[u][S]+a[v]){
						f[v][S]=f[u][S]+a[v];
						q[f[v][S]].push(v);
					}
				}
				continue;
			}
			int x=u/m,y=u%m;
			if(!y)y=m;
			else x++;
			for(int i=0;i<4;i++){
				int nx=x+xx[i],ny=y+yy[i];
				if(nx<=0||nx>n||ny<=0||ny>m)continue;
				int v=(nx-1)*m+ny;
				if(f[v][S]>f[u][S]+a[v]){
					f[v][S]=f[u][S]+a[v];
					q[f[v][S]].push(v);
				}
			}
		}
	}
	return ;
}
int main(){
	freopen("crosslink.in","r",stdin);
	freopen("crosslink.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n*m;i++){
		cin>>ch;
		if(ch>='0'&&ch<='9')a[i]=ch-'0';
		else a[i]=ch-'A'+10;
	}
	memset(f,0x3f,sizeof f);
	f[n*m+1][1]=f[n*m+2][2]=f[n*m+3][4]=f[n*m+4][8]=0;
	for(int i=1;i<=m;i++)h[0].push_back(i),h[2].push_back((n-1)*m+i);
	for(int i=1;i<=n;i++)h[1].push_back(m*(i-1)+1),h[3].push_back(i*m);
	for(int S=1;S<M;S++){
		for(int T=S&(S-1);T;T=T&(T-1)){
			for(int i=1;i<=n*m+4;i++)f[i][S]=min(f[i][S],f[i][T]+f[i][S^T]-a[i]);
		}
		dij(S);
	}
	int ans=1061109567;
	for(int i=1;i<=n*m+4;i++)ans=min(ans,f[i][(1<<5)-1]);
	cout<<ans;
	return 0;
}

C 楼层任务

题目描述

有一棵以 \(1\) 为根的 \(n\) 个节点的树,有大小为 \(k\) 的关键点集合 \(x\),求将树染成 \(k+1\) 种颜色的方案数,满足 \(\forall i\in[1,k]\),颜色集 \(i\) 不为空集且 LCA 为 \(x_i\)

\(3\leq n\leq 10^6,1\leq k \leq \min(n,2000)\)

做法

一个关键点 \(x_i\) 的影响的范围是它的子树,因为它的子树外不可能有点的颜色为 \(i\)。很可以树形 dp 的样子,考虑朴素树形 dp。

由于统计方案很需要知道一个点能选择的颜色种类个数,设 \(f_{u,i}\) 表示在点 \(u\) 的子树中,有 \(i+1\) 种颜色可染的方案数。

  1. 若点 \(u\) 不是关键点,那么选什么都可以。

\[f_{u,i}=(i+1)\prod_{v\in son_u}f_{v,i} \]

  1. 若点 \(u\) 是第 \(j\) 个关键点,它会让它的子树多一种颜色可以选,必须满足 \(u\) 是 LCA,现在 \(u\) 子树外没有颜色 \(j\)

    • \(u\) 染颜色 \(j\),那么 \(v\) 也是选什么都可以。

    \[f_{u,i}=\prod_{v\in son_u}f_{v,i+1} \]

    • \(u\) 不染颜色 \(j\),因为 \(u\) 必须是颜色集 \(j\) 的 LCA,所以 \(u\) 必须有 \(>1\) 个儿子的子树内有颜色 \(j\),用总方案数减恰好有零个、一个子树内有颜色 \(j\) 的方案数。

    \[f_{u,i}=(i+1)\times(\prod_{v\in son_u}f_{v,i+1}-\prod_{v\in son_u}f_{v,i}-\sum_{v\in son_u}(f_{v,i+1}-f_{v,i})\prod_{w\in son_u\wedge w\neq v}f_{w,i}) \]

最终答案是 \(f_{1,0}\)

朴素 dp 是 \(\mathcal O(nk)\) 的。

因为关键点个数很少,非关键点转移简单,可以缩掉非关键点。

若点 \(u,v\) 都是非关键点,那么可以把这条边缩掉:

\[f_{u,i}=(i+1)\cdot f_{v,i}\cdot\prod_{w\in son_u\wedge w\neq v}f_{w,i}=(i+1)\prod_{w\in son_v}f_{w,i}\cdot (i+1)\prod_{w\in son_u\wedge w\neq v}f_{w,i} \]

把所有连接非关键点的边缩掉之后,新树由三种节点组成:关键点、骨干点、非关键叶子。骨干点指儿子有关键点的非关键点,其中前两类个数是 \(\mathcal O(k)\) 的,但是非关键叶子可能达到 \(\mathcal O(n)\) 级别。

第一部分先处理 \(u\) 是关键点的 dp 转移。把 \(u\) 的儿子分为两种:非关键叶子、关键点和骨干点,设 \(v,w\in son_u\)\(v\) 是第一类,\(w\) 是第二类点,因为儿子之间相对独立,分开处理,先做非关键叶子。对于第一类儿子,设 \(s_u\) 表示点 \(u\) 代表的原树非关键点的个数,那么 \(f_{v,i}=(i+1)^{s_v}\),记 \(S=\sum s_v\),那么转移方程可以写成:

\[\begin{aligned} &f_{u,i}=(i+2)^S+(i+1)\cdot\left((i+2)^S\ -(i+1)^S\ -(i+1)^S\sum\frac{(i+2)^{s_v}}{(i+1)^{s_v}}+|V|\cdot (i+1)^S\right)\\ &\quad\,\,\,\,=(i+2)^{S+1}+(i+1)^{S+1}\cdot\left(|V|-1-\sum(\frac{i+2}{i+1})^{s_v}\right) \end{aligned} \]

这里省略了第二类点,实际上是

\[\begin{aligned} & =(i+2)^S\prod f_{w,i+1}+(i+1)\times\left((i+2)^S\prod f_{w,i+1}-(i+1)^S\prod f_{w,i}-(i+1)^S\prod f_{w,i}\cdot\sum_{x\in son_u}\frac{f_{x,i+1}-f_{x,i}}{f_{x,i}}\right)\\ & =(i+2)^S\prod f_{w,i+1}+(i+1)\times\Bigg((i+2)^S\prod f_{w,i+1}-(i+1)^S\prod f_{w,i}\\ & \hspace{4cm}+(|V|+|W|)\times (i+1)^S\prod f_{w,i}-(i+1)^S\prod f_{w,i}\cdot(\sum(\frac{i+2}{i+1})^{s_v}+\sum\frac{f_{w,i+1}}{f_{w,i}})\Bigg)\\ & =(i+2)^{S+1}\prod f_{w,i+1}+(i+1)^{S+1}\prod f_{w,i}\times\left(-1+|V|+|W|-\sum(\frac{i+2}{i+1})^{s_v}-\sum\frac{f_{w,i+1}}{f_{w,i}}\right) \end{aligned} \]

第二部分处理 \(u\) 是骨干点的转移:

\[f_{u,i}=(i+1)^{s_u}\times(i+1)^S\times\prod f_{w,i} \]

发现非关键叶子虽然很多,但是只与子树大小有关,把子树大小相同的点一起处理,因为总和不大于 \(n\),则种类数不超过 \(\sqrt n\) 个,那么时间复杂度就是 \(k^2+k\cdot \sum_{\text{u是关键点或骨干点}}\sqrt S_u\)\(S_u\) 之和是 \(\mathcal O(n)\),最坏的情况是 \(S_u\) 平均分布,\(\mathcal O(k^2+k^{\frac{3}{2}}\sqrt n)\)\(k^2\) 是第二类儿子转移的复杂度。

由于最终转移式里有除法,如果 \(f_{w,i}\) 在取模意义下等于 \(0\),会出一些问题。需要额外定义一个辅助数组。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1e6+5,M=2e3+5,mod=998244353;
int add(int a){return a>=mod?a-mod:a;}
int ksm(int a,int b){int ans=1;while(b){if(b&1)ans=1ll*ans*a%mod;a=1ll*a*a%mod;b>>=1;}return ans;}
int n,k,tag[N],siz[N],mp[N],mp2[N],cnt,b[N],inv[M];
int fa[N];
int Find(int u){return fa[u]==u?u:fa[u]=Find(fa[u]);}
vector<int>h[N],G[N];
void dfs(int u,int fat){
	siz[u]=!tag[u];
	for(int i=0;i<h[u].size();i++){
		int v=h[u][i];
		if(v==fat)continue;
		dfs(v,u);
		if(!tag[u]&&!tag[v]){
			fa[Find(v)]=Find(u);
			siz[u]+=siz[v];
			tag[v]=2;
		}
	}
	return ;
}
void cv(int* a){
	for(int i=1;i<=n;i++)b[i]=a[i],a[i]=0;
	for(int i=1;i<=cnt;i++)a[i]=b[mp[i]];
	return;
}
bool cmp(int a,int b){
	return tag[a]>tag[b];
}
int st[M],tp,backet[N];
int g[N];
vector<int>f[N];
void dp(int u,int fat){
	int pos=-1,S=siz[u];
	for(int i=0;i<G[u].size();i++){
		int v=G[u][i];
		if(v==fat)continue;
		dp(v,u);
		if(tag[v]&&!tag[u])tag[u]=3;
	}
	sort(G[u].begin(),G[u].end(),cmp);
	tp=0;
	for(int i=0;i<G[u].size();i++){
		int v=G[u][i];
		if(v==fat)continue;
		if(tag[v])pos=i;
		else{
			if(!backet[siz[v]])st[++tp]=siz[v];
			backet[siz[v]]++;
			S+=siz[v];
		}
	}
	if(tag[u]==1){
		for(int i=0;i<=k;i++){
			int s=0,p1=ksm(i+2,S+1),p2=ksm(i+1,S+1);
			for(int j=1;j<=tp;j++)s=add(s+1ll*backet[st[j]]*ksm(1ll*(i+2)*inv[i+1]%mod,st[j])%mod);s=add(mod-s+G[u].size()-2);
			g[pos+1]=1;
			for(int j=pos;j>=0;j--){
				int v=G[u][j],nxt=j+1;
				if(v==fat)continue;
				if(j<pos&&G[u][nxt]==fat)nxt++;
				g[j]=1ll*g[nxt]*f[v][i]%mod;
				p1=1ll*p1*f[v][i+1]%mod;
				p2=1ll*p2*f[v][i]%mod;
			}
			f[u].push_back(add(1ll*p1+1ll*p2*s%mod));
			s=1;p1=ksm(i+1,S+1);
			for(int j=0;j<=pos;j++){
				int v=G[u][j],nxt=j+1;
				if(v==fat)continue;
				if(j<pos&&G[u][nxt]==fat)nxt++;
				f[u][i]=add(f[u][i]+mod-1ll*p1*s%mod*f[v][i+1]%mod*g[nxt]%mod);
				s=1ll*s*f[v][i]%mod;
			}
		}
	}
	if(tag[u]==3){
		for(int i=0;i<=k;i++){
			f[u].push_back(ksm(i+1,S));
			for(int j=0;j<=pos;j++){
				int v=G[u][j];
				if(v==fat)continue;
				f[u][i]=1ll*f[u][i]*f[v][i]%mod;
			}
		}
	}
	for(int i=pos+1;i<G[u].size();i++){
		int v=G[u][i];
		if(v==fat)continue;
		backet[siz[v]]--;
	}
	return ;
}
int main(){
	freopen("tasks.in","r",stdin);
	freopen("tasks.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>k;
	for(int i=1;i<=k;i++){int x;cin>>x;tag[x]=1;}
	for(int i=1;i<=k+1;i++)inv[i]=ksm(i,mod-2);
	for(int u=2;u<=n;u++){
		int v;cin>>v;
		h[u].push_back(v);
		h[v].push_back(u);
	}
	for(int i=1;i<=n;i++)fa[i]=i;
	dfs(1,-1);
	for(int i=1;i<=n;i++)if(tag[i]!=2){
		mp[++cnt]=i;
		mp2[i]=cnt;
	}
	cv(siz);cv(tag);
	for(int u=1;u<=n;u++){
		for(int i=0;i<h[u].size();i++){
			int v=h[u][i];
			if(Find(u)!=Find(v))G[mp2[fa[u]]].push_back(mp2[fa[v]]);
		}
	}
	G[1].push_back(-1);
	dp(1,-1);
	cout<<f[1][0];
	return 0;
}

D 旋转齿轮

题目描述

有一棵 \(n\) 个节点的树,每个节点是一个齿轮,一条边描述两个齿轮的接触关系。

\(q\) 个操作,每个操作有两种:

  1. 不改变角度地取下/放回某个齿轮

  2. 将齿轮 \(u\) 转动 \(x\) 度,相邻的齿轮会反方向跟着转相同的角度

每次操作 \(2\) 输出这次操作转动的齿轮数量乘转动角度。所有操作完成后输出所有齿轮的角度之和,角度对 \(360\) 取模。

\(1\leq n,q\leq 10^5\)

做法

\(u\)\(v\) 所在连通块的最高点,如何刻画这个连通块?发现这个连通块的点都满足在 \(u\) 子树内,且到根的路径上被取下的点个数相同。操作 \(1,2\) 都与子树有关,在 dfn 序上建立线段树。

\(c_u\) 表示 \(u\) 到根的路径上有几个被取下的点,这棵线段树需要实现的功能是:

  1. 区间加 \(c\) 数组,单点查询 \(c_u\)

  2. 区间内 \(c_u=D\)\(u\) 的权值加上 \(v\),查询区间内 \(c_u=D\) 的个数,单点查询点 \(u\) 的权值。

这需要每个节点维护三个变量:

\(p\) 为某个线段树区间,记 \(S_p\) 表示线段树的根到 \(p\)\(ad\) 之和。

令齿轮 \(u\) 满足 \(dfn_u\in p\)\(q\) 表示 \(u\) 在线段树上对应的区间(其实是一个点)。

第一个变量是 \(ad\) 表示这个区间 \(c\) 数组整体加 \(ad\),标记不下传。由于标记不下传,那么 \(c_u\) 等于 \(S_q\)
第二个变量是 \(tag\) 表示这个区间中 \(c_u=S_p\) 的点各自被加了多少,标记下传。
第三个变量是 \(cnt\) 表示这个区间中 \(c_u=S_p\) 的点的个数。

然后就是具体实现,加油自己想。

现在我们还要对某个 \(v\) 快速找到 \(u\),看到有关维护点到根的路径的信息的可以想到重链剖分,直接维护区间最深被取下的点是两只 \(\log\)。讲讲单 \(\log\) 做法,发现只有最深的有取下节点的重链是有用的,需要加速判断一条重链是否含有被取下的点,每条重链记录 \(minn\) 表示最浅的被取下节点的 dfn 序,跳重链的时候根据 \(minn\) 是否小于等于 \(dfn_v\) 就能判断这条重链是否含有被取下的点了。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
typedef long long ll;
const int N=1e5+5,M=20,inf=1061109567;
int add(int a){return a>=360?a-360:a;}
int n,Q,ans[N];
int fa[N][M+1],dep[N],siz[N],hson[N];
int belong[N],dfn[N],Cnt,_dfn[N],tail[N];
int minn[N];
vector<int>h[N];
void dfs1(int u,int fat){
	siz[u]=1;
	for(int i=0;i<h[u].size();i++){
		int v=h[u][i];
		if(v==fat)continue;
		fa[v][0]=u;dep[v]=dep[u]+1;
		for(int j=1;j<=M;j++)fa[v][j]=fa[fa[v][j-1]][j-1];
		dfs1(v,u);
		siz[u]+=siz[v];
		if(siz[v]>siz[hson[u]])hson[u]=v;
	}
	return ;
}
void dfs2(int u,int fat){
	dfn[u]=++Cnt;_dfn[Cnt]=u;
	tail[belong[u]]=max(tail[belong[u]],Cnt);
	minn[belong[u]]=inf;
	if(hson[u]){
		belong[hson[u]]=belong[u];
		dfs2(hson[u],u);
	}
	for(int i=0;i<h[u].size();i++){
		int v=h[u][i];
		if(v==fat||v==hson[u])continue;
		belong[v]=v;
		dfs2(v,u);
	}
	return;
}
struct tree1{
	int mi[N<<2],mx[N<<2];
	void build(int id,int l,int r){
		mi[id]=inf;mx[id]=-inf;
		if(l==r)return;
		int mid=(l+r)>>1,ls=id<<1,rs=ls|1;
		build(ls,l,mid);build(rs,mid+1,r);
		return ;
	}
	void update(int id,int l,int r,int x,int y){
		if(l==r){
			if(y==0)mi[id]=mx[id]=l;
			else mi[id]=inf,mx[id]=-inf;
			return ;
		}
		int mid=(l+r)>>1,ls=id<<1,rs=ls|1;
		if(x<=mid)update(ls,l,mid,x,y);
		else update(rs,mid+1,r,x,y);
		mi[id]=min(mi[ls],mi[rs]);
		mx[id]=max(mx[ls],mx[rs]);
		return;
	}
	pair<int,int> query(int id,int l,int r,int x,int y){
		if(l>=x&&r<=y)return make_pair(mi[id],mx[id]);
		int mid=(l+r)>>1,ls=id<<1,rs=ls|1;
		if(x<=mid&&y>=mid+1){
			pair<int,int> a=query(ls,l,mid,x,y),b=query(rs,mid+1,r,x,y);
			a.first=min(a.first,b.first);
			a.second=max(a.second,b.second);
			return a;
		}
		if(x<=mid)return query(ls,l,mid,x,y);
		return query(rs,mid+1,r,x,y);
	}
}F;
int work(int u,int v){
	if(!v)return 0;
	while(belong[u]!=belong[v]){
		if(dep[belong[u]]>dep[belong[v]])swap(u,v);
		if(minn[belong[v]]<=dfn[v])return F.query(1,1,n,dfn[belong[v]],dfn[v]).second;
		v=fa[belong[v]][0];
	}
	if(dfn[u]>dfn[v])swap(u,v);
	int w=F.query(1,1,n,dfn[u],dfn[v]).second;
	if(w!=-inf)return w;
	return 0;
}
struct tree2{
	int sum[N<<2],ad[N<<2],cnt[N<<2];
	void build(int id,int l,int r){
		sum[id]=0,ad[id]=0,cnt[id]=r-l+1;
		if(l==r)return ;
		int mid=(l+r)>>1,ls=id<<1,rs=ls|1;
		build(ls,l,mid);build(rs,mid+1,r);
		return ;
	}
	void pushdown(int id,int ls,int rs){
		if(sum[id]){
			if(!ad[ls])sum[ls]=add(sum[ls]+sum[id]);
			if(!ad[rs])sum[rs]=add(sum[rs]+sum[id]);
			sum[id]=0;
		}
		return ;
	}
	int update(int id,int l,int r,int x,int y,int cur,int v){
		if(l>=x&&r<=y){
			if(cur^ad[id])return 0;
			return sum[id]=add(sum[id]+v),cnt[id];
		}
		int mid=(l+r)>>1,ls=id<<1,rs=ls|1,sum=0;
		pushdown(id,ls,rs);
		if(x<=mid)sum+=update(ls,l,mid,x,y,cur-ad[id],v);
		if(y>=mid+1)sum+=update(rs,mid+1,r,x,y,cur-ad[id],v);
		return sum;
	}
	void pushup(int id,int ls,int rs){
		cnt[id]=0;
		if(!ad[ls])cnt[id]+=cnt[ls];
		if(!ad[rs])cnt[id]+=cnt[rs];
		return ;
	}
	void modify(int id,int l,int r,int x,int y,int v){
		if(l>=x&&r<=y){
			ad[id]+=v;
			return ;
		}
		int mid=(l+r)>>1,ls=id<<1,rs=ls|1;
		pushdown(id,ls,rs);
		if(x<=mid)modify(ls,l,mid,x,y,v);
		if(y>=mid+1)modify(rs,mid+1,r,x,y,v);
		pushup(id,ls,rs);
		return;
	}
	int qry(int id,int l,int r,int x){
		if(l==r)return ad[id];
		int mid=(l+r)>>1,ls=id<<1,rs=ls|1;
		pushdown(id,ls,rs);
		if(x<=mid)return qry(ls,l,mid,x)+ad[id];
		return qry(rs,mid+1,r,x)+ad[id];
	}
	void query(int id,int l,int r){
		if(l==r){
			ans[++Cnt]=sum[id];
			return ;
		}
		int mid=(l+r)>>1,ls=id<<1,rs=ls|1;
		pushdown(id,ls,rs);
		query(ls,l,mid);query(rs,mid+1,r);
		return ;
	}
}f;
int limto(int u,int top){
	for(int i=M;i>=0;i--)if(dep[fa[u][i]]>dep[top])u=fa[u][i];
	return u;
}
int lst[N];
int main(){
	freopen("gears.in","r",stdin);
	freopen("gears.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v;cin>>u>>v;
		h[u].push_back(v);
		h[v].push_back(u);
	}
	dep[1]=belong[1]=1;
	dfs1(1,-1);dfs2(1,-1);
	F.build(1,1,n);f.build(1,1,n);
	cin>>Q;
	while(Q--){
		int op,u,x;
		cin>>op>>u;
		if(op==1){
			F.update(1,1,n,dfn[u],0);minn[belong[u]]=F.query(1,1,n,dfn[belong[u]],tail[belong[u]]).first;
			f.modify(1,1,n,dfn[u],dfn[u]+siz[u]-1,1);
		}
		if(op==2){
			F.update(1,1,n,dfn[u],1);minn[belong[u]]=F.query(1,1,n,dfn[belong[u]],tail[belong[u]]).first;
			f.modify(1,1,n,dfn[u],dfn[u]+siz[u]-1,-1);
		}
		if(op==3){
			cin>>x;
			int v=work(1,fa[u][0]);v=limto(u,_dfn[v]);
			cout<<x*f.update(1,1,n,dfn[v],dfn[v]+siz[v]-1,f.qry(1,1,n,dfn[v]),((dep[u]&1)?360-x:x))<<"\n";
		}
	}
	Cnt=0;
	f.query(1,1,n);
	for(int i=1;i<=n;i++){
		if(dep[i]&1)ans[dfn[i]]=add(360-ans[dfn[i]]);
		ans[0]+=ans[dfn[i]];
	}
	cout<<ans[0];
	return 0;
}
折叠(另一个做法,但写假了)

本题主要分为两部分:操作 \(2\) 的回答和最终每个齿轮角度的统计。

先看第一部分:操作 \(2\) 怎么做。

实际上就是在问齿轮 \(x\) 所在连通块大小,令 \(v\) 表示某个连通块深度最小的齿轮,那么这个连通块的大小就等于齿轮 \(v\) 的子树大小减 \(v\) 子树内最高的被取下齿轮的子树大小和,最高定义为 \(v\) 子树内该节点的祖先(不包括自己和 \(v\))都没有被取下。

这个维护很简单,齿轮 \(u\) 记录 \(f_u\) 表示将其父亲视为取下,将 \(u\) 视为未取下,\(u\) 所在连通块大小,即 \(u\) 的子树大小减子树内(不包括该齿轮)最高的被取下的齿轮的子树大小和。

\(v\) 表示 \(u\) 最近的被取下的祖先。

  • 删掉齿轮 \(u\)\(f_v\leftarrow f_v-f_u\)

  • 放回齿轮 \(u\)\(f_v\leftarrow f_v+f_u\)

树剖加线段树二分找 \(v\) 的复杂度是 \(\mathcal O(n\log^2 n)\) 的,这是直接跳重链,对每个重链区间查的复杂度。

可以优化到单 \(\log\),发现只有最深的有被取下的齿轮的重链有用,如果对于每个重链能快速判断是否有被取下的齿轮,那么就只需要进行一次线段树二分了。每个重链记录这条重链上被取下的齿轮的最小深度 \(mindep\),在跳重链的时候,每个重链在 \(u\) 到根的路径上出现的是一段前缀 \(belong_x\sim x\),判断 \(mindep\) 是否小于等于 \(x\) 的深度即可。

然后是第二部分:统计最终每个齿轮的转动角度。

相邻的两个齿轮增加值互为相反数可以想到按深度奇偶分类,原本的转动规则等价于整个连通块加 \((-1)^{dep_u}\times x\),然后最后输出的时候在按奇偶乘上系数。

如何维护连通块加?为什么出题人只让我们在最后输出呢?显然是想让我们 \(\mathcal O(1)\) 修改,\(\mathcal O(n)\) 统计啊!这不就是树上差分吗?

考虑维护树上差分数组,每次操作 \(2\),令 \(v\) 表示 \(u\) 所在连通块的最高齿轮,直接加 \(v\) 的子树,但是有一些齿轮我们并不像让它加,对于一个已经取下的齿轮,它的子树内的齿轮应不受祖先的影响,当这个齿轮被重新放上去的时候,减去这个齿轮的增加量就可以抵消不该有的祖先的贡献。所有操作完成后,让所有齿轮都放回,计算答案。

posted @ 2026-09-08 17:08  Tanliu  阅读(7)  评论(0)    收藏  举报