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\) 种颜色可染的方案数。
- 若点 \(u\) 不是关键点,那么选什么都可以。
-
若点 \(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\) 都是非关键点,那么可以把这条边缩掉:
把所有连接非关键点的边缩掉之后,新树由三种节点组成:关键点、骨干点、非关键叶子。骨干点指儿子有关键点的非关键点,其中前两类个数是 \(\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\),那么转移方程可以写成:
这里省略了第二类点,实际上是
第二部分处理 \(u\) 是骨干点的转移:
发现非关键叶子虽然很多,但是只与子树大小有关,把子树大小相同的点一起处理,因为总和不大于 \(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\) 个操作,每个操作有两种:
-
不改变角度地取下/放回某个齿轮
-
将齿轮 \(u\) 转动 \(x\) 度,相邻的齿轮会反方向跟着转相同的角度
每次操作 \(2\) 输出这次操作转动的齿轮数量乘转动角度。所有操作完成后输出所有齿轮的角度之和,角度对 \(360\) 取模。
\(1\leq n,q\leq 10^5\)
做法
令 \(u\) 为 \(v\) 所在连通块的最高点,如何刻画这个连通块?发现这个连通块的点都满足在 \(u\) 子树内,且到根的路径上被取下的点个数相同。操作 \(1,2\) 都与子树有关,在 dfn 序上建立线段树。
记 \(c_u\) 表示 \(u\) 到根的路径上有几个被取下的点,这棵线段树需要实现的功能是:
-
区间加 \(c\) 数组,单点查询 \(c_u\)。
-
区间内 \(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\) 的子树,但是有一些齿轮我们并不像让它加,对于一个已经取下的齿轮,它的子树内的齿轮应不受祖先的影响,当这个齿轮被重新放上去的时候,减去这个齿轮的增加量就可以抵消不该有的祖先的贡献。所有操作完成后,让所有齿轮都放回,计算答案。

浙公网安备 33010602011771号