点分治学习笔记
点分治
点分治是在一棵树上对具有某些限定条件的路径静态地进行统计的算法。(抄自算阶)
例题:树上的询问
一棵 \(n\) 个点的带边权树,有 \(p\) 个询问,每次询问树中是否存在一条长度恰好为 \(k\) 的路径,如果是,输出
Yes
否则输出No
。
若指定树上节点 \(x\) 为根,则树上路径可以分为两种:
- 经过节点 \(x\) 的;
- 包含于 \(x\) 的某棵子树中的。
那么我们可以每次指定一个节点为根,然后统计符合条件1的路径数量,再向下递归,在每棵子树中重复以上过程。这就是点分治。这里每次取的节点都应该是当前子树的重心,整个算法的复杂度为 \(O(N \log^2 N)\)。
用递归的方式实现分治:
void div(int x)
{
vis[x]=true;//标记为已访问
solve(x);//计算经过x的合法路径数量
for(int i=lk[x];i;i=e[i].nxt)//找子节点
{
int y=e[i].y;
if(vis[y])continue;
root=0;sz=siz[y];//重置
get_rt(y,0);//找子树重心
div(root);//向下分治
}
}
计算经过某个节点的合法路径数的过程可以分成两步(也抄自算阶):
- 把树中每个节点放进数组,并把数组按照到根节点距离增加的顺序排序。
- 使用两个指针分别从前、后开始扫描数组。不难发现,指针
l
从左向右扫描的过程中,恰好使得dis[st[l]]+dis[st[r]]==k
的r
是从右向左单调递减的。如果发现了符合条件的l
、r
就标记为已找到,继续找下一个k
的答案。
void getdis(int x,int fa,int d,int rt);
void solve(int x)
{
top=0;//清空栈
st[++top]=x;dis[x]=0;bl[x]=x;//标记到x的距离,x所属子树
for(int i=lk[x];i;i=e[i].nxt)
{
int y=e[i].y;
if(vis[y])continue;
getdis(y,x,e[i].v,y);//递归标记其子节点的距离和所属子树
}
sort(st+1,st+1+top,cmp);//排序
for(int i=1;i<=m;i++)
{
int l=1,r=top;
if(ok[i])continue;
while(l<r)//找恰好满足条件的
{
if(dis[st[l]]+dis[st[r]]>q[i])r--;
else if(dis[st[l]]+dis[st[r]]<q[i])l++;
else if(bl[st[r]]==bl[st[l]])//若在同一棵子树中,一定不符合条件
{
if(dis[st[r]]==dis[st[r-1]])r--;
else l++;
}
else
{
ok[i]=true;//太好了是dis[st[l]]+dis[st[r]]==k我们有救了
break;
}
}
}
}
void getdis(int x,int fa,int d,int rt)
{
st[++top]=x;//元素进栈
dis[x]=d;bl[x]=rt;//标记距离和所属子树
for(int i=lk[x];i;i=e[i].nxt)
{
int y=e[i].y;
if(vis[y]||y==fa)continue;
getdis(y,x,d+e[i].v,rt);//递归
}
}
在一些题目里,类似于以上的统计方法需要容斥。
当然,还有一种方法可以不用容斥原理,每扫完一棵子树就记录一下当前栈顶,并把此子树中所有点的dis
用数组存起来,统计下一棵子树时只统计上一次记录的栈顶到当前栈顶之间的点的答案,重复以上过程。
在另一道题中的实现如下:
void solve(int x)
{
cnt=0;st[++cnt]=x;int tmp=cnt+1;//记录栈顶
dis[x]=0;num[x]=0;add(0,0);//数组中加入根节点
for(int i=lk[x];i;i=e[i].nxt)
{
if(vis[ty])continue;
get_dis(ty,x,e[i].v,1);
for(int j=tmp;j<=cnt;j++)
ans=min(ans,query(k-dis[st[j]])+num[st[j]]);//更新答案
for(int j=tmp;j<=cnt;j++)
add(dis[st[j]],num[st[j]]);//数组中加入答案
tmp=cnt+1;//记录栈顶
}
while(cnt)erase(dis[st[cnt--]]);//清空数组
}
题目
一棵 \(n\) 个节点的树,标记其中 \(m\) 个节点,求出经过至多 \(k\) 个标记点的最长路径。
设f[i]
表示经过至多i
个标记点的最长路径,用树状数组来维护。更新和统计答案的过程同上。
统计经过长度恰好为 \(k\) 的路径的最小边数。
由于需要长度恰好为 \(k\) 的路径,这题就不能用树状数组存答案,而是数组。
采药人的路径
有两种不同的边,统计路径的数量,要求路径上两种边的数量相等,而且路径上有至少一个到两端的路径上两种边数量也相等的点(休息点)。
两种颜色的边分别标记为 \(1\) 和 \(-1\) ,那么需要统计边权和为 \(0\) 的路径。不难发现,经过点 x
的路径有三种:
- 边权和不为 \(0\) 的路径;
- 边权和为 \(0\) ,但没有休息点的路径;
- 边权和为 \(0\) ,休息点不是
x
的路径; - 边权和为 \(0\) ,休息点是
x
的路径。
想办法统计以上路径的数量并且转移状态就好了。
[2018/7 D班集训]路径规划
在树上找出一条路径,使得该路径权的最小值乘边权和最大。
首先,显然路径上最小边权相同时,只需要记录当前最大的边权和。计算结果时,对于已经统计结果的节点,与新来的节点合并新路径时,如果新来的节点们有更小的 minn
,答案更新为max(ans,minn[st[j]]*(dis[st[j]]+query(minn[st[j]])))
,那么如果新节点并没有更小的最小边权呢?很简单,反着再跑一遍就行了。可能需要因此放弃链式前向星
易错
div(root);
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析