倍增的模板:
void dfs(int x,int fa){//树的典型的DFS的传入参数。
dep[x]=dep[fa]+1;//记录深度
dp[x]=1;//记录子树大小,刚开始赋值为1.
f[x][0]=fa;//记录自己的直接父亲。
for(int i=1;i<=31;i++){//必须在这里就要进行x的祖先的更改,因为之后DFS到的其他人会要用到自己的一些祖先结点信息。
f[x][i]=f[f[x][i-1]][i-1];
}
for(auto v:tr[x]){
if(v==fa) continue;
dfs(v,x);
dp[x]+=dp[v];//更新信息。
}
}
用倍增求LCA的模板:
int lca(int a,int b){
if(dep[a]<dep[b]) swap(a,b);//让左边的点的深度更深。
if(dep[a]!=dep[b]){//如果深度不一样就要对左边的点进行跳跃操作。
for(int i=30;i>=0;i--){
if(dep[f[a][i]]>=dep[b]) a=f[a][i];//这里是等于的时候也要跳跃。
}
}
if(a==b) return a;//如果本身两个点里面一个就是另外一个的祖先,这个时候需要再这里就直接退出。
for(int i=30;i>=0;i--){//然后看能不能继续跳,如果能就跳。
//判断的条件是什么意思?两个是同事跳的,最后的母的是为了让a.b刚好是公共祖先的直接儿子。
//我的理解是这样子方便书写,而且处理起来也并没有神恶魔麻烦的。
if(f[a][i]!=f[b][i]){
a=f[a][i];
b=f[b][i];
}
}
return f[a][0];//返回我的直接父亲,对应了上文所说的跳跃之后的参数的实际意义。
}
规定一个点往上跳dis下的模板:
//背景:我要得到x 和y的公共祖先z的直接儿子中,哪一个是x的祖先。
//就是从x跳跃dep[x]-dep[z]-1步。
int px=x;
int dis=dep[x]-dep[z];
dis--;
for(int i=30;i>=0;i--){
if((dis>>i) &1) {//用2进制表示的时候如果这一位是1,就跳跃。可以写个数字理解一下。
px=f[px][i];
}
}
三种DFS序列
DFN序列:时间戳,只记录入栈的序列。
DFS序列:每个点只会入栈出栈一次
**欧拉序列:**记录不断地入栈出栈。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZFOp9uso-1680962517844)(C:\Users\93085\AppData\Roaming\Typora\typora-user-images\image-20230328150429580.png)]
上面,一般所说的DFS序列是DFN序列,只记录入栈的顺序,上面就是12 4 58 9 6 3 7
进出栈都记录的话,最重要的就是中间两个相同的数字之间都是子树。也就是欧拉序列。
B 树
题目:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-id03wuvK-1680962517846)(C:\Users\93085\AppData\Roaming\Typora\typora-user-images\image-20230328150648949.png)]
里面提到了X到Y的路径上所有的点的颜色都要与X与Y一样,关于路径的问题就容易想到DFS序列,也就是DFS序列,因为两个点之间的所有的路径也就是DFS序列里面中两个点对应编号的中间的所有的对应的原先的编号。
用dp[i] [j]表示前i个点里面用了j种颜色的时候总共的方案数,对于一个新的点,只有两种情况需要考虑,一种是使用了原先没有使用过的颜色:这个时候dp[i] [j]=dp[i-1] [j-1] *(k-j+1).
另外一种情况是用了之前使用过的颜色,1但是,为了保证两个相同颜色之间的所有经过的点的颜色都是一致的,我们只有一种选择:让这个点和他的父亲节点的颜色一样,因为从这个点到达其他的点的路径上,一定会经过父亲这个结点,所以我么可以直接让这个点和父亲节点的颜色一致,这个时候,dp[i] [j] =dp[i-1] [j] …
然后会发现,这个题目,其实并不需要我们真的求出来一个点的DFS序列,直接写代码就可以了。但是对于DFS序列的理解,还是很重要的。
代码:
int n,k;
cin>>n>>k;
for(int i=1;i<n;i++) cin>>x>>y;
dp[1][1]=k;
for(int i=2;i<=n;i++){
for(int j=1;j<=k;j++){
dp[i][j]=(dp[i-1][j]+dp[i-1][j-1]*(k-j+1))%mod;
}
}
int ans=0;
for(int i=1;i<=k;i++){
ans=(ans+dp[n][i])%mod;
}
cout<<ans<<endl;
D 花花和月月种树
题目:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I7S46Oxo-1680962517846)(C:\Users\93085\AppData\Roaming\Typora\typora-user-images\image-20230328233220967.png)]
总结:原先是0号节点,之后三种类型:
1 x 表示x 新加进去一个儿子,儿子的编号是现在最大的编号加1,也应该是现在是第几个1。
2 x val x的子树里面都加上val。
3 x 输出x这个节点的权值。(这个是最重要的,也就是最后只需要知道这个点的权值,这一个点就可以了)因为最后只需要输出一个值,所以我们完全可以用tr数组,作为lazy标记来使用,具体操作在里面看
还有一个很重要的点:
现在我们要不断地建立新的节点,怎么处理?线段树是不能新加点的,也不能把每个点都预留1e5个子节点,不然会爆掉。(当然这句话,如果明白了线段树 dfs序的话,你会觉得是一个废话。。因为好像都和具体哪个点没有关系,只不过是多了两个in out 数组,仅此而已)
采用离线处理的方法来解决这个问题,刚开始把所有的信息都读入下来,存储下来,加点的操作直接建边就可以。
在已经有了这个树的前提下,跑一遍DFS序,然后之后套模板就可以。子树都加val就是区间修改,问节点全职,就是单点查询,中间tr数组说白了就是一个懒惰标记的作用。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N=100005;
int n,m,in[N],out[N],tr[400105];
struct ty{
int type,x,num;
}op[400010];
vector<int>edge[N];
int a[N];
int num=0;
void dfs(int x){
in[x]=++num;
for(auto v:edge[x]){
dfs(v);
}
out[x]=num;//里面的dfs就搞完了。
}
void change(int k,int l,int r,int x){
if(l==r) {
tr[k]=0;
return;
}
int mid=(l+r)>>1;
if(tr[k]!=0){
tr[2*k]+=tr[k],tr[2*k+1]+=tr[k];
tr[k]=0;
}//用作懒惰标记。。。。。
if(x<=mid) change(2*k,l,mid,x);
if(x>mid) change(2*k+1,mid+1,r,x);
}
void add(int k,int l,int r,int x,int y,int val){
if(x<=l && r<=y) {
tr[k]+=val;
return ;
}
int mid=(l+r)>>1;
if(tr[k]!=0){
tr[2*k]+=tr[k],tr[2*k+1]+=tr[k];
tr[k]=0;
}//用作懒惰标记。。。。。
if(x<=mid) add(2*k,l,mid,x,min(mid,y),val);
if(y>mid) add(2*k+1,mid+1,r,max(mid+1,x),y,val);
}
int find(int k,int l,int r,int x){
if(l==r) return tr[k];
int mid=(l+r)>>1;
if(tr[k]!=0){
tr[2*k]+=tr[k],tr[2*k+1]+=tr[k];
tr[k]=0;
}
if(x<=mid) return find(2*k,l,mid,x);
else return find(2*k+1,mid+1,r,x);
}
int main()
{
cin.tie(0);
ios::sync_with_stdio(false);
int number=1;
cin>>m;
for(int i=1;i<=m;i++){
cin>>op[i].type;
if(op[i].type==1) {
cin>>op[i].x;
op[i].x++;
edge[op[i].x].push_back(++number);
op[i].x=number;
}
if(op[i].type==2){
cin>>op[i].x>>op[i].num;
op[i].x++;
}
if(op[i].type==3){
cin>>op[i].x;
op[i].x++;
}
}
dfs(1);
for(int i=1;i<=m;i++){
if(op[i].type==1){
change(1,1,number,in[op[i].x]);
}
if(op[i].type==2){
add(1,1,number,in[op[i].x],out[op[i].x],op[i].num);
}
if(op[i].type==3){
int sum=find(1,1,number,in[op[i].x]);
cout<<sum<<endl;
}
}
return 0;
}
用结构题存储信息,类型1,就建边,然后我们下一次是要把新的儿子改为0,所以我们把op[i].num更改为新加进去的节点编号,之后更改in[ op[i].num] 就可以实现操作
区间更改:add(1,1,number,in[op[i].x],out[op[i].x],op[i].num); 单点查询:sum=find(1,1,number,in[op[i].x]);
E. A and B and Lecture Rooms
题目:
给定一个树,给定很多个询问,每一个询问里面有两个点,问能够找到几个点,距离这两个点的距离相等?
思路:
树,边权为1,问距离,典型的需要LCA来帮忙处理距离问题的方法。
如果两个点的距离是奇数,一定无解。如果是偶数,就要分情况讨论:
如果两个本来是相同深度的,那么路径的中点一定是LCA,可以找到,此时整个图里面
可有如下解释:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AEF56Abu-1680962517847)(C:\Users\93085\AppData\Roaming\Typora\typora-user-images\image-20230330162259023.png)]
比如对于D E LCA可以找到是A,那么所以点中除了B C 的所有子树不可以,其他的都可以,可以画图理解一下。
深度不同:
比如G C:距离为4 可以根据距离找到中点,是B。
具体过程:根据距离,dis/2就是G要跳的步数,可以跳到D处,然后D的父亲就是我们需要的中点,为什么要跳到dis/2-1的位置?因为最后结果是dp[B]-dp[D].
DP[X]存储的是 子树的大小。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
vector<int>tr[N];
int n,f[N][60],dep[N],dp[N];
void dfs(int x,int fa){
dep[x]=dep[fa]+1;
dp[x]=1;
f[x][0]=fa;
for(int i=1;i<=30;i++){
f[x][i]=f[f[x][i-1]][i-1];
}
for(auto v:tr[x]){
if(v==fa) continue;
dfs(v,x);
dp[x]+=dp[v];
}
}
int lca(int a,int b){
if(dep[a]<dep[b]) swap(a,b);
if(dep[a]!=dep[b]){
for(int i=30;i>=0;i--){
if(dep[f[a][i]]>=dep[b]) a=f[a][i];
}
}
if(a==b) return a;
for(int i=30;i>=0;i--){
if(f[a][i]!=f[b][i]){
a=f[a][i];
b=f[b][i];
}
}
return f[a][0];
}
int solve(int x,int y){
int z=lca(x,y);
// cout<<z<<endl;
if(dep[x]==dep[y]){
int px=x;
int py=y;
int dis=dep[x]-dep[z];
dis--;
for(int i=30;i>=0;i--){
if((dis>>i) &1) {
px=f[px][i];
py=f[py][i];
}
}
return n-dp[px]-dp[py];
}
else{
int dis=dep[x]+dep[y]-2*dep[z];
if(dis&1) return 0;
dis/=2;
dis--;
int px=x;
for(int i=31;i>=0;i--){
if((dis>>i) &1) {
px=f[px][i];
}
}
int fa=f[px][0];
return dp[fa]-dp[px];
}
}
int main()
{
cin>>n;
for(int i=1;i<n;i++){
int x,y; cin>>x>>y;
tr[x].push_back(y);
tr[y].push_back(x);
}
dfs(1,0);
int q;
cin>>q;
while(q--){
int x,y; cin>>x>>y;
if(dep[x]<dep[y]) swap(x,y);
int sum=solve(x,y);
cout<<sum<<endl;
}
return 0;
}