树上问题训练记录 2025.2
Minimum spanning tree for each edge
https://www.gxyzoj.com/d/hzoj/p/4509
最小生成树+LCA
原题,LCA在生成树上找边权最大的边即可
Information Reform
https://www.gxyzoj.com/d/hzoj/p/4510
树形dp
很新的一种 dp 方式,原理上可以说是预支代价,就是将可能产生的代价提前计算
这个题涉及最短路径,可以先 LCA 或 floyd 求出,接下来考虑什么情况会产生贡献
整个贡献分成两部分,一个是放置的位置,一个是路径,所以可以设
首先,对于点
-
也由 转移,那么在之前计算时已经统计过在 放的代价了,所以要 -
由其他点转移,但是如果去枚举这个点,时间复杂度是 的,但是可以记录一个 ,就是当前点的最优转移
接下来考虑如何统计答案,根一定是它的最优决策点
其他的依然是两种情况,如果沿用上面的决策点,记为
点击查看代码
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
int n,k,a[200],edgenum,head[200],d[200][200];
int ans[200];
struct edge{
int to,nxt;
}e[400];
void add_edge(int u,int v)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
head[u]=edgenum;
}
ll f[200][200];
int p[200];
void dfs(int u,int fa)
{
for(int i=1;i<=n;i++)
{
f[i][u]=a[d[i][u]]+k;
}
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
dfs(v,u);
for(int j=1;j<=n;j++)
{
f[j][u]+=min(f[p[v]][v],f[j][v]-k);
}
}
p[u]=1;
for(int i=1;i<=n;i++)
{
if(f[i][u]<f[p[u]][u]) p[u]=i;
}
}
void dfs1(int u,int fa,int pos)
{
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
if(f[p[v]][v]<f[pos][v]-k) ans[v]=p[v];
else ans[v]=pos;
dfs1(v,u,ans[v]);
}
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<n;i++)
{
scanf("%d",&a[i]);
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
d[i][j]=1e9;
}
d[i][i]=0;
}
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add_edge(u,v);
add_edge(v,u);
d[u][v]=d[v][u]=1;
}
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
d[i][j]=min(d[i][k]+d[k][j],d[i][j]);
}
}
}
dfs(1,0);
printf("%lld\n",f[p[1]][1]);
ans[1]=p[1];
dfs1(1,0,p[1]);
for(int i=1;i<=n;i++)
{
printf("%d ",ans[i]);
}
return 0;
}
「JZOI-1」旅行
https://www.gxyzoj.com/d/hzoj/p/LG7359
树形dp+LCA
先从简单的开始考虑,如果对于每组询问单独将链拎出来,依次标号,进行 dp,显然的,设
但是,注意到,其实这个东西的转移时所关系到的状态只有两头是什么,因为唯一要考虑的船,可以认为是在这一段
所以,假设当前已经算好了两组数据的值,只需要知道他们的左右端点就可以拼接
接下来看如何拼接,如果两边待拼的点有一个走陆地,就直接相加,否则,因为两边都造了船,要减去一次造船的贡献
就剩下预处理和统计答案了,先看答案分成了什么
路径是由起点到 lca,这一段跳父亲,和 lca 到终点,这一段从父亲到儿子组成的
所以涉及两个方向,都要先 DFS 处理出来
最后,统计答案可以倍增或树剖+线段树(应该没有人会这么闲吧)合并即可
点击查看代码
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
int n,l,T,edgenum,head[200005];
const ll inf=1e18;
struct edge{
int to,val,nxt,sp,typ;
}e[400005];
struct edge1{
int u,v,w,sp;
}a[200005];
void add_edge(int u,int v,int w,int sp,int typ)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
e[edgenum].val=w;
e[edgenum].sp=sp;
e[edgenum].typ=typ;
head[u]=edgenum;
}
struct node{
ll a,b,c,d;
// l 0,0,1,1
// r 0,1,0,1
//0->路 1->水
}d[2][200005][20];
int f[200005][20],dep[200005];
node merge(node x,node y)
{
node tmp;
if(y.d==1e18) return x;
if(x.d==1e18) return y;
tmp.a=min(min(x.a+y.a,x.a+y.c),min(x.b+y.a,x.b+y.c-l));
tmp.b=min(min(x.a+y.b,x.a+y.d),min(x.b+y.b,x.b+y.d-l));
tmp.c=min(min(x.c+y.a,x.c+y.c),min(x.d+y.a,x.d+y.c-l));
tmp.d=min(min(x.c+y.b,x.c+y.d),min(x.d+y.b,x.d+y.d-l));
return tmp;
}
void getdep(int u,int fa)
{
dep[u]=dep[fa]+1;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
// printf("%d %d\n",u,v);
if(v==fa) continue;
getdep(v,u);
}
}
void dfs(int u,int fa,int typ)
{
f[u][0]=fa;
for(int i=1;i<=19;i++)
{
f[u][i]=f[f[u][i-1]][i-1];
d[typ][u][i]=merge(d[typ][u][i-1],d[typ][f[u][i-1]][i-1]);
}
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
int x=e[i].val;
if(e[i].typ==typ) x-=e[i].sp;
else x+=e[i].sp;
d[typ][v][0]=(node){e[i].val,inf,inf,l+x};
dfs(v,u,typ);
}
}
node lca(int x,int y)
{
int fl=0;
if(dep[x]<dep[y]) swap(x,y),fl=1;
node res1,res2;
res1=res2=(node){0,inf,inf,inf};
for(int i=19;i>=0;i--)
{
if(dep[f[x][i]]>=dep[y])
{
// printf("1");
res1=merge(res1,d[fl][x][i]);
x=f[x][i];
}
if(x==y) return res1;
}
// printf("%lld %lld %lld %lld\n",res1.a,res1.b,res1.c,res1.d);
for(int i=19;i>=0;i--)
{
if(f[x][i]!=f[y][i])
{
res1=merge(res1,d[fl][x][i]);
res2=merge(res2,d[fl^1][y][i]);
x=f[x][i],y=f[y][i];
}
}
res1=merge(res1,d[fl][x][0]),res2=merge(res2,d[fl^1][y][0]);
swap(res2.b,res2.c);
res1=merge(res1,res2);
return res1;
}
int main()
{
scanf("%d%d%d",&n,&l,&T);
for(int i=1;i<n;i++)
{
int u,v,w,sp,typ;
scanf("%d%d%d%d%d",&u,&v,&w,&sp,&typ);
if(typ==0) swap(u,v);
add_edge(u,v,w,sp,0);
add_edge(v,u,w,sp,0);
a[i]=(edge1){u,v,w,sp};
}
getdep(1,0);
edgenum=0;
for(int i=1;i<=n;i++) head[i]=0;
for(int i=1;i<n;i++)
{
int fl=0;
if(dep[a[i].u]<dep[a[i].v]) fl=1;
add_edge(a[i].u,a[i].v,a[i].w,a[i].sp,fl);
add_edge(a[i].v,a[i].u,a[i].w,a[i].sp,fl);
}
dfs(1,0,0);
dfs(1,0,1);
// for(int i=1;i<=n;i++)
// {
// printf("%d ",dep[i]);
// }
while(T--)
{
int u,v;
scanf("%d%d",&u,&v);
node tmp=lca(u,v);
printf("%lld\n",min(min(tmp.a,tmp.b),min(tmp.c,tmp.d)));
}
return 0;
}
「DBOI」Round 1 人生如树
https://www.gxyzoj.com/d/hzoj/p/4512
LCA+二分+hash
先考虑放到数组怎么做,暴力一定是依次匹配,但是数很多,在询问很多的情况下会 T
因为是前
此时,匹配就不能暴力枚举了,因为每个位置的值确定,考虑哈希
接下来看如何把这个东西放到树上,依然是两段,一个是从 u 到 lca,一个是从 lca 到 v(此处的 v 是二分时长度是 mid 的数组的终点,不是原终点)
因为要保证数组的顺序,所以不能在求出 lca 到两点的哈希值后直接拼起来
但是从 lca 到 v 的这部分就是从父亲到儿子,可以直接相减
但是 u 到 lca 这部分,要从儿子跳父亲,为了避免重复,将
对于 a 数组要加上的值,预处理即可
点击查看代码
#include<cstdio>
#include<algorithm>
#define ull unsigned long long
using namespace std;
int n,m,idx,edgenum,head[200005],a[200004];
struct edge{
int to,nxt;
}e[400005];
void add_edge(int u,int v)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
head[u]=edgenum;
}
struct ques{
int u1,v1,u2,v2;
}qs[100005];
int f[200005][20],dep[200005];
ull p1[200005],d[200005][20],d1[200005],p=13331,p2[200005];
void dfs(int u,int fa)
{
dep[u]=dep[fa]+1,d1[u]=d1[fa]*p+a[u];
f[u][0]=fa,d[u][0]=a[fa];
for(int i=1;i<=19;i++)
{
f[u][i]=f[f[u][i-1]][i-1];
d[u][i]=d[u][i-1]*p1[1<<(i-1)]+d[f[u][i-1]][i-1];
}
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
dfs(v,u);
}
}
int lca(int x,int y)
{
if(dep[x]<dep[y]) swap(x,y);
for(int i=19;i>=0;i--)
{
if(dep[f[x][i]]>=dep[y])
{
x=f[x][i];
}
if(x==y) return x;
}
for(int i=19;i>=0;i--)
{
if(f[x][i]!=f[y][i])
{
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
int fl;
ull get(int u,int v,int x)
{
int lc=lca(u,v),tmp=x-1,now=u;
if(-dep[lc]*2+dep[u]+dep[v]+1<x)
{
fl=1;
return 0;
}
fl=0;
ull sum=a[u];
for(int i=19;i>=0;i--)
{
if(tmp>=(1<<i)&&dep[f[now][i]]>=dep[lc])
{
tmp-=(1<<i);
sum=sum*p1[1<<i]+d[now][i];
now=f[now][i];
}
}
if(x>dep[u]-dep[lc]+1)
{
tmp=x-(dep[u]-dep[lc]+1);
sum*=p1[tmp];
tmp=dep[v]-(dep[lc]+tmp),now=v;
for(int i=19;i>=0;i--)
{
if(tmp>=(1<<i))
{
tmp-=(1<<i);
now=f[now][i];
}
}
sum+=d1[now]-d1[lc]*p1[dep[now]-dep[lc]];
}
return sum;
}
bool check(int id,int x)
{
if(x==1)
{
if(a[qs[id].u1]+1==a[qs[id].u2]) return 1;
return 0;
}
ull tmp1=get(qs[id].u1,qs[id].v1,x)+p2[x];
if(fl) return 0;
ull tmp2=get(qs[id].u2,qs[id].v2,x);
if(fl) return 0;
if(tmp1==tmp2) return 1;
else return 0;
}
int main()
{
scanf("%d%d%d",&n,&m,&idx);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add_edge(u,v);
add_edge(v,u);
}
idx=0;
for(int i=1;i<=m;i++)
{
int opt;
scanf("%d",&opt);
if(opt==1)
{
int u1,v1,u2,v2;
scanf("%d%d%d%d",&u1,&v1,&u2,&v2);
qs[++idx]=(ques){u1,v1,u2,v2};
}
else
{
int u,w;
scanf("%d%d",&u,&w);
a[++n]=w;
add_edge(u,n);
add_edge(n,u);
}
}
p1[0]=1;
for(int i=1;i<=n;i++)
{
p1[i]=p1[i-1]*p;
p2[i]=p2[i-1]*p+i;
// printf("%llu %llu\n",p1[i],p2[i]);
}
dfs(1,0);
for(int i=1;i<=idx;i++)
{
int l=0,r=n;
while(l<r)
{
int mid=(l+r+1)>>1;
if(check(i,mid)) l=mid;
else r=mid-1;
}
printf("%d\n",l);
}
return 0;
}
Mobile Phone Network
https://www.gxyzoj.com/d/hzoj/p/4514
树剖+线段树+最小生成树
先强制选上这
首先可以跑一次最小生成树,加入的新边不会影响答案,因为不成环,而成环的边,要保证环上所包含的没有权值的边小于等于当前边
此时,树剖+线段树维护即可
点击查看代码
#include<cstdio>
#include<algorithm>
#define lid id<<1
#define rid id<<1|1
#define ll long long
using namespace std;
const int inf=2e9;
int n,m,k,f[500005],edgenum,head[500005];
int read()
{
int x=0;
char ch=getchar();
while(ch<48||ch>57) ch=getchar();
while(ch>=48&&ch<=57)
{
x=x*10+ch-48;
ch=getchar();
}
return x;
}
struct node{
int u,v,w;
}a[500005],b[500005];
int find(int x)
{
if(f[x]!=x) f[x]=find(f[x]);
return f[x];
}
struct edge{
int to,nxt;
}e[2000005];
void add_edge(int u,int v)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
head[u]=edgenum;
}
int siz[500005],son[500005],dep[500005],top[500005];
int dfn[500005],idx,vis[500005];
inline void dfs(int u,int fa)
{
siz[u]=1,f[u]=fa,dep[u]=dep[fa]+1;
int maxn=-1;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
dfs(v,u);
siz[u]+=siz[v];
if(siz[v]>maxn) maxn=siz[v],son[u]=v;
}
}
inline void dfs1(int u,int t)
{
dfn[u]=++idx,top[u]=t;
if(son[u])
{
dfs1(son[u],t);
}
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==f[u]||v==son[u]) continue;
dfs1(v,v);
}
}
struct seg_tr{
int l,r,mx,lazy,tag;
}tr[4000004];
inline void build(int id,int l,int r)
{
tr[id].l=l,tr[id].r=r,tr[id].mx=tr[id].lazy=inf;
if(l==r) return;
int mid=(l+r)>>1;
build(lid,l,mid);
build(rid,mid+1,r);
}
inline void pushdown(int id)
{
tr[lid].mx=min(tr[lid].mx,tr[id].lazy);
tr[lid].lazy=min(tr[lid].lazy,tr[id].lazy);
tr[rid].mx=min(tr[rid].mx,tr[id].lazy);
tr[rid].lazy=min(tr[rid].lazy,tr[id].lazy);
tr[id].lazy=inf;
}
inline void update(int id,int l,int r,int x)
{
if(l>r||tr[id].tag) return;
if(tr[id].l==l&&tr[id].r==r)
{
tr[id].mx=min(tr[id].mx,x);
tr[id].lazy=min(tr[id].lazy,x);
tr[id].tag=1;
return;
}
pushdown(id);
int mid=(tr[id].l+tr[id].r)>>1;
if(r<=mid) update(lid,l,r,x);
else if(l>mid) update(rid,l,r,x);
else update(lid,l,mid,x),update(rid,mid+1,r,x);
tr[id].tag|=(tr[lid].tag&tr[rid].tag);
}
inline int query(int id,int x)
{
if(tr[id].l==tr[id].r)
{
return tr[id].mx;
}
pushdown(id);
int mid=(tr[id].l+tr[id].r)>>1;
if(x<=mid) return query(lid,x);
else return query(rid,x);
}
void add(int u,int v,int w)
{
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) swap(u,v);
update(1,dfn[top[u]],dfn[u],w);
u=f[top[u]];
}
if(dfn[u]>dfn[v]) swap(u,v);
update(1,dfn[u]+1,dfn[v],w);
}
int main()
{
n=read(),k=read(),m=read();
for(int i=1;i<=n;i++) f[i]=i;
for(int i=1;i<=k;i++)
{
int u,v;
u=read(),v=read();
f[find(u)]=find(v);
a[i]=(node){u,v,0};
add_edge(u,v);
add_edge(v,u);
}
for(int i=1;i<=m;i++)
{
int u,v,w;
u=read(),v=read(),w=read();
b[i]=(node){u,v,w};
}
for(int i=1;i<=m;i++)
{
int v=b[i].v,u=b[i].u;
if(find(u)!=find(v))
{
f[find(u)]=find(v);
add_edge(u,v);
add_edge(v,u);
vis[i]=1;
}
}
dfs(1,0);
dfs1(1,0);
build(1,1,n);
for(int i=1;i<=m;i++)
{
if(!vis[i])
{
add(b[i].u,b[i].v,b[i].w);;
}
}
ll ans=0;
for(int i=1;i<=k;i++)
{
int u=a[i].u,v=a[i].v;
if(dep[u]>dep[v]) swap(u,v);
ll x=query(1,dfn[v]);
if(x==inf)
{
ans=-1;
break;
}
ans+=x;
// printf("%d %d %d\n",u,v,x);
}
printf("%lld",ans);
return 0;
}
Close Vertices
https://www.gxyzoj.com/d/hzoj/p/4514
前置知识:点分治
看过点分治之后这个东西就不难了,先考虑只有一个限制的时候怎么做
和模板题类似,都要先统计出每个点对于当前根的深度,但是如果正常双指针去加的话,会出现两条路径在同一个子树内的情况
考虑容斥,可以固定当前根枚举到的子节点
对于这种二维限制的,树状数组统计即可
点击查看代码
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
int n,len,lim,head[100005],edgenum;
struct edge{
int to,nxt,val;
}e[200005];
ll ans[100005],c[100005];
void add_edge(int u,int v,int w)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
e[edgenum].val=w;
head[u]=edgenum;
}
int lowbit(int x)
{
return x & (-x);
}
void add(int x,int v)
{
if(x>len+2) return;
while(x<=len+2)
{
c[x]+=v;
x+=lowbit(x);
}
}
int query(int x)
{
int res=0;
while(x>0)
{
res+=c[x];
x-=lowbit(x);
}
return res;
}
int siz[100005],sum,rt,mx[100005];
bool vis[100005];
void dfs1(int u,int fa)
{
siz[u]=1,mx[u]=0;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa||vis[v]) continue;
dfs1(v,u);
siz[u]+=siz[v];
mx[u]=max(mx[u],siz[v]);
}
mx[u]=max(mx[u],sum-siz[u]);
if(mx[u]<mx[rt]) rt=u;
}
int dep[100005],dis[100005],cnt;
struct node{
int d1,d2;
}a[100005];
bool cmp(node x,node y)
{
if(x.d2!=y.d2) return x.d2<y.d2;
return x.d1<y.d1;
}
void dfs2(int u,int fa)
{
dep[u]=dep[fa]+1;
a[++cnt]=(node){dep[u],dis[u]};
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa||vis[v]) continue;
dis[v]=dis[u]+e[i].val;
if(dis[v]>lim) dis[v]=lim+1;
dfs2(v,u);
}
}
ll clac(int u)
{
cnt=0;
dfs2(u,0);
sort(a+1,a+cnt+1,cmp);
// printf("a%d\n",u);
ll tmp=0;
for(int i=1;i<=cnt;i++)
{
add(a[i].d1+1,1);
// printf("%d %d\n",a[i].d1,a[i].d2);
// if(a[i].d1<=len&&a[i].d2<=lim) tmp++;
}
int l=1,r=cnt;
// printf("a");
while(l<r)
{
if(a[l].d2+a[r].d2<=lim)
{
add(a[l].d1+1,-1);
tmp+=query(len-a[l++].d1+1);
}
else
{
add(a[r--].d1+1,-1);
}
}
add(a[l].d1+1,-1);
return tmp;
}
void dfs(int u,int fa)
{
vis[u]=1;
dis[u]=0,dep[0]=-1;
ans[u]=clac(u);
// printf("%d ",ans[u]);
// printf("%d %d\n",u,fa);
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa||vis[v]) continue;
dis[v]=e[i].val,dep[0]=0;
ans[u]-=clac(v);
}
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa||vis[v]) continue;
sum=siz[v],rt=0;
dfs1(v,-1);
dfs1(rt,-1);
// printf("%d %d %d\n",u,v,rt);
dfs(rt,u);
}
}
int main()
{
scanf("%d%d%d",&n,&len,&lim);
for(int i=2;i<=n;i++)
{
int v,w;
scanf("%d%d",&v,&w);
add_edge(i,v,w);
add_edge(v,i,w);
}
sum=n,mx[0]=1e9;
dfs1(1,-1);
dfs1(rt,-1);
// printf("%d",rt);
dfs(rt,-1);
ll res=0;
for(int i=1;i<=n;i++)
{
res+=ans[i];
// printf("%d ",ans[i]);
}
printf("%lld",res);
return 0;
}
Nastia Plays with a Tree
https://www.gxyzoj.com/d/hzoj/p/4515
贪心trick题
这个题一个显然的结论就是加边前每个点的度数都小于等于 2,因为加和断的边数相同,所以只要让断的边数尽量小
因为最后都是要让点的度数到达一个范围,可以 DFS,先处理儿子,再处理父亲
假设当前点 u 的所有儿子都已经处理好了,考虑 u,如果 u 点先断掉与儿子相连的边,只会对 u 自己产生贡献
如果先断掉与父亲相连的,则有可能另外对父亲产生贡献,所以当度数不满足条件时,先断掉父亲是更优的
此时,对于儿子,只需留下两个就满足条件
点击查看代码
#include<cstdio>
#include<algorithm>
using namespace std;
int T,n,head[100005],edgenum,cnt1,cnt2;
struct edge{
int to,nxt;
}e[200005];
struct node{
int u,v;
}a[100005],add[100005],del[100005];
void add_edge(int u,int v)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
head[u]=edgenum;
}
int vis[100005],f[100005],d[100005],dep[100005];
int find(int x)
{
if(f[x]!=x) f[x]=find(f[x]);
return f[x];
}
void dfs(int u,int fa)
{
dep[u]=dep[fa]+1;
int d1=1;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
dfs(v,u);
if(!vis[v]) d1++;
}
if(d1<3) return;
d1=2,vis[u]=1;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa||vis[v]) continue;
if(!d1) vis[v]=1;
else d1--;
}
}
int l[100005],r[100005];
int main()
{
// freopen("1.txt","w",stdout);
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
edgenum=0;
for(int i=1;i<=n;i++) d[i]=vis[i]=head[i]=l[i]=r[i]=0,f[i]=i;
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add_edge(u,v);
add_edge(v,u);
a[i]=(node){u,v};
}
dfs(1,0);
int sum=0;
for(int i=2;i<=n;i++) sum+=vis[i];
printf("%d\n",sum);
if(sum==0) continue;
for(int i=1;i<n;i++)
{
int v=a[i].v,u=a[i].u;
if(dep[u]<dep[v]) swap(u,v);
if(vis[u])
{
del[++cnt1]=a[i];
continue;
}
// printf("%d %d\n",u,v);
d[u]++,d[v]++;
f[find(u)]=find(v);
}
for(int i=1;i<=n;i++)
{
// printf("%d ",d[i]);
if(d[i]==1)
{
int x=find(i);
if(l[x]) r[x]=i;
else l[x]=i;
}
if(d[i]==0) l[i]=r[i]=i;
}
int lst=0;
for(int i=1;i<=n;i++)
{
// printf("%d %d\n",l[i],r[i]);
if(l[i])
{
if(lst) add[++cnt2]=(node){lst,l[i]};
lst=r[i];
}
}
for(int i=1;i<=sum;i++)
{
printf("%d %d %d %d\n",del[i].u,del[i].v,add[i].u,add[i].v);
}
cnt1=cnt2=0;
}
return 0;
}
[ARC165E] Random Isolation
https://www.gxyzoj.com/d/hzoj/p/4508
树形dp
实在太抽象了,其实可以将题目转化为对于一个随机的长度为
假设当前的子树
根据期望的线性性,可以树形dp
如果要保证这个联通快的出现,必然要使得所有与之相连的边都断掉,所以需要额外记录边数
因为连向父亲的边不好统计,所以设状态为
最后统计答案时,一个长度为
点击查看代码
#include<cstdio>
#define ll long long
using namespace std;
const int mod=998244353;
int n,m,edgenum,head[105];
struct edge{
int to,nxt;
}e[205];
void add_edge(int u,int v)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
head[u]=edgenum;
}
ll f[105][105][105],d[105][105],fac[205],inv[205];
int siz[105];
ll qpow(ll x,int y)
{
ll res=1;
while(y)
{
if(y&1) res=res*x%mod;
x=x*x%mod;
y>>=1;
}
return res;
}
void dp(int u,int v)
{
f[v][0][1]=1;
for(int i=0;i<=siz[u];i++)
{
for(int j=0;j<=siz[u];j++)
{
for(int k=0;k<=siz[v];k++)
{
for(int l=0;l<=siz[v];l++)
{
d[i+k][j+l]+=f[u][i][j]*f[v][k][l]%mod;
d[i+k][j+l]%=mod;
}
}
}
}
siz[u]+=siz[v];
for(int i=0;i<=siz[u];i++)
{
for(int j=0;j<=siz[u];j++)
{
f[u][i][j]=d[i][j];
d[i][j]=0;
}
}
}
void dfs(int u,int fa)
{
f[u][1][0]=1,siz[u]=1;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
dfs(v,u);
dp(u,v);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add_edge(u,v);
add_edge(v,u);
}
dfs(1,0);
fac[0]=inv[0]=1;
for(int i=1;i<=2*n;i++)
{
fac[i]=fac[i-1]*i%mod;
inv[i]=qpow(fac[i],mod-2);
// printf("%d %d\n",fac[i],inv[i]);
}
ll ans=0;
for(int i=1;i<=n;i++)
{
for(int j=m+1;j<=siz[i];j++)
{
for(int k=0;k<=siz[i];k++)
{
int x=k+(i!=1);
ans+=f[i][j][k]*fac[x]%mod*fac[j]%mod*inv[x+j]%mod;
ans%=mod;
}
}
}
printf("%lld",ans);
return 0;
}
「LNOI2014」LCA
https://www.gxyzoj.com/d/hzoj/p/1239
原,不说了
Tree and Queries
https://www.gxyzoj.com/d/hzoj/p/4516
显然的,这个东西可以通过 DFS 序拍成一个序列,询问变成查询区间,接下来看怎么做
它有两个性质,一个是位置范围,一个是出现次数,对于这种无法整段处理的东西,考虑莫队
每次加入或删除一个点,就在这个数的
因为莫队是依次添加或删除的,所以这个东西一定更新全了,最后时间复杂度
点击查看代码
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
int n,m,a[100005],edgenum,head[100005];
struct edge{
int to,nxt;
}e[200005];
void add_edge(int u,int v)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
head[u]=edgenum;
}
int dfn[100005],siz[100005],idx,pos[100005],rnk[100005];
void dfs(int u,int fa)
{
dfn[u]=++idx,siz[u]=1,rnk[idx]=u;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
dfs(v,u);
siz[u]+=siz[v];
}
}
struct ques{
int l,r,x,id;
}q[100005];
bool cmp(ques x,ques y)
{
if(pos[x.l]!=pos[y.l]) return x.l<y.l;
return x.r<y.r;
}
int sum[100005],ans[100005],cnt[100005];
void del(int x)
{
x=rnk[x];
cnt[a[x]]--;
if(cnt[a[x]]>=0)
{
sum[cnt[a[x]]+1]--;
}
}
void add(int x)
{
x=rnk[x];
cnt[a[x]]++;
if(cnt[a[x]]>=0)
{
sum[cnt[a[x]]]++;
}
}
int main()
{
scanf("%d%d",&n,&m);
int blen=sqrt(n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
pos[i]=(i-1)/blen+1;
}
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add_edge(u,v);
add_edge(v,u);
}
dfs(1,0);
for(int i=1;i<=m;i++)
{
int u,k;
scanf("%d%d",&u,&k);
q[i]=(ques){dfn[u],dfn[u]+siz[u]-1,k,i};
}
sort(q+1,q+m+1,cmp);
int l=1,r=0;
for(int i=1;i<=m;i++)
{
while(l<q[i].l) del(l++);
while(l>q[i].l) add(--l);
while(r>q[i].r) del(r--);
while(r<q[i].r) add(++r);
ans[q[i].id]=sum[q[i].x];
}
for(int i=1;i<=m;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!