模板题引入

先来看一道模板题,CF上的。
这题的意思是给定一棵有nnn个节点的树,树上有一些关键点(keykeykey)。接下来有qqq组询问,每次给出kik_iki​个keykeykey,要求删去一些点,使得这些keykeykey不相连。要求删去的最少的点数。

模板题解析

第一眼看到这题,先想到的肯定是树形dpdpdp。毕竟是在树上嘛。但是接下来看了一眼范围1≤n≤1000001\le n\le 1000001≤n≤100000,而且1≤q≤1000001\le q\le 1000001≤q≤100000。如果对于每个qqq都跑一次完整的树,那么显然,O(n∗q)O(n*q)O(n∗q)一算就会TLETLETLE。
那么我们可以先不考虑这个,先想怎么dpdpdp。很简单,运用贪心,分类。
如果当前节点是关键点,那么查询它是否有子儿子是关键点,如果有,那么显然连向这个子节点的链上需要被截断,否则就不用。
如果不是关键点,那么如果子树中有关键点并且多于222个,那么显然,把当前节点删掉会最优,如果只有一个,那么留到后面和其他子树中的关键点分割更优。
因此,设当前节点是xxx,它的儿子是sonsonson,定义dp[i]dp[i]dp[i]表示以iii为根的子树,使得所有的keykeykey都不连通的最小删点个数,siz[i]siz[i]siz[i]表示以iii为根的子树里keykeykey的个数。则有:
if(x是key)if(x是key)if(x是key)
dp[x]+=dp[son];\qquad dp[x]+=dp[son];dp[x]+=dp[son];
if(siz[son])ans[x]++;\qquad\qquad if(siz[son]) ans[x]++;if(siz[son])ans[x]++;
elseelseelse
ans[x]+=ans[son];\qquad ans[x]+=ans[son];ans[x]+=ans[son];
siz[x]+=siz[son];\qquad siz[x]+=siz[son];siz[x]+=siz[son];
\qquad在遍历完子节点后
if(siz[x]>1)\qquad if(siz[x]>1)if(siz[x]>1)
siz[x]=0;\qquad\qquad siz[x]=0;siz[x]=0;
ans[x]++;\qquad\qquad ans[x]++;ans[x]++;
无解很简单,只有当两个keykeykey相连的时候是无解的。

虚树

接下来我们考虑一开始提出的问题。
读题几遍后发现:

因此如果我们只考虑keykeykey,那么是十分快的,只有O(k)O(k)O(k)。所以我们随便建了一棵树,并写了几个keykeykey。

那么显然,结果只和keykeykey有关,因此我们可以将其压缩,只保留红色节点即可。
但是马上发现,只留下蓝色和红色的,会发现不够啊,如果只有红色的节点,无法使得节点之间的父子关系显示出来,无法很好地dpdpdp。因此下面这个绿色的点也需要保留,以表示绿色下面的是其儿子,从而传递。

多写几棵树,就可以发现,其实只要把keykeykey和几个keykeykey的LCALCALCA保留,就可以维护树的形状,又能简化树,使得dpdpdp的复杂度大大降低。
此时就变成了删边使得每个keykeykey都不相连。(判断无解只要在简化之前做就可以了,这样每个keykeykey之间都会有点可以删,就可以变成删边)
像这样建出来的只保留keykeykey的树,称之为虚树
虚树通常是树形dpdpdp时常用的优化操作。上面这棵树简化后是这样的:

看,是不是简单了很多呢?

建虚树

首先需要建出实树,然后算出其dfsdfsdfs序的编号,然后求LCALCALCA,将LCALCALCA也加入到虚树里即可。那么LCALCALCA选用倍增的方式求,要求出每个节点的deepdeepdeep。当然也可以不用倍增,只要能求出来就好。

建实树,fafafa是倍增用的。(别问怎么又是这个QAQQAQQAQ)

