『点分治及其简单运用』

<更新提示>

<第一次更新>


<正文>

点分治

分治思想

点分治是一种用于处理静态树上路径统计问题的算法,其核心原理还是基于分治思想。

我们不妨对这类树上静态路径统计问题抽象化,例如:

给定一个棵无根树,求满足要求\(P\)的路径有几条。

我们可以使用分治算法求解本题,对于无根树,我们指定一个节点为根,显然,这样的路径可以归为两类:

\(1.\) 满足要求\(P\)且经过根\(root\)的路径

\(2.\) 满足要求\(P\)但不经过根\(root\)的路径

第二类路径不经过根,所以一定在根的某一刻子树中,这我们就可以递归地进行求解,而重点就在于统计第一类路径。

先不考虑统计问题,我们可以设计如下的分治算法:

\(1.\) 求出根\(root\)

\(2.\) 得到相关信息,统计第一类路径并累加入答案中

\(3.\) 删除节点\(root\),对于\(root\)的每一棵子树递归的执行分治算法

显然,递归的层数和每一次选取的根\(root\)有关,当树退化成一条链时,如果任意的选取根,递归层数很可能就达到了\(O(n)\)级别。

解决方法就是选取树的重心作为树的根,容易证明,每一次选取树的重心作为根时,点分治算法最多递归\(log_2n\)层。

计算函数

点分治的另一个重要部分就是如何设计统计经过根的路径的函数,通常来说,我们有两种方法。

树上直接统计

我们可以遍历根的每一颗子树\(s_i\),对于\(s_i\)中的每一个节点,我们利用一层数据结构和之前子树的节点进行统计答案,然后在再将这个子树中的所有节点加入数据结构中。

单调性+尺取法

我们可以一遍\(dfs\)遍历取出所有子节点,然后通过分析有关信息的单调性,用尺取法在\(O(n)\)的时间内统计得到答案。

我们将结合两道例题来分析这两种统计方法。

tree(POJ 1741)

Description

给一颗 n 个节点的树,每条边上有一个距离 v

定义 d(u,v) 为 u 到 v 的最小距离。

给定 k 值,求有多少点对 (u,v) 满足:d(u,v)≤k。

Input Format

输入有多组数据, 以两个 0 结尾。

每组数据第一行两个整数 n,k。

接下来 n−1 行,每行三个整数 x,y,v,表示点 x 到点 y 有一条边距离是 v。

Output Format

对每组数据一行一个答案。

Sample Input

5 4
1 2 3
1 3 1
1 4 2
3 5 1
0 0

Sample Output

8

解析

一道裸的树上路径统计问题,我们使用点分治算法。

其他部分只需要套用点分治算法的模板就可以了,我们考虑如何设计统计函数。对于确定的根\(root\),我们可以通过一遍\(dfs\)求出每一个点到根的距离。我们将每一个点的距离取出来存在临时数组中并排序,那么我们就可以使用尺取法统计答案了。显然,对于一个位置\(l\),满足\(dis[l]+dis[r]\leq k\)的位置\(r\)是单调的,并且随着\(l\)的增大,\(r\)的位置只会减小,那么我们用双指针就能统计答案。

还有一个重复统计的问题,如果两个点在同一棵子树中,显然这种不符合要求的方案也会被尺取法统计进去,我们需要用到容斥原理的思想,在递归求解根的每一颗子树前,先再次的统计出这棵子树中的答案,并在答案中减去这些方案即可。

