orzYCBphp
%自爲風月馬前卒巨佬% 用於優化一類樹形DP問題。 當狀態轉移只和樹中的某些關鍵點有關的時候,咱們把這些點和它們兩兩之間的LCA弄出來,以點的祖孫關係連成一棵新的樹,這就是虛樹。 容易證實,若是關鍵點數量爲$m$,則虛樹點數不超過$2m$。html
dfs原樹,對點進行dfn標號,並將關鍵點按dfn從小到大排序。 搞個棧,棧內的點知足:都在從棧頂的點到原樹的根的一條鏈上。 如今咱們準備加入一個點$x$ 直接加可能破壞一條鏈的性質,因而把棧頂的元素彈掉直到能夠加入爲止。求個LCA討論一波,具體參考代碼。 彈棧的時候就能夠連好虛樹邊了。c++
int p=0;//st[0]表明一個dfn爲0的0號空點,方便處理 sort(a+1,a+m+1,cmp);//按dfn排序 for(int i=1;i<=m;st[++p]=a[i++]){ int y=lca(a[i],st[p]); while(p&&dfn[st[p-1]]>=dfn[y]) add(st[p-1],st[p]),--p; if(y!=st[p])add(y,st[p]),st[p]=y;//注意判斷 } while(p>1)add(st[p-1],st[p]),--p;//st[1]應爲虛樹根
固然,可能有些題的虛樹在關鍵點之間也有限制?寫出來都不同。 好比洛谷P2495 [SDOI2011]消耗戰 有一個固定的$1$號點,再就是隻能保留沒有祖孫關係($1$號點除外)的關鍵點。寫法也有好幾處不同算法
int p=0;st[0]=1; sort(h+1,h+k+1,cmp); for(R i=1;i<=k;++i){ if(!p){st[++p]=h[i];continue;} R x=h[i],y=lca(x,st[p]); if(y==st[p])continue; while(p&&l[st[p-1]]>=l[y])add(st[p-1],st[p]),--p; if(y!=st[p])add(y,st[p]),st[p]=y; st[++p]=x; } while(p)add(st[p-1],st[p]),--p;
因此看來虛樹這個東西關鍵不在於背板子,而在於靈活運用。數組
每一個詢問建虛樹,兩遍dfs肯定每一個虛樹上的點被哪裏管理(第一遍從下往上更新,第二遍從上往下) 對於兩個虛樹點中間的部分,倍增找出臨界點,兩邊的size分開貢獻。 找臨界點是個極其噁心的討論就對了。 倍增代碼短常數大,表示基本沒有看到別的小於2.5k的代碼。。。優化
#include<bits/stdc++.h> #define R register int #define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin)) using namespace std; const int SZ=1<<19,N=3e5+9,M=2*N; char buf[SZ],*ie=buf+SZ,*ip=ie-1; inline int in(){ G;while(*ip<'-')G; R x=*ip&15;G; while(*ip>'-'){x*=10;x+=*ip&15;G;} return x; } int p,he[N],ne[M],to[M],l[N],sr[N],d[N],o[N],fa[N][20]; void dfs(R x,R f){ l[x]=++p;sr[x]=1;d[x]=d[f]+1;fa[x][0]=f; for(R&i=o[x];(fa[x][i+1]=fa[fa[x][i]][i]);++i); for(R i=he[x];i;i=ne[i]) if(to[i]!=f)dfs(to[i],x),sr[x]+=sr[to[i]]; } int lca(R x,R y){ if(d[x]<d[y])swap(x,y); for(R i=o[x];~i;--i) if(d[fa[x][i]]>=d[y])x=fa[x][i]; if(x==y)return x; for(R i=o[x];~i;--i) if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i]; return fa[x][0]; } namespace VT{ int h[N],a[N],st[N],he[N],ne[N],tp[N],mn[N],id[N],si[N],ans[N],ok[N]; inline bool cmp(R x,R y){ return l[x]<l[y]; } inline void add(R x,R y){ ne[y]=he[x];he[x]=tp[y]=y; for(R i=0,k=d[y]-d[x]-1;k;k>>=1,++i) if(k&1)tp[y]=fa[tp[y]][i]; } inline void chkmn(R x,R y){ R t=mn[y]+abs(d[y]-d[x]); if(mn[x]>t)mn[x]=t,id[x]=id[y]; else if(mn[x]==t&&h[id[x]]>h[id[y]])id[x]=id[y]; } void calc(R x,R y){ R z=y,p=d[x]-mn[x]+d[y]+mn[y]; if(p&1)p=(p+1)>>1; else p=(p>>1)+(h[id[x]]<h[id[y]]||mn[x]+d[x]==mn[y]+d[y]); for(R i=0,k=d[y]-p;k;k>>=1,++i) if(k&1)z=fa[z][i]; ans[id[y]]+=sr[z]-si[y]; ans[id[x]]+=sr[tp[y]]-sr[z]; he[y]=si[y]=0; } void dfsup(R x){ if(!ok[x])mn[x]=M; for(R y=he[x];y;y=ne[y]) dfsup(y),chkmn(x,y),si[x]+=sr[tp[y]]; } void dfsdn(R x){ for(R y=he[x];y;y=ne[y]) chkmn(y,x),dfsdn(y),calc(x,y); } void work(){ R m=in(),p=0; for(R i=1;i<=m;++i){ R x=h[i]=a[i]=in(); mn[x]=0,id[x]=i,ok[x]=1; } sort(a+1,a+m+1,cmp); for(R i=1;i<=m;st[++p]=a[i++]){ R y=lca(a[i],st[p]); while(p&&l[st[p-1]]>=l[y])add(st[p-1],st[p]),--p; if(y!=st[p])add(y,st[p]),st[p]=y; } while(p)add(st[p-1],st[p]),--p; dfsup(0);dfsdn(0);he[0]=0; for(R i=1;i<=m;++i)printf("%d ",ans[i]),ok[h[i]]=ans[i]=0;puts(""); } } int main(){ R n=in();to[he[0]=1]=1; for(R i=1,p=1;i<n;++i){ R x=in(),y=in(); ne[++p]=he[x];to[he[x]=p]=y; ne[++p]=he[y];to[he[y]=p]=x; } dfs(0,0); for(R q=in();q;--q)VT::work(); return 0; }
就是Tarjan算法用的那種結構,邊分紅樹邊和返祖邊。 放到仙人掌上就會有一個性質:返祖邊覆蓋的樹邊區間是沒有交錯重疊的。 那麼,咱們不用寫Tarjan也能夠很方便的知道那些點在一個環裏。 因而已經能夠解決一點點問題了。spa
求仙人掌最大獨立集 yyb說額外記一維表示環底下那個點的狀態 蒟蒻以爲,先把環上其它子樹都作完,放到環上,再單獨取環底下那個點的兩個狀態分別在環上跑DP,也是挺吼的。 暫時BZOJ rank1code
仙人掌上選若干個點知足兩兩之間最短路$>=3$,最大化點權和。和帶權最大獨立集很像的。 每一個點的狀態有三個:本身選,兒子選,本身和兒子都不選。轉移隨便yy就行了,細節有一些,但應該仍是不難。 環上DP應該要考慮最下面兩個點,依據環底部點對環頂部點的影響(也就是環底部點離最近已選點的距離)分紅三類。 然而這個題是個假題。。。 https://www.luogu.org/discuss/lists?forumname=P2478 https://www.lydsy.com/JudgeOnline/wttl/wttl.php?pid=1952 因此下面的代碼蒟蒻也不能保證正確性 (蒟蒻的寫法應該是能夠適用於仙人掌而不侷限於題面說的點至多在一個環上
)htm
#include<bits/stdc++.h> #define R register int #define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin)) using namespace std; const int SZ=1<<19,N=1e6+9,M=2*N,INF=-2e9; char buf[SZ],*ie=buf+SZ,*ip=ie-1; inline int in(){ G;while(*ip<'-')G; R x=*ip&15;G; while(*ip>'-'){x*=10;x+=*ip&15;G;} return x; } int he[N],ne[M],to[M],fa[N],d[N]; struct Dat{ int f0,f1,f2; inline void operator+=(const Dat&a){ f0=max(f0+max(a.f0,a.f1),f1+a.f2); f1+=max(a.f0,a.f1); f2+=a.f1; } }g[N]; void dp(R x,R f){ Dat now,lst,res=(Dat){0,0,0};R y=fa[x]; for(R op=0;op<3;++op){//x到已選點的最短路是op switch(op){ case 0:now=(Dat){g[y].f1+g[x].f2,INF,INF};break; case 1:now=(Dat){g[y].f0+g[x].f0,g[y].f1+g[x].f0,g[y].f2+g[x].f1};break; case 2:now=(Dat){g[y].f0+g[x].f1,g[y].f1+g[x].f1,INF}; } for(R y=fa[x];y!=f;y=fa[y]) lst=now,(now=g[fa[y]])+=lst; switch(op){ case 2:res.f2=max(res.f2,now.f2); case 1:res.f0=max(res.f0,now.f0);res.f1=max(res.f1,now.f1);break; case 0:res.f0=max(res.f0,now.f1); } } g[f]=res; } int dfs(R x,R f){ fa[x]=f;d[x]=d[f]+1; R top=0;//環頂端 for(R i=he[x];i;i=ne[i]){ if(to[i]==f)continue; if(d[to[i]]){ if(d[to[i]]>d[x])dp(to[i],x),top^=x; else top^=to[i]; } else top^=dfs(to[i],x); } if(!top)g[f]+=g[x]; return top; } int main(){ R n=in(),m=in(),ans=0; for(R i=1;i<=n;++i)g[i].f2=in(); for(R i=1,p=0;i<=m;++i){ R x=in(),y=in(); ne[++p]=he[x];to[he[x]=p]=y; ne[++p]=he[y];to[he[y]=p]=x; } for(R i=1;i<=n;++i) if(!d[i])dfs(i,0),ans+=max(g[i].f0,max(g[i].f1,g[i].f2)); printf("%d\n",ans); return 0; }
求仙人掌直徑。 不在環上的記一下最大值和次大值轉移便可。 對於環仍是單獨來一遍DP,破環爲鏈,貢獻答案的限制爲兩點距離不超過環長的一半,顯然單調隊列優化。
專門用來優化仙人掌上的一些問題。 圓方樹的點分爲圓點和方點,圓點與原來仙人掌中的點一一對應,方點與仙人掌的每一個環一一對應。 圓方樹的邊也有兩種:
能夠想象成,把仙人掌的全部環上的邊抹去,環中央建一個方點向四周放射狀連邊,就造成了圓方樹。
另參考YL的總結,圓方樹的另外一種寫法是,不在環上的邊中間也強行插入一個方點。 或者說,把不在環上的邊視爲兩條重邊造成的環。 這樣的圓方樹會有一些更好的性質,好比任意路徑上的圓點和方點相間。
仙人掌最短路,多組詢問,不帶修改。 由於環上兩點的最短路能夠直接算,因此建出圓方樹:
每一個詢問在圓方樹上求LCA:
#include<bits/stdc++.h> #define LL long long #define R register int #define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin)) using namespace std; const int SZ=1<<19,N=3e4+9; char buf[SZ],*ie=buf+SZ,*ip=ie-1; inline int in(){ G;while(*ip<'-')G; R x=*ip&15;G; while(*ip>'-'){x*=10;x+=*ip&15;G;} return x; } int n,he[N],ne[N],to[N],w[N],d[N],fa[N]; inline int calc(R x,R y,R len){//環上最短路 R r=abs(d[x]-d[y]); return min(r,len-r); } namespace RST{ int he[N],ne[N],to[N],w[N],d[N],l[N],o[N],fa[N][15]; inline void add(R x,R y,R z){ ne[y]=he[x];he[x]=y;fa[y][0]=x;w[y]=z; } void dfs(R x){ for(R&i=o[x];(fa[x][i+1]=fa[fa[x][i]][i]);++i); for(R y=he[x];y;y=ne[y]) d[y]=d[x]+1,w[y]+=w[x],dfs(y); } int qry(R x,R y){ if(d[x]<d[y])swap(x,y); R r=w[x]+w[y]; for(R i=o[x];~i;--i) if(d[fa[x][i]]>=d[y])x=fa[x][i]; if(x==y)return r-2*w[x]; for(R i=o[x];~i;--i) if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i]; return l[fa[x][0]]?r+calc(x,y,l[fa[x][0]])-w[x]-w[y]:r-2*w[fa[x][0]]; } } void build(R x,R f,R len){ RST::l[++n]=len; for(;x!=f;x=fa[x]) RST::add(n,x,calc(x,f,len)); RST::add(x,n,0); } int dfs(R x){ R top=0; for(R y,i=he[x];i;i=ne[i]){ if((y=to[i])==fa[x])continue; if(d[y]){ if(d[y]>d[x])build(y,x,d[y]-d[x]+w[i]),top^=x; else top^=y; } else fa[y]=x,d[y]=d[x]+w[i],top^=dfs(y); } if(!top&&x!=1)RST::add(fa[x],x,d[x]-d[fa[x]]); return top; } int main(){ n=in();R m=in(),q=in(); for(R p=0,i=1;i<=m;++i){ R x=in(),y=in();w[p+1]=w[p+2]=in(); ne[++p]=he[x];to[he[x]=p]=y; ne[++p]=he[y];to[he[y]=p]=x; } d[1]=RST::d[1]=1;//防止一些邊界狀況 dfs(1);RST::dfs(1); while(q--) printf("%d\n",RST::qry(in(),in())); return 0; }
將仙人掌的環對應通常圖的點雙,圓方樹也就變成了廣義圓方樹。 寫Tarjan求割點,把點雙裏的點壓進棧裏,一塊兒連邊後一塊兒彈出來。 核心構建代碼
void dfs(R x){ low[x]=dfn[x]=++df;st[++p]=x; for(R y,i=he[x];i;i=ne[i]) if(dfn[y=to[i]])cmn(low[x],dfn[y]); else{ dfs(y),cmn(low[x],low[y]); if(low[y]==dfn[x]){ RST::add(x,++RST::n);R z; do RST::add(RST::n,z=st[p--]);while(z!=y); } } }
洛谷P4320 道路相遇 (板子題)
遍歷整張圖,不重不漏地通過每一條邊的路徑。若是起點終點相同則稱做歐拉回路。 判斷歐拉路、歐拉回路是否存在的充要條件:
無向圖歐拉回路:全部點度數爲偶數 有向圖歐拉回路:全部點入度等於出度 無向圖歐拉路:至多兩點度數爲奇數 有向圖歐拉路:至多一點入度等於出度+1,一點入度等於出度-1,其它全部點入度等於出度
構造方法:dfs,每次訪問一條未訪問的邊並打上訪問標記,回溯時將邊加入答案數組,最後將數組倒序輸出。 UOJ117 歐拉回路
#include<bits/stdc++.h> #define LL long long #define R register int #define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin)) using namespace std; const int SZ=1<<19,N=1e5+9,M=4*N; char buf[SZ],*ie=buf+SZ,*ip=ie-1; inline int in(){ G;while(*ip<'-')G; R x=*ip&15;G; while(*ip>'-'){x*=10;x+=*ip&15;G;} return x; } int t,n,m,p,he[N],ne[M],to[M],d[N],ans[M]; bool vis[M]; void dfs1(R x){ for(R i=he[x];i;i=he[x]){ he[x]=ne[he[x]]; if(!vis[i]){ vis[i^1]=1; dfs1(to[i]),ans[++p]=(i>>1)*(i&1?-1:1); } } } void dfs2(R x){ for(R i=he[x];i;i=he[x]){ he[x]=ne[he[x]]; dfs2(to[i]),ans[++p]=i; } } int main(){ t=in(),n=in(),m=in(); for(R i=1,p=t&1;i<=m;++i){ R x=in(),y=in(); ne[++p]=he[x],to[he[x]=p]=y,++d[y]; if(t&1)ne[++p]=he[y],to[he[y]=p]=x,++d[x]; else --d[x]; } for(R i=1;i<=n;++i) if(t&1?d[i]&1:d[i])return puts("NO"),0; for(R i=1;i<=n;++i) if((t&1?dfs1:dfs2)(i),p)break; if(p<m)return puts("NO"),0; puts("YES"); for(R i=p;i;--i)printf("%d ",ans[i]); puts(""); return 0; }