虚树
虚树,是对于一棵给定的节点数为 n的树 T,构造一棵新的树 T' 使得总结点数最小且包含指定的某几个节点和他们的LCA。
虚树是只包含关键点以及关键lca的点,而其他不影响虚树结构的点和边都相当于进行了路径压缩,整颗虚树的规模不会超过关键点数目的两倍.
虚树实际就是为了解决一类树形动态规划问题而诞生的
伪虚树:
Kamp
简化一下,假如K= N ,(设边权和sum)那么 ans=2*sum-最长链
首先,对于没有被标记的且不在两两标记点的路径上 的点,是没有用处的;
所以我们可以建立一棵虚树,虚树上保留的点是,标记点或标记点路径上的点;
建立方法是从一个标记点出发,遍历整棵树,如果一个点及其子树内有标记点,那么它就保留;——具体维护siz,dfs_siz实现
对于虚树上的点,遍历整棵树并回到原点的距离一定是整个虚树的边权和×2;
如果不用回到原点,它就可以停留在距离最远的叶子里,这个点一定是直径的端点,两次DFS就好;
对于虚树外的点,最优方案一定是先走最近的路径到虚树上,再加上虚树上的这个点的答案;
(具体实现我们维护一个top数组,维护这个点挂在虚树上的哪个点)
下放代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N=1000005;
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
int n,k;
int hd[N],nxt[N],to[N],w[N],tot;
void add(int x,int y,int z) {
to[++tot]=y;w[tot]=z;nxt[tot]=hd[x];hd[x]=tot;
}
int siz[N],st;
void dfs_siz(int x,int f) {
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(y==f) continue;
dfs_siz(y,x);
siz[x]+=siz[y];
}
}
int top[N];//挂在虚树上的哪个点
LL p,dia,sum;
LL dis[N];//dis不在虚树上的点i到虚树的最短距离
void dfs_top(int x,int f,LL d) {
if(top[x]==x) {
if(dia<d) dia=d,p=x;
}
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(y==f) continue;
if(siz[y]) {
sum+=w[i];
top[y]=y;
} else {
top[y]=top[x];
dis[y]=dis[x]+w[i];
}
dfs_top(y,x,d+w[i]);
}
}
LL len[N];//虚树上能停留的最远距离
void dfs(int x,int f,LL d) {
len[x]=max(len[x],d);
if(dia<d) dia=d,p=x;
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(y==f) continue;
if(siz[y])
dfs(y,x,d+w[i]);
}
}
int main(){
n=read();k=read();
if(n==1) {puts("0");return 0;}
int x,y,z;
for(int i=1;i<n;i++) {
x=read();y=read();z=read();
add(x,y,z);add(y,x,z);
}
for(int i=1,x;i<=k;i++) {
x=read();siz[x]++;st=x;
}
dfs_siz(st,0);
p=st;
dia=0;
top[st]=st;
dfs_top(st,0,0);
dia=0;
dfs(p,0,0);
dia=0;
dfs(p,0,0);
sum<<=1;
for(int i=1;i<=n;i++)
printf("%lld\n",sum+dis[i]-len[top[i]]);
return 0;
}
真正的虚树
Kamp加强
多次询问,每次给定K个人
∑k<= 500000
对树上的点集S建立虚树,点x在虚树上当且仅当x属于S或x是S中某两个点的LCA
注意到我们的定义,如果一个点的两个子树内都有关键点,那么这个点就在虚树上。 所以我们最起码可以通过一遍完整的DFS求解O(n) 。 如何O(|S|)求出虚树上的点?即,如何快速把两两的 LCA 加入虚树
由于 LCA 对应区间深度最小值,只需要按照 DFS 序(入栈序)排序,将相邻两个点的 LCA 加入即可
获得点集之后,我们还需要维护虚树的结构,也就是为每个点找到虚树上的父节点。
模拟DFS维护到根的链
对点集模拟 DFS ,按照 DFS 序遍历所有点,用一个栈维护递归栈,在加入一个点时,弹出所有不是它祖先的点
构建虚树见下题
Kingdom and its Cities
Description
给定一棵树,多组询问,每组询问给定 \(k\) 个点,你可以删掉不同于那 \(k\) 个点的 \(m\) 个点,使得这 \(k\) 个点两两不连通,要求最小化 \(m\),如果不可能输出 \(-1\)。询问之间独立。
数据范围 $$n\leq10^5,\sum k\leq10^5$$
Solution
一看到这种 $$\sum k\leq10^5$$ 的题很可能就是虚树了。
一般用sort, O(|S|log|S|)建立虚树即可,遇到卡常题,可以使用基数排序以实现 O(|S|)建树
inline void ins(int x) {
if(!tp) {st[tp=1]=x;return;}//栈空
int lca=LCA(st[tp],x);
while((tp>1)&&(dep[lca]<dep[st[tp-1]])) add(st[tp-1],st[tp]),--tp;
if(dep[lca]<dep[st[tp]]) add(lca,st[tp--]);//不断弹出dep>lca的,并连上边
if((!tp) || (st[tp]!=lca)) st[++tp]=lca;
st[++tp]=x;
}
...
sort(a+1,a+1+m,cmp);//遇到卡常的可写下面的基数排序
if(!a[1]) st[tp=1]=1;
for(int i=1;i<=m;i++)
insert(a[i]);
if(tp) while(tp--) add(st[tp-1],st[tp]);//最后一条链的关系
基数排序
void qs(int l,int r)//按照dfs序排序
{
int i=l,j=r,m=dfn[a[l+r>>1]];
while (i<=j)
{
while (dfn[a[i]]<m) ++i;
while (dfn[a[j]]>m) --j;
if (i<=j) swap(a[i++],a[j--]);
}
if (i<r) qs(i,r);
if (l<j) qs(l,j);
}
SDOI2011消耗战
给定一棵树,割断每一条边都有代价,每次询问会给定一些点,求用最少的代价使所有给定点都和1号节点不连通。
预处理出 代表从1到x路径上最小的边权。如果x是询问点,那么切断x及其子树上 询问点的最小代价 ;否则,最小代价 (其中v是x的儿子)。
预处理每个点到1号点的路径中w[]的最小值 minw[](在预处理lca的时候就可以搞出来)
如果x是询问点,那么切断x及其子树上询问点的最小代价 dp[x]=minw[x];否则,最小代价 $$dp[x]=min(minw[x],\sum dp[y])$$(其中y是x的儿子)。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N=500050;
const int M=2000050;
typedef long long LL;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
int n,m,q,a[N];
int hd[N],nxt[M],to[M],w[M],tot;
void add(int x,int y,int z) {
to[++tot]=y;w[tot]=z;nxt[tot]=hd[x];hd[x]=tot;
}
int dep[N],f[N][20],dfn[N],dfn_cnt;
LL minw[N];
void dfs(int x,int fa,int d) {
dep[x]=d;
f[x][0]=fa;
dfn[x]=++dfn_cnt;
for(int i=1;i<=18&&f[x][i-1];i++)
f[x][i]=f[f[x][i-1]][i-1];;
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(y==fa) continue;
minw[y]=min(minw[x],(LL)w[i]);
dfs(y,x,d+1);
}
}
int LCA(int x,int y) {
if(dep[x]<dep[y]) swap(x,y);
for(int i=18;i>=0;i--)
if(dep[f[x][i]]>=dep[y])
x=f[x][i];
if(x==y) return x;
for(int i=18;i>=0;i--)
if(f[x][i]!=f[y][i])
x=f[x][i],y=f[y][i];
return f[x][0];
}
bool cmp(int a,int b) {return dfn[a]<dfn[b];}
int st[N],tp,lca;
bool mark[N];
void add1(int x,int y) {
to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
void ins(int x) {
if(!tp) {st[tp=1]=x;return;}
lca=LCA(st[tp],x);
while((tp>1)&&(dep[lca]<dep[st[tp-1]]))
add1(st[tp-1],st[tp]),tp--;
if(dep[lca]<dep[st[tp]]) add1(lca,st[tp--]);
if((!tp)||(lca!=st[tp])) st[++tp]=lca;
st[++tp]=x;
}
LL dp(int x,int f) {
LL sum=0;
for(int i=hd[x];i;i=nxt[i])
if(to[i]!=f)
sum+=dp(to[i],x);
hd[x]=0;//记得清空
return mark[x]?minw[x]:min(sum,minw[x]);
}
int main() {
n=read();
for(int i=1,x,y,z;i<n;i++) {
x=read();y=read();z=read();
add(x,y,z),add(y,x,z);
}
minw[1]=1ll<<60;
dfs(1,0,1);
memset(hd,0,sizeof hd);
memset(nxt,0,sizeof nxt);
m=read();
while(m--){
tot=tp=0;
q=read();
for(int i=1;i<=q;i++)
a[i]=read();
sort(a+1,a+1+q,cmp);
for(int i=1;i<=q;i++)
ins(a[i]),mark[a[i]]=1;;
while(tp>1) add1(st[tp-1],st[tp]),--tp;
printf("%lld\n",dp(st[1],0));
for(int i=1;i<=q;i++)
mark[a[i]]=0,a[i]=0;
}
return 0;
}
寻宝游戏
相当于动态维护虚树
实际上就是动态维护相邻两个点的LCA
这道题需要求所有关键点形成的极小连通子树的边权和的两倍,其实就(a排序后)
set维护即可
这题也是动态维护lca——简直一毛一样异象石 —— blog
set讨论费劲。。。
咳咳实测树剖lca快于RMQ
#include <set>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N=100005;
const int M=200005;
typedef long long LL;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
int n,m;
int hd[N],nxt[M],to[M],w[M],tot;
inline void add(int x,int y,int z) {
to[++tot]=y;w[tot]=z;nxt[tot]=hd[x];hd[x]=tot;
}
LL dep[N],dis[N];//
int f[N][20],dfn[N],rev[N],dfn_cnt;
void dfs(int x,int fa,LL d) {
dep[x]=d;
f[x][0]=fa;
dfn[x]=++dfn_cnt;rev[dfn_cnt]=x;
for(int i=1;i<=18&&f[x][i-1];i++)
f[x][i]=f[f[x][i-1]][i-1];
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(y==fa) continue;
dis[y]=dis[x]+w[i];
dfs(y,x,d+1);
}
}
inline int LCA(int x,int y) {
if(dep[x]<dep[y]) swap(x,y);
for(int i=18;i>=0;i--)
if(dep[f[x][i]]>=dep[y])
x=f[x][i];
if(x==y) return x;
for(int i=18;i>=0;i--)
if(f[x][i]!=f[y][i])
x=f[x][i],y=f[y][i];
return f[x][0];
}
inline LL path(int x,int y) {
return dis[x]+dis[y]-2*dis[LCA(x,y)];
}
set<int>s;
bool vis[N];
LL ans;
int main() {
n=read();m=read();
for(int i=1,x,y,z;i<n;i++) {
x=read();y=read();z=read();
add(x,y,z),add(y,x,z);
}
dfs(1,0,1);
set<int>::iterator it;
for(int i=1,x;i<=m;i++) {
x=read();
if(!vis[x]) s.insert(dfn[x]);
int l=rev[(it= s.lower_bound(dfn[x]))==s.begin()?*--s.end():*--it];
int r=rev[(it= s.upper_bound(dfn[x]))==s.end()?*s.begin():*it];
if(vis[x]) s.erase(dfn[x]);
LL d=path(l,r)-path(l,x)-path(x,r);
ans+= vis[x]?d:(-d);
printf("%lld\n",ans);
vis[x]^=1;
}
return 0;
}
大工程
虚树上的边权 $$ dis=dep[y]-dep[x] $$
代价和:考虑贡献,每条边对答案的贡献为 $$ dis*siz[y]*(m-siz[y]) $$
最大/小值:
树形dp维护次长链,最长链、次短链,最短链
次长链、次短链必须都赋为 -inf,inf
对于标志点我们就让最长链和最短链都为0,任凭更新
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
using namespace std;
const int N=1000005;
const int M=2000005;
const int inf=0x3f3f3f3f;
int hd[M],nxt[M],to[M],tot;
inline void add(int x,int y,int z) {
to[++tot]=y;w[tot]=z;nxt[tot]=hd[x];hd[x]=tot;
}
int fa[N],siz[N],son[N],dep[N];
void dfs_son(int x,int f) {
siz[x]=1;fa[x]=f;dep[x]=dep[f]+1;
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(y==f) continue;
dfs_son(y,x);
siz[x]+=siz[y];
if(siz[y]>siz[son[x]]) son[x]=y;
}
}
int dfn[N],top[N],dfn_cnt;
void dfs_chain(int x,int tp) {
top[x]=tp;
dfn[x]=++dfn_cnt;
if(son[x]) dfs_chain(son[x],tp);
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(dfn[y]) continue;
dfs_chain(y,y);
}
}
int LCA(int x,int y) {
while(top[x]!=top[y]) {
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=fa[top[x]];
}
return dep[x]<dep[y]?x:y;
}
int n,m,q,a[N];
void qs(int l,int r) {
int i=l,j=r,m=dfn[a[(l+r)>>1]];
while(i<=j) {
while(dfn[a[i]]<m) i++;
while(dfn[a[j]]>m) j--;
if(i<=j) swap(a[i++],a[j--]);
}
if(i<r) qs(i,r);
if(l<j) qs(l,j);
}
inline void add(int x,int y) {
to[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;
}
int st[N],tp;
void ins(int x) {
if(!tp){st[tp=1]=x;return;}
int lca=LCA(st[tp],x);
while((tp>1)&&(dep[lca]<dep[st[tp-1]]))
add(st[tp-1],st[tp]),tp--;
if(dep[lca]<dep[st[tp]]) add(lca,st[tp--]);
if((!tp)||(lca!=st[tp])) st[++tp]=lca;
st[++tp]=x;
}
long long ans;
int mi,mx;
int mark[N];
int dp[N][4];//min-0长 1 次长,max-2长 3次长
void dfs(int x) {
siz[x]=mark[x];
dp[x][0]=(mark[x]^1)*(dp[x][1]=inf);
dp[x][2]=(mark[x]^1)*(dp[x][3]=-inf);
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
dfs(y);
siz[x]+=siz[y];
int dis=dep[y]-dep[x];
ans+=(long long)dis*siz[y]*(m-siz[y]);
if(dp[x][0]>dp[y][0]+dis) {
dp[x][1]=min(dp[x][1],dp[x][0]);
dp[x][0]=dp[y][0]+dis;
} else dp[x][1]=min(dp[x][1],dp[y][0]+dis);
if(dp[x][2]<dp[y][2]+dis) {
dp[x][3]=max(dp[x][3],dp[x][2]);
dp[x][2]=dp[y][2]+dis;
} else dp[x][3]=min(dp[x][3],dp[y][2]+dis);
}
mi=min(mi,dp[x][0]+dp[x][1]);
mx=max(mx,dp[x][2]+dp[x][3]);
hd[x]=mark[x]=0;
}
int main() {
n=read();
for(int i=1,x,y;i<n;i++) {
x=read();y=read();
add(x,y),add(y,x);
}
dfs_son(1,0);dfs_chain(1,1);
memset(hd,0,sizeof hd);
q=read();
while(q--) {
tot=0;
m=read();
for(int i=1;i<=m;i++)
a[i]=read();
qs(1,m);
if(a[1]!=1) st[tp=1]=1;
for(int i=1;i<=m;i++)
ins(a[i]),mark[a[i]]=1;
while(--tp) add(st[tp],st[tp+1]);
ans=0;mi=inf;mx=0;
dfs(1);
printf("%lld %d %d\n",ans,mi,mx);
}
return 0;
}
世界树
gugugu
http://lazycal.logdown.com/posts/202331-bzoj3572
[HNOI2014]世界树
留坑待填 \leftarrow←我去不敢想象在学虚树时摸掉的题被猫讲了
我们先两次DP求出虚树上每个点被哪个点覆盖。然后,我们对于虚树上的每条边 (p,x)(p,x)单独考虑:
如果它们的bel相同,相当于链上的所有点有同一个bel,我们在父亲处算贡献。
如果不同,我们可以找出bel的分界点,用倍增预处理链上除了这个儿子外的size之和,贡献答案。
调这道题调了半天,因为自己举的例子太水了,忽略了虚树上有新建的不能覆盖别人的节点。
void gfs(int x,int p)
{
int d=dep[x]-dep[p];
if(p&&(rad[p]+d<rad[x]||(rad[p]+d==rad[x]&&bel[p]<bel[x])))
rad[x]=rad[p]+d,bel[x]=bel[p];
for(solid v:sn[x])
{
gfs(v,x);
si[x]+=si[v];
}
if(!p) ans[bel[x]]+=n-si[x];
else if(bel[x]!=bel[p])
{
int mid=rad[x]+rad[p]+d,t=((mid&1)||bel[x]<bel[p])?jump(x,mid/2-rad[x]):jump(x,mid/2-1-rad[x]);
ans[bel[x]]+=sz[x]-si[x]+t;
if(sz[x]-si[x]+t<0) out(x,p,fa[x][0],sz[x],si[x],t,sz[x]-si[x]+t);
si[x]=sz[x]+t;
}
}