点分治&点分树复习
点分治
点分治是处理树上路径问题的一种通用的方式,是树分治的一种。
从最一般的问题来入手,有这样一个经典的问题:
例题1
给定一棵 (n) 个节点的树,每条边有边权,求出树上两点距离小于等于 (k) 的点对数量。
如果我们直接求所有点对,那么复杂度是(O(n^2)) 的,对于这道题 (nle4 imes10^4)来说,是无法接受的。
而这道题的问题只是求距离的数量,和具体的点没关系,也就是说,我们只需要知道距离的集合即可。
想到一个点一个点的考虑,确定一个点,然后枚举所有的子树,求出所有到这个点的距离的集合,在这些距离中,已经有满足(disle k) 的,且还有可以组合起来仍然(le k) 的,先不管它们怎么组合,我们先考虑通过这样的方法我们做到了什么,这样的操作相当于通过这个点能够找到的距离已经全部被处理过了,且经过这个点的两个不同子树内的点也被考虑过了,所以我们可以只单独的考虑每个子树内的内容,而不用再考虑和这个点有关的路径,然后我们可以递归处理每个子树内的情况,当然这样的操作还不能降低复杂度,因为递归下去可能遇到的还是一颗大小近似(n)的子树,那怎么办?遇到问题就解决问题,既然可能遇到一颗大小近似(n)的子树,那就提前让确定的这个点的子树都不超过(n/2) 即我们找到树的中心,然后如果我们能够(O(n)) 解决我们组合一个点所搜索到的(dis) ,那么我们就可以(T(n)=T(n/2)+n) 即复杂度(O(nlogn))的得到最终的结果;即使是(O(nlogn))的解决每次的面临的问题,最终的复杂度也只是(O(nlog^2n))。
这就是点分治,可以概括为三个步骤:
1 找到树的重心,统计有关重心的路径信息。
2 统计所有路径信息,不同子树间进行归并后计算,直接和重心相连的路径直接计算。
3 删除重心,递归到子树做同样的事情。
听起来是不难的,确实,分治的思想是不困难的,对于点分治来说困难的是:中间归并的操作。
这道模板题就需要点小小的技巧,首先是对于和重心相连的点,(disle k) 的直接计入答案,然后将所有子树的dis计入到一个数组中,将数组排序之后,考虑使用双指针,一个在头,一个在尾,二者相加如果(le k) 的话,那么所有的左指针前的点和右指针这个点的dis之和都是(le k) 的,然后左指针右移;反之,如果二者相加大于k,那么右指针应该向左移动;正确性:我们是从边界开始考虑的,所以说一开始的(l)如果不能满足(r) ,那么(r) 只能左移,这个时候(l) 如果满足了新的(r),那么也一定能够满足向左移动的(r) ,然后我们可以尝试让(l) 向右移动看看能不能满足更多的(l) ,就这样,这样是满足一个单调性的,而两个指针如果相遇了,那么只要(r) 一直向左推就好。还有一个问题就是,这个过程中可能出现同一颗子树到重心的dis,它们是不能被组合的,所以我们在搜索每一颗子树的dis的时候,就先对着这颗子树内的dis进行一波(le k)的组合的统计,然后直接让ans减去这些答案即可,收工!。
具体代码如下
点击此处展开和收起代码
#include<bits/stdc++.h>
#define reg register
typedef long long ll;
using namespace std;
inline int qr(){
int x=0,f=0;char ch=0;
while(!isdigit(ch)){f|=ch=='-';ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return f?-x:x;
}
const int maxn=2e4+100;
int n,m;
struct node{
int next,to,cost;
}edge[maxn];
int head[maxn],_tot;
bool o[maxn];
int p[maxn],q[maxn];//p存当前所有子树内的点到重心的距离,q数组存当前子树内所有点到重心的距离
void add(int u,int v,int l){edge[_tot].next=head[u];edge[_tot].to=v;edge[_tot].cost=l;;head[u]=_tot++;}
int get_size(int x,int fa){//求得子树大小
if(o[x]) return 0;
int res=1;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y==fa) continue;
res+=get_size(y,x);
}
return res;
}
int get_wc(int x,int fa,int tot,int &wc){//求树的伪重心 tot表示当前节点的子树大小
if(o[x]) return 0;
int sum=1,maxx=0;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y==fa) continue;
int t=get_wc(y,x,tot,wc);
maxx=max(maxx,t);
sum+=t;
}
maxx=max(maxx,tot-sum);
if(maxx<=tot/2) wc=x;
return sum;//返回子树的大小
}
void get_dis(int x,int fa,int dis,int &qt){//求到重心的距离
if(o[x]) return ;
q[qt++]=dis;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y==fa) continue;
get_dis(y,x,dis+edge[i].cost,qt);
}
}
int get(int a[],int k){//用双指针算法求一个序列中两数相加小于等于k的数的数量
sort(a,a+k);
int res=0;
for(reg int i=k-1,j=-1;i>=0;i--){
while(j+1<i&&a[j+1]+a[i]<=m) j++;
j=min(j,i-1);
res+=j+1;
}
return res;
}
int calc(int x){
if(o[x]) return 0;
int res=0;
get_wc(x,-1,get_size(x,-1),x);
o[x]=1;//删除重心
int len=0;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to,qt=0;
get_dis(y,-1,edge[i].cost,qt);
res-=get(q,qt);
for(reg int k=0;k<qt;k++){
if(q[k]<=m) res++;
p[len++]=q[k];
}
}
res+=get(p,len);
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
res+=calc(y);
}
return res;
}
int main(){
// freopen("poj1741_tree.in","r",stdin);
// freopen("poj1741_tree.out","w",stdout);
n=qr();
memset(head,-1,sizeof(head));
memset(o,0,sizeof(o));
_tot=0;
for(reg int i=1;i<n;i++){
int u=qr()-1,v=qr()-1,l=qr();
add(u,v,l);add(v,u,l);
}
m=qr();
printf("%d
",calc(0));
return 0;
}
/*
点分治 对树上的点进行分治,递归归并求解以达到nlog^2n 的复杂度
针对一棵树先找到树的伪重心(任意子树大小小于1/2即可),然后针对每一棵子树递归
继续求解,然后对子树内容进行归并求解
*/
例题2
再介绍一道 :Race/权值
给定一颗大小为 (N) 的树,求权值为 (k) 的经过边数最小的路径,不存在则输出-1,(Nle2 imes10^{5},Kle10^6)。
考虑DP,(f[i]) 表示权值为 (i) 的路径最短边数是多少,然后我们按照类似的过程:
枚举所有方案 ,首先 找到树的重心,然后开始分治
1 两个点都在某个子树内,递归下去求解
2 有一个点是重心,求每个点到重心的距离即可
3 两个点分别在不同的子树内,开一个桶,i存到重心的距离是i的所有点中边数最小的点
所以枚举一个子树内有dis==x,考虑其它子树的桶内k-x的点即可
和上一道题一模一样,基本都是考虑这三种情况 。
点击此处展开和收起代码
#include<bits/stdc++.h>
#define reg register
#define X first
#define y second
typedef long long ll;
using namespace std;
inline int qr(){
int x=0,f=0;char ch=0;
while(!isdigit(ch)){f|=ch=='-';ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return f?-x:x;
}
const int maxn=2e5+100;
const int M=1e6+100;
const int inf=0x3f3f3f3f;
int n,m;
struct node{
int next,to,cost;
}edge[maxn<<1];
int head[maxn],_tot;
void add(int u,int v,int l){edge[_tot].next=head[u];edge[_tot].to=v;edge[_tot].cost=l;head[u]=_tot++;}
pair<int,int>q[maxn],p[maxn];
int f[M];
bool o[maxn];
int ans;
int get_size(int x,int fa){
if(o[x]) return 0;
int res=1;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y==fa) continue;
res+=get_size(y,x);
}
return res;
}
int get_wc(int x,int fa,int tot,int &wc){
if(o[x]) return 0;
int sum=1,maxx=0;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y==fa ) continue;
int t=get_wc(y,x,tot,wc);
maxx=max(maxx,t);
sum+=t;
}
maxx=max(maxx,tot-sum);//父亲也得算
if(maxx<=tot/2) wc=x;
return sum;
}
void get_dis(int x,int fa,int dis,int cnt,int &qt){//cnt表示经过的边的数量
if(o[x]||dis>m) return ;//小剪枝
q[qt++]=make_pair(dis,cnt);
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y==fa) continue;
get_dis(y,x,dis+edge[i].cost,cnt+1,qt);
}
}
void calc(int x){
if(o[x])return;
get_wc(x,-1,get_size(x,-1),x);
o[x]=1;
int len=0;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to,qt=0;
get_dis(y,-1,edge[i].cost,1,qt);
for(reg int k=0;k<qt;k++){//归并
pair<int,int> t =q[k];
if(t.X==m) ans=min(ans,t.y);//对于直接等于m的点,比较ans
ans=min(ans,f[m-t.X]+t.y);//否则去桶里找
p[len++]=t;//统计一下整个联通块的合法点
}
for(reg int k=0;k<qt;k++){
pair<int,int >t =q[k];
f[t.X]=min(f[t.X],t.y);
}//把自己放在桶里
}
for(reg int i=0;i<len;i++){//清空
f[p[i].X]=inf;
}
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
calc(y);
}
}
int main(){
// freopen("test.in","r",stdin);
// freopen("test.out","w",stdout);
n=qr();m=qr();
memset(f,0x3f,sizeof(f));
ans=inf;
memset(head,-1,sizeof(head));
for(reg int i=1;i<n;i++){
int u=qr(),v=qr(),l=qr();
add(u,v,l);add(v,u,l);
}
calc(0);
if(ans==inf) ans=-1;
printf("%d
",ans);
return 0;
}
例题3
看一道比较复杂的
[SPOJ1825] 免费旅行II
在两周年纪念日的旅行之后,在第三年,旅行社SPOJ又一次踏上的打折旅行的道路。
这次旅行是ICPC岛屿上进行的,一个位于太平洋上,不可思议的小岛。我们列出了N个地点(编号从1到N)供旅客游览。这N个点由N-1条边连成一个树,每条边都有一个权值,这个权值可能为负。我们可以选择两个地点作为旅行的起点和终点。
由于当地正在庆祝节日,所以某些地方会特别的拥挤(我们称这些地方为拥挤点)。旅行的组织者希望这次旅行最多访问K个拥挤点。同时,我们希望我们经过的道路的权值和最大。
其中(nle 2 imes10^5),M,K与N同阶
确认是点分治之后,主要考虑如何利用信息进行归并,我们需要的是路径的长度(经过的边数)以及路径上的点的数量,需要达成的目的是满足点的数量的同时取路径最长,和上一道题类似,上一道是满足权值求最小边,所以我们设置的状态是 (f[i]) 表示权值为 (i) 然后状态的属性是 (max) 边的数量,那么这道题仍然考虑DP,设 (f[i]) 表示当前子树内经过 (i) 个拥挤点路径的最长值,(g[i]) 表示已经遍历过的子树内经过 (i) 个拥挤点路径的最长值,那么我们在确定好重心之后,对于每个(f[i]) 考虑已经处理过的子树内$0 (~)K-i$ 的状态,二者相加来更新ans,怎么考虑这个(0)~(K-i) 的状态?本人采用了ST表的办法,这样每次询问就是(O(1))的,统计过答案之后更新 (g) 数组(再次建表),每次建表是(O(nlogn)) 所以总复杂度(O(nlog^2n))。
可能ST表复杂度比较高。而题目(Nle2 imes10^5) ,所以(O(nlog^2n))太大了,所以代码里对于(K==M)的部分采用了树形DP。
点击此处展开和收起代码
#include<bits/stdc++.h>
#define reg register
typedef long long ll;
using namespace std;
inline int qr(){
int x=0,f=0;char ch=0;
while(!isdigit(ch)){f|=ch=='-';ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return f?-x:x;
}
const int maxn=2e5+100;
int n,K,M;//点数,忍受数,拥挤点数
struct node{
int next,to,cost;
}edge[maxn<<1];
int head[maxn],_tot;
void add(int u,int v,int l){edge[_tot].next=head[u];edge[_tot].to=v;edge[_tot].cost=l;head[u]=_tot++;}
ll f[maxn],g[maxn];
ll F[5000000][22];
bool o[maxn];
bool sb[maxn];//拥挤的点
int mx,maxs;
ll ans;
int get_size(int x,int fa){
if(o[x]) return 0;
int res=1;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y==fa) continue;
res+=get_size(y,x);
}
return res;
}
int get_wc(int x,int fa,int tot,int &wc){
if(o[x]) return 0;
int sum=1,maxx=0;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y==fa) continue;
int t=get_wc(y,x,tot,wc);
sum+=t;
maxx=max(maxx,t);
}
maxx=max(maxx,tot-sum);
if(maxx<=tot/2) wc=x;
return sum;
}
int cnt;
ll fp,G;
void get_dis(int x,int fa,ll dis){//cnt表示经过了几个拥挤点
fp=max(fp,dis);
if(o[x]) return ;
if(sb[x]&&cnt>=K) return;
if(sb[x]) cnt++,f[cnt]=max(dis,f[cnt]);
else f[cnt]=max(f[cnt],dis);
maxs=max(cnt,maxs);
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y==fa) continue;
get_dis(y,x,dis+edge[i].cost);
}
if(sb[x]) cnt--;
}
void st(){
for(int i = 0; i <= mx+1; i ++) F[i][0] = g[i];
int t = log(n) / log(2) + 1;
for(int j = 1; j < 20; j ++){
for(int i = 0; i <= mx - (1 << j) +1; i ++){
F[i][j] = max(F[i][j-1],F[i + (1 << (j - 1))][j - 1]);
}
}
}
int query(int x, int y)
{
int t = log(abs(y-x +1))/ log(2);
int a = F[x][t];
int b = F[y - (1 << t) +1][t];
return max(a,b);
}
ll dp1[maxn],dp2[maxn];
void calc(int x){
if(o[x]) return;
if(K==M) {//特判树形DP
o[x]=1;G=0;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(o[y]) continue;
calc(y);
if(dp1[x]<dp1[y]+edge[i].cost){
dp2[x]=dp1[x];dp1[x]=dp1[y]+edge[i].cost;
}else if(dp2[x]<dp1[y]+edge[i].cost){
dp2[x]=dp1[y]+edge[i].cost;
}
ans=max(dp1[x]+dp2[x],ans);
}
return;
}
get_wc(x,-1,get_size(x,-1),x);
o[x]=1;mx=0;
if(sb[x])K--;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(o[y]) continue;
maxs=0;cnt=0;
get_dis(y,x,edge[i].cost);
mx=max(maxs,mx);
st();//重新建表
for(reg int j=0;j<=maxs;j++){
ans=max(ans,f[j]);
ans=max(ans,f[j]+query(0,min(mx,max(0,K-j))));//查询区间最大值
}
for(reg int j=0;j<=maxs;j++) {
g[j]=max(g[j],f[j]);f[j]=0;
}
}
if(sb[x]) K++;
for(reg int i=0;i<=mx;i++) g[i]=0;//清空
for(reg int i=head[x];~i;i=edge[i].next) calc(edge[i].to);
}
int main(){
freopen("freetourII.in","r",stdin);
freopen("freetourII.out","w",stdout);
n=qr(),K=qr(),M=qr();
memset(head,-1,sizeof(head));
for(reg int i=1;i<=M;i++){
int x=qr(); sb[x]=1;
}
for(reg int i=1;i<n;i++){
int u=qr(),v=qr(),l=qr();
add(u,v,l);add(v,u,l);
}
calc(1);
printf("%lld",ans);
return 0;
}
/*
找到一条最长路径,其经过的拥挤点不超过K个
设f[i]为经过i个拥挤点,到达重心的最长距离,每次合并所有子树的时候只需要计算f[i+j]中最大的即可 i+j<=K
*/
到这里基本印证了先前的说法,点分治的模板框架并不难,每道题的核心都在于归并。
例题4
马上就是小苗的生日了,为了给小苗准备礼物,小葱兴冲冲地来到了商店街。商店街有 (n)个商店,并且它们之间的道路构成了一棵树的形状。
第 (i) 个商店只卖第 (i)种物品,小苗对于这种物品的喜爱度是 (w_i),物品的价格为$ c_i$,物品的库存是 (d_i)。但是商店街有一项奇怪的规定:如果在商店 (u,v)买了东西,并且有一个商店 (p) 在 (u)到 (v)的路径上,那么必须要在商店 (p)买东西。小葱身上有 (m)元钱,他想要尽量让小苗开心,所以他希望最大化小苗对买到物品的喜爱度之和。
这种小问题对于小葱来说当然不在话下,但是他的身边没有电脑,于是他打电话给同为OI选手的你,你能帮帮他吗?
(Nle500;m,w_i,c_ile 4000;d_ile100;c_ile m;) 多测 (Tle5)
假如问题放在序列上,那么这道题就是一个简单的多重背包,放在树上就需要考虑树形背包,一般的树形背包是(f[i])表示子树下的状态,但这题限制了选择了两个点之后,两点之间的路径上所有商店都至少要选择一个商品,这样就出现了"树上路径"问题,考虑另一种状态表示,(f[i][j])表示考虑了dfn序 (i) ~ (n) 的物品,背包占用为 (j) 的最大价值,我们先肯定我们一定要选择重心,不选择重心的情况我们递归考虑,然后我们求出从重心出发每个点的dfn序,并每个点记录一个out表示子树内dfn序的最大值,然后我们倒序考虑dfn序,即从深到浅考虑背包,并且将每个点必选一个的状态强行放入,这样我们每个点就能够"拓扑的"考虑到它被强迫选择的情况(因为一定要选择重心,所以假如有选择了更深处的点,那么一定会选择较浅的点),并更新背包的状态,当然对于一个子树可以完全不选择以达到更优,这个时候我们的out就可以帮助我们将(f) 带回到采用这个子树前的状态,然后"纠正"背包的状态,背包需要采用二进制分组来优化,这样复杂度就可以做到 (O(nmlog d log n)) 了。
点击此处展开和收起代码
#include<bits/stdc++.h>
#define reg register
#define mp make_pair
typedef long long ll;
using namespace std;
inline int qr(){
int x=0,f=0;char ch=0;
while(!isdigit(ch)){f|=ch=='-';ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return f?-x:x;
}
const int N=4140;
int n,m,ans;
int w[N],c[N],d[N];
int dfn[N],dfncnt,out[N];
int f[N][N];//考虑了dfn序列i~n的背包
bool o[N];
struct node{
int next,to;
}edge[N<<1];
int head[N],_tot;
inline void add(int u,int v){
edge[_tot]=(node){head[u],v},head[u]=_tot++;
}
pair<int,int>p[N];
int get_size(int x,int fa){
if(o[x]) return 0;
int sum=1;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y==fa) continue;
sum+=get_size(y,x);
}
return sum;
}
int get_wc(int x,int fa,int tot,int &wc){
if(o[x]) return 0;
int res=1,maxson=-1;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y==fa) continue;
int c=get_wc(y,x,tot,wc);
res+=c;
maxson=max(c,maxson);
}
maxson=max(tot-maxson,maxson);
if(maxson<=tot/2) wc=x;
return res;
}
void get_dfn(int x,int fa){
if(o[x]) return;
dfn[++dfncnt]=x;
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
if(y==fa) continue;
get_dfn(y,x);
}out[x]=dfncnt;
}
void calc(int x){
if(o[x]) return;
get_wc(x,0,get_size(x,0),x);
dfncnt=0;
get_dfn(x,0);
o[x]=1;
for(reg int i=dfncnt;i;i--){
int s=d[dfn[i]]-1,num=0;
for(reg int j=1;j<=s;s-=j,j<<=1) p[++num]={w[dfn[i]]*j,c[dfn[i]]*j};//二进制分组
if(s) p[++num]={w[dfn[i]]*s,c[dfn[i]]*s};
for(reg int j=m;j>=c[dfn[i]];j--){//强行规定必选一个i的状态
f[i][j]=f[i+1][j-c[dfn[i]]]+w[dfn[i]];
}
for(reg int k=1;k<=num;k++){//再考虑之前的所有物品
for(reg int j=m;j>=p[k].second;j--){
f[i][j]=max(f[i][j],f[i][j-p[k].second]+p[k].first);
}
}
for(reg int j=0;j<=m;j++){//不选择这颗子树,考虑子树外,相当于尝试纠正刚刚考虑强塞这颗子树的点的错误状态
f[i][j]=max(f[i][j],f[out[dfn[i]]+1][j]);
}
}
ans=max(ans,f[1][m]);
for(reg int i=0;i<=dfncnt;i++){
for(reg int j=0;j<=m;j++){
f[i][j]=0;
}
}
for(reg int i=head[x];~i;i=edge[i].next){
int y=edge[i].to;
calc(y);
}
}
void clear(){
memset(head,-1,sizeof head);
memset(o,0,sizeof o);
_tot=ans=dfncnt=0;
}
int main(){
freopen("shopping.in","r",stdin);
freopen("shopping.out","w",stdout);
int T=qr();
while(T--){
clear();
n=qr(),m=qr();
for(reg int i=1;i<=n;i++) w[i]=qr();
for(reg int i=1;i<=n;i++) c[i]=qr();
for(reg int i=1;i<=n;i++) d[i]=qr();
for(reg int i=1;i<n;i++){
int u=qr(),v=qr();
add(u,v);add(v,u);
}
calc(1);
printf("%d
",ans);
}
return 0;
}
点分治小总结
*思考,在什么情况下使用点分治而不是其他的大数据结构,树形DP之类的算法。
1 树上路径问题,求点对之类的问题,这类问题比较容易点分。
2 对于确定一个点之后,其它点和它的关系所产生的信息能够在(O(nlogn))或者一个再乘(logn)不会T的复杂度之内解决的(一般来说不会是(n^2),因为 (n^2) 大概率是暴力统计点对),且不需要再考虑这个点的信息的问题。
点分树
(待更)