\(Code:\)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10020 , INF = 0x7f7f7f7f;
int n,k,Head[N*2],t,tot,ans;
int dis[N],flag[N],temp[N];
int size[N],Max[N],root;
struct edge{int ver,val,next;}e[N*2];
inline void insert(int x,int y,int v)
{
    e[++t] = (edge){y,v,Head[x]}; Head[x] = t;
}
inline void input(void)
{
    for (int i=1;i<n;i++)
    {
        int x,y,v;
        scanf("%d%d%d",&x,&y,&v);
        insert(x,y,v) , insert(y,x,v);
    }
}
inline void dp(int x,int f)
{
    size[x] = 1 , Max[x] = 0;
    for (int i=Head[x];i;i=e[i].next)
    {
        int y = e[i].ver;
        if ( flag[y] || y == f ) continue;
        dp(y,x);
        size[x] += size[y];
        Max[x] = max( Max[x] , size[y] );
    }
    Max[x] = max( Max[x] , tot - size[x] );
    if ( Max[root] > Max[x] ) root = x;
}
inline void dfs(int x,int f)
{
    temp[ ++temp[0] ] = dis[x];
    for (int i=Head[x];i;i=e[i].next)
    {
        int y = e[i].ver;
        if ( flag[y] || y == f ) continue;
        dis[y] = dis[x] + e[i].val;
        dfs(y,x);
    } 
}
inline int calc(int x,int len)
{
    dis[x] = len; temp[0] = 0;
    dfs(x,0);
    sort( temp+1 , temp+temp[0]+1 );
    int l = 1 , r = temp[0] , res = 0;
    while ( l < r )
    {
        if ( temp[l] + temp[r] <= k ) 
            res += r-l , l++;
        else r--;
    }
    return res;
}
inline void divide(int x)
{
    flag[x] = true;
    ans += calc(x,0);
    for (int i=Head[x];i;i=e[i].next)
    {
        int y = e[i].ver;
        if ( flag[y] ) continue;
        ans -= calc(y,e[i].val);
        tot = size[y] , root = 0;
        dp(y,0);
        divide(root);
    }
}
inline void reset(void)
{
    t = 0;
    memset( Head , 0 , sizeof Head );
    memset( flag , 0 , sizeof flag );
    ans = 0 , tot = n;
    root = 0 , Max[0] = INF;
}
int main(void)
{
    while ( scanf("%d%d",&n,&k) && n && k )
    {
        reset();
        input();
        dp(1,0);
        divide(root);
        printf("%d\n",ans);
    }
    return 0;
}

免费旅行II(SPOJ 1825)

Description

在两周年纪念日的旅行之后,在第三年,旅行社SPOJ又一次踏上的打折旅行的道路。

这次旅行是ICPC岛屿上进行的,一个位于太平洋上,不可思议的小岛。我们列出了N个地点(编号从1到N)供旅客游览。这N个点由N-1条边连成一个树,每条边都有一个权值,这个权值可能为负。我们可以选择两个地点作为旅行的起点和终点。

由于当地正在庆祝节日,所以某些地方会特别的拥挤(我们称这些地方为拥挤点)。旅行的组织者希望这次旅行最多访问K个拥挤点。同时,我们希望我们经过的道路的权值和最大。

Input Format

第一行,三个整数N,K,M(1 <= N <= 200000, 0 <= K <=M, 0 <= M <= N)

之后的M行,每行一个拥挤点的编号。

最后的N-1行,每行三个整数u,v,l,代表u和v之间有一条权值为l的边

Output Format

一个整数,权值和最大的旅行线路

Sample Input

8 2 3
3
5
7
1 3 1
2 3 10
3 4 -2
4 5 -1
5 7 6
5 6 5
4 8 3

Sample Output

12

解析

题目大意:给定一棵树,其中有若干个点是特殊点,求经过特殊点不超过\(k\)个的最长路径长度。

我们还是考虑点分治算法,显然,我们可以把所有经过特殊点不超过\(k\)的路径都统计出来,然后来更新最优解。

计算函数如何设计呢?我们就可以尝试使用树上直接统计的方法了。我们先一遍\(dfs\)求出每一个节点到根的路径上有几个特殊点,然后建立一个树状数组,下标为经过特殊点的数量,值为路径长度,并维护前缀最大值。

对于根的每一颗子树,我们扫描子树中的每一个节点,并在树状数组中取出经过特殊点数量加上当前点经过特殊点数量不超过\(k\)的最大距离,在加上当前点到根的距离显然就是一个合法的备选答案,尝试更新。

然后我们重新扫描子树中的每一个节点,把它加到树状数组里,就可以在下一棵子树中更新了。

值得注意的是,在进行完当前的统计后,我们还需将树状数组还原,需要在进行一次同样的遍历。

\(Code:\)

