【笔记】生成树
来自\(\texttt{SharpnessV}\)的省选复习计划中的生成树。
从小到大排序,能加边则加边。
结论:任意一条没有加的边\(u -v\),最小生成树上\(u\)和\(v\)之间路径上的边权一定不大于当前边的边权。
证明:反正,如果存在一条大于,我们可以加入当前边而删去最大边,仍然是一颗生成树而权值更小。
这就是 \(\rm Kruscal\) 算法。
#include<cstdio>
#include<algorithm>
int fa[5005];
int get(int x){
return fa[x]==x?x:fa[x]=get(fa[x]);
}
struct node{
int x,y,data;
}a[200005];
bool cmp(node x,node y){
return x.data<y.data;
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
fa[i]=i;
for(int i=1;i<=m;i++)
scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].data);
std::sort(a+1,a+m+1,cmp);int ans=0;
for(int i=1;i<=m;i++){
if(get(a[i].x)!=get(a[i].y))
ans+=a[i].data,fa[get(a[i].x)]=get(a[i].y);
}
printf("%d\n",ans);
return 0;
}
一定存在与最小生成树只相差一条边的次小生成树。
因为每替换一条边都只会使答案更劣,而替换两次显然劣于替换一次。
我们先任意生成一棵树,对于每条额外边,考虑对应路径上的最大边替换。
由于是严格次小生成树,所以如果最大边与当前边权值相同,我们需要替换次大边。
这都可以通过倍增完成。
最小生成树不仅使得边权之和最小,还可使得任意两点路径上得最大值最小。
这里我们跑最大生成树,然后倍增维护两点路径上的边权最小值。
最小生成树建模。
如果元素 \(i\) 在集合 \(j\) 中,则在元素\(i\)和集合\(j\)中连边。以此建出新图。
新图显然是二分图,如果成环,则左右各至少都要经过两个节点,代表两个点之间,同时在两个颜色中出现两次。
所以如果新图成环,则原图成彩虹环。我们要使得新图没有环,直接跑最小生成树即可。
\(\rm Kruscal\) 重构树。
在 \(\rm Kruscal\) 生成树的过程中,每连一条边,就建立一个虚拟节点,左右儿子分别为这条边两边的联通块。
这样我们得到一个大小为\(2N-1\)的生成树。
对于两个节点\(u,v\),它们在新树上的\(\rm LCA\)的权值为原图中两点之间路径最大值的最小值。
再借助可持久化线段树,我们可以完成本题。
跑最短路后建立 \(\rm Kruscal\) 重构树,然后倍增维护即可。
建立最小生成树和最大生成树的重构树,然后倍增出点的取值区间,这就是个二维数点问题。原题强制在线,直接主席树维护一下即可。
表达式生成树。
对于一个表达式,我们建立一颗树,叶子节点为数值,其余节点为运算符。对于前后缀表达式,按照运算顺序直接建出即可。对于中缀表达式,用栈维护顺序并同时建出表达式树。
计算一颗子树的表达式的值,先计算左右子树的值,然后计算总值。
我们发现表达式树实际上包括了运算顺序。因此我们可以借助表达式树完成很多表达式问题。
对于本题我们先建立表达式树,由于每个变量只出现一次,所以我们只用知道该变量对答案是由有影响,这显然可以\(\rm dp\)求得。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 1000005
using namespace std;
char s[20];int ls[N],rs[N],f[N],op[N],top,sta[N],idx,ed[N],u[N],t[N];
int get(){
int n=strlen(s+1),cur=0;
rep(i,2,n)cur=cur*10+s[i]-'0';
return cur;
}
void dfs(int x){
//cout<<x<<" "<<op[x]<<" "<<ls[x]<<" "<<rs[x]<<endl;
if(op[x]>0){f[x]=u[op[x]];return;}
if(op[x]==0)dfs(ls[x]),f[x]=!f[ls[x]];
else{
dfs(ls[x]);dfs(rs[x]);
if(op[x]==-1){
f[x]=f[ls[x]]&f[rs[x]];
if(!f[ls[x]])t[rs[x]]=1;
if(!f[rs[x]])t[ls[x]]=1;
}
else {
f[x]=f[ls[x]]|f[rs[x]];
if(f[ls[x]])t[rs[x]]=1;
if(f[rs[x]])t[ls[x]]=1;
}
}
}
void solve(int x){
if(op[x]>0)ed[op[x]]=t[x]^1;
else if(op[x]==0)t[ls[x]]|=t[x],solve(ls[x]);
else t[ls[x]]|=t[x],t[rs[x]]|=t[x],solve(ls[x]),solve(rs[x]);
}
int main(){
scanf("%s",s+1);
op[++idx]=get();sta[++top]=idx;
while(top){
scanf("%s",s+1);
if(s[1]>='0'&&s[1]<='9')break;
if(s[1]=='x'){
op[++idx]=get();sta[++top]=idx;
}
else if(s[1]=='!'){
op[++idx]=0;ls[idx]=sta[top--];
sta[++top]=idx;
}
else if(s[1]=='&'){
op[++idx]=-1;
ls[idx]=sta[top-1];rs[idx]=sta[top];
top--;top--;sta[++top]=idx;
}
else{
op[++idx]=-2;
ls[idx]=sta[top-1];rs[idx]=sta[top];
top--;top--;sta[++top]=idx;
}
}
int m=strlen(s+1),n=0,rt=sta[top];
rep(i,1,m)n=n*10+s[i]-'0';
rep(i,1,n)scanf("%d",&u[i]);
dfs(rt);solve(rt);int q;scanf("%d",&q);
while(q--){
int x;scanf("%d",&x);
printf("%d\n",f[rt]^ed[x]);
}
return 0;
}
新定义的运算同样可以建立表达式树。
这题我们建立表达式树,然后枚举每个可能的结果计算有多少种方案使得值为当前枚举的结果。
这可以在表达式树上\(\rm DP\)。观察一下发现结果只与变量的排列有关,状压一下即可。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 50005
#define P 1000000007
using namespace std;
int f[1<<10][N][2],ans=0;int n,m,a[11][N],bas,c[N],idx,ls[N],rs[N],val[N],sta[N],op[N],top,tp,rt;char s[N];pair<int,int>u[11];
inline void check(){while(~op[top])val[++idx]=-op[top--],ls[idx]=sta[tp-1],rs[idx]=sta[tp],sta[--tp]=idx;}
void dfs(int x){
if(val[x]>=0){rep(S,0,bas)if((S>>val[x])&1)f[S][x][0]=1;else f[S][x][1]=1;return;}
dfs(ls[x]),dfs(rs[x]);
rep(S,0,bas){
if(-1==val[x])
f[S][x][1]=1LL*f[S][ls[x]][1]*f[S][rs[x]][1]%P,
f[S][x][0]=(1LL*(f[S][ls[x]][0]+f[S][ls[x]][1])*(f[S][rs[x]][0]+f[S][rs[x]][1])-f[S][x][1])%P;
else if(-2==val[x])
f[S][x][0]=1LL*f[S][ls[x]][0]*f[S][rs[x]][0]%P,
f[S][x][1]=(1LL*(f[S][ls[x]][0]+f[S][ls[x]][1])*(f[S][rs[x]][0]+f[S][rs[x]][1])-f[S][x][0])%P;
else f[S][x][0]=(1LL*f[S][ls[x]][0]*f[S][rs[x]][0]*2+1LL*f[S][ls[x]][0]*f[S][rs[x]][1]+1LL*f[S][ls[x]][1]*f[S][rs[x]][0])%P,
f[S][x][1]=(1LL*f[S][ls[x]][1]*f[S][rs[x]][1]*2+1LL*f[S][ls[x]][0]*f[S][rs[x]][1]+1LL*f[S][ls[x]][1]*f[S][rs[x]][0])%P;
}
}
void calc(int x){
rep(i,0,m-1)u[i]=make_pair(a[i][x],i);sort(u,u+m);int S=0;
rep(i,0,m-1)ans+=1LL*(f[S][rt][1]-f[S+(1<<u[i].second)][rt][1])*u[i].first%P,S|=1<<u[i].second,ans%=P;
}
int main(){
scanf("%d%d",&n,&m);bas=(1<<m)-1;op[0]=~0;
rep(i,0,m-1)rep(j,1,n)scanf("%d",&a[i][j]);
scanf("%s",s+1);int len=strlen(s+1);
rep(i,1,len)
if(s[i]=='(')op[++top]=-1;
else if(s[i]=='<')op[++top]=1;
else if(s[i]=='>')op[++top]=2;
else if(s[i]=='?')op[++top]=3;
else if(s[i]==')')top--,check();
else val[sta[++tp]=++idx]=s[i]-'0',check();
rt=sta[tp];dfs(rt);rep(i,1,n)calc(i);
return printf("%d\n",(ans%P+P)%P),0;
}
有些题目需要发现隐藏的生成树。
这道题,我们记录每个骨牌推到后停下来的位置为 \(\rm next_i\),并连接\(i\to next_{i}\)的边,发现这就是一颗树。
所以我们直接用树上倍增即可解决。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 200005
using namespace std;
int n,m,p[N],l[N],fa[N][20],mx[N],f[N][20];
int main(){
scanf("%d",&n);
rep(i,1,n)scanf("%d%d",&p[i],&l[i]);
scanf("%d",&m);fa[n][0]=0;int t=log2(n);
pre(i,n-1,1){
int cur=i+1;mx[i]=max(mx[i],p[i]+l[i]);
while(cur&&p[cur]<=p[i]+l[i])mx[i]=max(mx[i],mx[cur]),cur=fa[cur][0];
fa[i][0]=cur;f[i][0]=p[cur]-mx[i];
}
rep(i,1,t)rep(x,1,n)f[x][i]=f[x][i-1]+f[fa[x][i-1]][i-1],fa[x][i]=fa[fa[x][i-1]][i-1];
while(m--){
int l,r;scanf("%d%d",&l,&r);
if(fa[l][0]>r)printf("0\n");
else {
int ans=0;
pre(i,t,0)if(fa[l][i]&&fa[l][i]<=r)ans+=f[l][i],l=fa[l][i];
printf("%d\n",ans);
}
}
return 0;
}