void QAQ(int x,int f,int depth)
{ldfn[x]=++cnt;//顺搜的dfndep[x]=depth;//深度fa[x][0]=f;//x节点向上跳2^0步就是它的父节点for(int i=1;i<20;i++)fa[x][i]=fa[fa[x][i-1]][i-1];for(int i=0;i<rea[x].size();i++)//遍历实树if(rea[x][i]!=f)QAQ(rea[x][i],x,depth+1);rdfn[x]=cnt;//回溯的dfn,后文会讲为什么要存回溯的
}

求LCALCALCA,倍增的板子。

int LCA(int x,int y)
{if(dep[x]<dep[y]) swap(x,y);int delta=dep[x]-dep[y];for(int i=0;i<20&&delta;i++)if(delta&(1<<i))x=fa[x][i];if(x==y) return x;for(int i=19;i>=0;i--)if(fa[x][i]!=fa[y][i]){x=fa[x][i];y=fa[y][i];}return fa[x][0];
}//?不会有人还不知道倍增吧,不会吧不会吧。

接下来就是比较困难的建虚树。首先我们要理解其原理。

先把元素按ldfnldfnldfn排序。维护一个树链,对于一个待插入的元素,在当前的树链上找,找到第一个这个元素的祖先,就把这个元素插入到这个祖先的子树里去即可。
原理非常简单,但是怎么实现呢?首先观察一个元素的祖先唯一满足什么条件。不妨对于上面的树,我们算一下ldfnldfnldfn和rdfnrdfnrdfn,以数对(ldfn,rdfn)(ldfn,rdfn)(ldfn,rdfn)的形式列出来,然后找规律。

注意,回溯的时候编号不加一。
我们可以发现,对于一条树链,一个节点aaa如果是节点bbb的祖先,那么必定是rdfn[a]≥ldfn[b]rdfn[a]\ge ldfn[b]rdfn[a]≥ldfn[b],反之也成立。原因很简单,因为枚举到当前节点的时候,它的祖先节点的rdfnrdfnrdfn还没有算,要等到这棵子树遍历完后再赋值,那么肯定是比当前节点大的。1

接下来就是怎么维护树链了。只要维护一个从底到顶深度不断增加的单调栈即可。每次将当前元素与栈顶元素比较,如果不满足栈顶元素是当前元素的祖先,那么弹出当前栈顶元素直到满足为止,然后将当前元素压入栈,再维护一条新的链。由于元素已经排序,所以肯定是从左到右建立所有的链。

接下来考虑一个问题,LCALCALCA如果每对元素都求的话,复杂度还是很高。因此我们要考虑简化。探索后发现,只要求相邻两个元素的LCALCALCA,由于是按ldfnldfnldfn排序了的元素,这样的LCALCALCA肯定覆盖了所有需要的节点,因此只要扫一遍然后怼出LCALCALCA即可。

接下来手动模拟一次,以上面的树为例:



接下来看弹出。

看,是不是一样建出来了!看官可以自行造几组样例,按上面的表述去建一建虚树。

接下来给出代码:

bool cmp(int x,int y)
{return ldfn[x]<ldfn[y];
}//按dfn排序
void build()
{sort(d+1,d+1+m,cmp);int keynum=m;for(int i=1;i<keynum;i++)d[++m]=LCA(d[i],d[i+1]);//求相邻两个的LCAsort(d+1,d+1+m,cmp);m=unique(d+1,d+1+m)-d-1;//去重,unique对一个已经排序的数组可以实现去重,//多出来的扔到了数组的后面,并返回数组大小的地址int top=0;stk[++top]=d[1];for(int i=2;i<=m;i++){while(top&&rdfn[stk[top]]<ldfn[d[i]])top--;//如果不是祖先,则弹出元素if(top) vir[stk[top]].push_back(d[i]);//加入到其祖先的子树里stk[++top]=d[i];//维护新的链}
}