#include <bits/stdc++.h>
using namespace std;
const int N = 200020 , INF = 0x7f7f7f7f;
struct BinaryIndexedTree
{
    int c[N],size;
    inline int lowbit(int x){return x&-x;}
    inline void reset(void)
    {
        memset( c , -0x7f , sizeof c );
    }
    inline void insert(int p,int v)
    {
        for ( ;p<=size;p+=lowbit(p))
            c[p] = max( c[p] , v );
    }
    inline int query(int p)
    {
        if ( p <= 0 ) return -INF;
        int res = -INF;
        for ( ;p;p-=lowbit(p)) 
            res = max( res , c[p] );
        return res;
    }
    inline void remove(int p)
    {
        for ( ;p<=size;p+=lowbit(p))
            c[p] = -INF;
    }
}tree;
int n,k,m,Head[N*2],t,tot,spe[N],ans,k_;
int dis[N],flag[N],cnt[N];
int size[N],Max[N],root;
struct edge{int ver,val,next;}e[N*2];
inline void insert(int x,int y,int v)
{
    e[++t] = (edge){y,v,Head[x]} , Head[x] = t;
}
inline void input(void)
{
    scanf("%d%d%d",&n,&k,&m);
    for (int i=1;i<=m;i++)
    {
        int x; scanf("%d",&x);
        spe[x] = true;
    }
    for (int i=1;i<n;i++)
    {
        int x,y,v; scanf("%d%d%d",&x,&y,&v);
        insert(x,y,v) , insert(y,x,v);
    }
}
inline void dp(int x,int f)
{
    size[x] = 1 , Max[x] = 0;
    for (int i=Head[x];i;i=e[i].next)
    {
        int y = e[i].ver;
        if ( flag[y] || y == f ) continue;
        dp( y , x );
        size[x] += size[y];
        Max[x] = max( Max[x] , size[y] );
    }
    Max[x] = max( Max[x] , tot - size[x] );
    if ( Max[root] > Max[x] ) root = x;
}
inline void dfs(int x,int f)
{
    for (int i=Head[x];i;i=e[i].next)
    {
        int y = e[i].ver;
        if ( flag[y] || y == f ) continue;
        dis[y] = dis[x] + e[i].val , cnt[y] = cnt[x] + spe[y];
        dfs( y , x );
    }
}
inline void calc(int x,int f)
{
    ans = max( ans , dis[x] + tree.query( k_ - cnt[x] + 1 ) );
    for (int i=Head[x];i;i=e[i].next)
    {
        int y = e[i].ver;
        if ( flag[y] || y == f ) continue;
        calc( y , x );
    }
}
inline void updata(int x,int f,int type)
{
    if ( type == 1 ) tree.insert( cnt[x] + 1 , dis[x] );
    if ( type == -1 ) tree.remove( cnt[x] + 1 );
    for (int i=Head[x];i;i=e[i].next)
    {
        int y = e[i].ver;
        if ( flag[y] || y == f ) continue;
        updata( y , x , type );
    }
}
inline void divide(int x)
{
    flag[x] = true;
    dis[x] = 0 , cnt[x] = spe[x] , k_ = k + spe[x];
    dfs( x , 0 );
    tree.insert( cnt[x] + 1 , 0 );
    for (int i=Head[x];i;i=e[i].next)
    {
        int y = e[i].ver;
        if ( flag[y] ) continue;
        calc( y , 0 ) , updata( y , 0 , 1 );
    }
    tree.remove( cnt[x] + 1 );
    for (int i=Head[x];i;i=e[i].next)
    {
        int y = e[i].ver;
        if ( flag[y] ) continue;
        updata( y , 0 , -1 );
    }
    for (int i=Head[x];i;i=e[i].next)
    {
        int y = e[i].ver;
        if ( flag[y] ) continue;
        tot = size[y] , root = 0;
        dp( y , 0 );
        divide( root );
    }
}
int main(void)
{
    input();
    root = 0 , tot = n , Max[0] = INF;
    tree.size = n+2 , tree.reset();
    dp(1,0);
    divide(root);
    printf("%d\n",ans);
    return 0;
}

<后记>

posted @ 2019-05-28 20:54  Parsnip  阅读(711)  评论(0编辑  收藏  举报