【筆記】生成樹
來自\(\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\)
新圖顯然是二分圖,如果成環,則左右各至少都要經過兩個節點,代表兩個點之間,同時在兩個顏色中出現兩次。
所以如果新圖成環,則原圖成彩虹環。我們要使得新圖沒有環,直接跑最小生成樹即可。
\(\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;
}