这样,我们就可以很好地解决复杂度过大的问题了。
给出模板题的标程:带注释

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+100;
int cnt,m;
int ldfn[MAXN],rdfn[MAXN],fa[MAXN][20],dep[MAXN],stk[MAXN],d[MAXN],vis[MAXN];
vector<int> rea[MAXN],vir[MAXN];
int ans[MAXN],siz[MAXN];
void QAQ(int x,int f,int depth)
{ldfn[x]=++cnt;dep[x]=depth;fa[x][0]=f;for(int i=1;i<20;i++)fa[x][i]=fa[fa[x][i-1]][i-1];for(int i=0;i<rea[x].size();i++)if(rea[x][i]!=f)QAQ(rea[x][i],x,depth+1);rdfn[x]=cnt;
}
int LCA(int x,int y)
{if(dep[x]<dep[y]) swap(x,y);int delta=dep[x]-dep[y];for(int i=0;i<20&&delta;i++)if(delta&(1<<i))x=fa[x][i];if(x==y) return x;for(int i=19;i>=0;i--)if(fa[x][i]!=fa[y][i]){x=fa[x][i];y=fa[y][i];}return fa[x][0];
}
bool cmp(int x,int y)
{return ldfn[x]<ldfn[y];
}
void build()
{sort(d+1,d+1+m,cmp);int keynum=m;for(int i=1;i<keynum;i++)d[++m]=LCA(d[i],d[i+1]);sort(d+1,d+1+m,cmp);m=unique(d+1,d+1+m)-d-1;int top=0;stk[++top]=d[1];for(int i=2;i<=m;i++){while(top&&rdfn[stk[top]]<ldfn[d[i]])top--;if(top) vir[stk[top]].push_back(d[i]);stk[++top]=d[i];}
}
void dfs(int x)
{ans[x]=0;if(vis[x]){siz[x]=1;for(int i=0;i<vir[x].size();i++){int son=vir[x][i];dfs(son);ans[x]+=ans[son];if(siz[son]) ans[x]++;}}else{siz[x]=0;for(int i=0;i<vir[x].size();i++){int son=vir[x][i];dfs(son);ans[x]+=ans[son];siz[x]+=siz[son];}if(siz[x]>1){siz[x]=0;ans[x]++;}}
}
int main()
{int n,q,u,v;while(~scanf("%d",&n)){for(int i=0;i<=n;i++)rea[i].clear();for(int i=1;i<n;i++){scanf("%d%d",&u,&v);rea[u].push_back(v);rea[v].push_back(u);}cnt=0;QAQ(1,0,0);scanf("%d",&q);while(q--){scanf("%d",&m);for(int i=1;i<=m;i++){scanf("%d",&d[i]);vis[d[i]]=1;}int fl=1;for(int i=1;i<=m;i++)if(vis[fa[d[i]][0]]){fl=0;break;}if(!fl) puts("-1");else{build();dfs(d[1]);printf("%d\n",ans[d[1]]);}for(int i=1;i<=m;i++)vis[d[i]]=0,vir[d[i]].clear();}}
}

例题一枚

非常经典的,虚树必刷的消耗战,洛谷上的链接。
其实就是上面题的简化版,要求所有的keykeykey都不与1连通。同样的先摆好dpdpdp,再想建虚树。直接看代码注释吧。

#include<bits/stdc++.h>
#define ll long long
#define inf 1ll<<60
using namespace std;
const int MAXN=250010;
struct node{ll to,w;node(){}node(ll _to,ll _w){to=_to,w=_w;}
};//注意实树里要存边权值
ll m,cnt,ldfn[MAXN],rdfn[MAXN],dep[MAXN],fa[MAXN][20],d[MAXN<<1];
ll stk[MAXN],dp[MAXN],w[MAXN];
//dp[i]表示i的子树割断的最小花费
bool vis[MAXN];
vector<node> rea[MAXN];
vector<ll> vir[MAXN];
void QAQ(ll x,ll f,ll depth)
{ldfn[x]=++cnt;dep[x]=depth;fa[x][0]=f;for(ll i=1;i<20;i++)fa[x][i]=fa[fa[x][i-1]][i-1];for(ll i=0;i<rea[x].size();i++)if(rea[x][i].to!=f){w[rea[x][i].to]=min(w[x],rea[x][i].w);//由于只要隔断1,因此维护每个节点到1的最短边割断即可QAQ(rea[x][i].to,x,depth+1);}rdfn[x]=cnt;
}
ll LCA(ll x,ll y)
{if(dep[x]<dep[y]) swap(x,y);ll delta=dep[x]-dep[y];for(ll i=0;i<20&&delta;i++)if(delta&(1<<i))x=fa[x][i];if(x==y) return x;for(ll i=19;i>=0;i--)if(fa[x][i]!=fa[y][i]){x=fa[x][i];y=fa[y][i];}return fa[x][0];
}
bool cmp(ll x,ll y){return ldfn[x]<ldfn[y];}
void build()
{sort(d+1,d+1+m,cmp);ll keynum=m;for(ll i=1;i<keynum;i++)d[++m]=LCA(d[i],d[i+1]);d[++m]=1;sort(d+1,d+1+m,cmp);m=unique(d+1,d+1+m)-d-1;ll top=0;stk[++top]=d[1];for(ll i=2;i<=m;i++){while(top&&rdfn[stk[top]]<ldfn[d[i]])top--;if(top) vir[d[i]].push_back(stk[top]),vir[stk[top]].push_back(d[i]);stk[++top]=d[i];}
}//板子
void dfs(ll x,ll fa)
{if(vir[x].size()<=0&&vis[x]){dp[x]=inf;//叶节点并且不是key,则不用考虑,因此为infreturn;}for(ll i=0;i<vir[x].size();i++){ll son=vir[x][i];if(son==fa) continue;dfs(son,x);if(vis[son]) dp[x]+=w[son];//如果儿子是key,那么显然要把这个儿子割断else dp[x]+=min(w[son],dp[son]);//否则可以考虑割断儿子的其他子树会更小}
}
int main()
{ll n,u,v,t,q;scanf("%lld",&n);for(ll i=1;i<n;i++){scanf("%lld%lld%lld",&u,&v,&t);rea[u].push_back(node(v,t));rea[v].push_back(node(u,t));}cnt=0;w[1]=inf;QAQ(1,0,0);
//  for(int i=1;i<=n;i++) cerr<<w[i]<<' ';scanf("%lld",&q);while(q--){scanf("%lld",&m);for(ll i=1;i<=m;i++) scanf("%lld",&d[i]),vis[d[i]]=1;build();dfs(d[1],-1);printf("%lld\n",dp[1]);for(ll i=1;i<=m;i++) dp[d[i]]=0,vis[d[i]]=0,vir[d[i]].clear();//精准扶贫,防TLE}
}

其实消耗战才是模板题,只是因为它有关权值,所以把它放到应用。

总结

虚树和矩阵快速幂一样,是优化型的算法。使用虚树的条件:
1、有关键点,且关键点的数量少。
2、有必要。显然,如果裸的dpdpdp可以跑,为什么要敲个虚树的板子呢?
以此记录虚树的学习。


  1. 这里可以回答上面注释里的问题了,由于只维护ldfnldfnldfn会产生混淆,比如上面的(8,8)就会放到(7,7)的下面而不是(6,8)的儿子。 ↩︎

虚树(Kindom and its Cities,消耗战)相关推荐

  1. 【BZOJ2286】消耗战(虚树,动态规划)

    [BZOJ2286]消耗战(虚树,动态规划) 题面 BZOJ Description 在一场战争中,战场由n个岛屿和n-1个桥梁组成,保证每两个岛屿间有且仅有一条路径可达.现在,我军已经侦查到敌军的总 ...

  2. BZOJ2286: [Sdoi2011]消耗战(虚树)

    BZOJ2286: [Sdoi2011]消耗战 Time Limit: 20 Sec Memory Limit: 512 MB Description 在一场战争中,战场由n个岛屿和n-1个桥梁组成, ...

  3. 虚树+树型DP SDOI2011消耗战

    <虚树+树型DP> SDOI2011消耗战 #include <iostream> #include <cstdio> #include <cstring&g ...

  4. 洛谷_2495 [SDOI2011]消耗战(虚树)

    消耗战 题目链接:https://www.luogu.com.cn/problem/P2495 题解: 对于单样例,可以考虑树形DP. 但此题是多实例,所以需要对树进行处理,每次询问有k+1(加上一号 ...

  5. P2495 [SDOI2011]消耗战(树形dp+虚树)

    P2495 [SDOI2011]消耗战 树形dp 状态表示:fuf_ufu​表示以uuu为根的子树中,uuu节点与子树中的关键的"隔开"所需要的最小代价 状态转移: 考虑uuu的一 ...

  6. 洛谷P2495 [SDOI2011]消耗战(虚树dp)

    P2495 [SDOI2011]消耗战 题目链接 题解: 虚树\(dp\)入门题吧.虚树的核心思想其实就是每次只保留关键点,因为关键点的dfs序的相对大小顺序和原来的树中结点dfs序的相对大小顺序都是 ...

  7. 「Luogu2495」 [SDOI2011]消耗战 虚树

    Luogu P2495 [SDOI2011]消耗战 problem Solution 苦思冥想稍作思考之后可以得到一个树形DP的方法: 令\(w(u,v)\)表示u,v之间的边的权值,\(f[u]\) ...

  8. 算法复习——虚树(消耗战bzoj2286)

    题目: Description 在一场战争中,战场由n个岛屿和n-1个桥梁组成,保证每两个岛屿间有且仅有一条路径可达.现在,我军已经侦查到敌军的总部在编号为1的岛屿,而且他们已经没有足够多的能源维系战 ...

  9. CodeForces - 613D Kingdom and its Cities(虚树+贪心)

    题目链接:点击查看 题目大意:给出一棵 n 个结点组成的树,有多组询问,每组询问给出 k 个点,现在可以删除不同于 k 个节点的 m 个节点,使得这 k 个节点两两不连通,要求最小化 m ,如果不可能 ...

最新文章

  1. Linux系统管理必备知识之查看系统用户和用户组
  2. 屈才了!当程序员会写诗后...... | 每日趣闻
  3. max分发-(网络系统克隆)
  4. backup1:开始数据库备份
  5. php学到什么程度可以学thinkphp,thinkphp学习一
  6. ENVI4.8下载与安装
  7. 灰鸽子***猛增五百多变种 网民生活受***监控
  8. 1024献礼,全栈工程师进击
  9. 亚马逊云科技发布“云拓计划”赋能企业加速上云及数字化转型
  10. ARM芯片内部堆栈的理解及MAP文件的查看
  11. IDEA设置一键导包
  12. OOM和JVM最详细介绍
  13. Win7 Win2003 双系统重装2003以后修复Windows 7 系统启动项
  14. docker+阿里云镜像服务
  15. 360桌面助手待办事项同步/迁移的方法(从一台电脑迁移到另外一台电脑上)
  16. C#软件license管理(简单软件注册机制)
  17. 英文中的一些常见缩写
  18. Java carr_Java抽象类和接口(三)
  19. python自动安装windows软件_python模块 - pywinauto(windows自动化安装软件)
  20. Python + Selenium + Chrome Driver 自动化点击+评论+刷弹幕(仅供学习)

热门文章

  1. 你踩过社群运营的坑吗?
  2. [ZCMU OJ]1750: 统计单词数(字符串处理/find运用)
  3. cloud探索 - ASW基础知识
  4. HTML常用符号大全
  5. SPC-1性能总榜第一,中国厂商霸榜背后是存储新势力崛起
  6. 终于完成coos的重构了
  7. 什么是鉴权?这些postman鉴权方式你又知道多少?
  8. 变身小小科学家 南瓜科学让孩子爱上实验
  9. 企业微信开发--网页授权(获取code)
  10. C++“准”标准库Boost学习指南(1):智能指针Boost.smart_ptr