【点分治】的学习笔记和众多例题

【前言】

  最近一段时间变成了通过题目学习算法,似乎整个人都乱套了(反思ing)

  不过还好,现在又调整为了学算法后做题。(唉,最近一段时间有点急躁,要记住万事不能速成啊)

【正题】点分治

  一句话:点分治主要用于树上路径点权统计问题。

一、【具体流程】

1,选取一个点,将无根树变成有根树
 为了使每次的处理最优,我们通常要选取树的重心。
 何为“重心”,就是要保证与此点连接的子树的节点数最大值最小,可以防止被卡。
 重心求法:
  1。dfs一次,算出以每个点为根的子树大小。
  2。记录以每个节点为根的最大子树大小
  3。判断:如果以当前节点为根更优,就更新当前根。

void getroot(int v,int fa)
{
    son[v] = 1; f[v] = 0;//f记录以v为根的最大子树的大小 
    for(int i = head[v];i;i=e[i].next)
        if(e[i].to != fa && !vis[e[i].to]) {
            getroot(e[i].to,v);//递归更新 
            son[v] += son[e[i].to];
            f[v] = max(f[v],son[e[i].to]);//比较每个子树 
        }
    f[v] = max(f[v],sum-son[v]);//别忘了以v父节点为根的子树 
    if(f[v] < f[root]) root = v;//更新当前根 
}

2、处理连通块中通过根节点的路径。
  (注意,是通过根节点的路径,所以后面要去掉同一子树内部的路径,即去重)
3、标记根节点(相当于处理后,将根节点从子树中删除)。
4、递归处理当前点为根的每棵子树。

int solve(int v)
{
    vis[v] = 1;//标记 
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to]) {
            root = 0;
            sum = son[e[i].to];
            getroot(e[i].to,v);
            solve(root);//递归处理下一个连通块 
        }
}
int main()
{
    sum = f[0] = n;//初始化 
    root = 0;
    getroot(1,0);//找重心 
    solve(root);//点分治 
}

【注释】:作者是用 son[] 来表示节点x为根的子树大小,可能他人更多地是用size[]来表示,二者同意。

二、【POJ 1741 & BZOJ 1468 & BZOJ 3365】

给你一棵TREE,以及这棵树上边的距离.问有多少对点它们两者间的距离小于等于K。
【题解】:
  我们找到树的重心,然后dfs,求出每个点到root的距离deep,然后对deep排序,扫描哪些点对是符合的。
  但是,点分治要求处理的路径是经过root,所以如果一条路径是在同一个子树之内的就不符合要求,所以还要对子树dfs一下,然后去重。
  接下来处理好root后,就可以处理其他连通块了,即递归其子树。
【代码】:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

#define N 10010
#define inf 1e9+10
struct node{int to,c,next;}g[N*2];
int head[N],m;
int son[N],f[N];
bool vis[N];
int d[N],deep[N];
int n,sum,root,k,ans;

void add_edge(int from,int to,int cost)
{
    g[++m].next = head[from];
    head[from] = m;
    g[m].to = to; g[m].c = cost;
}

void getroot(int v,int fa)
{
    son[v] = 1; f[v] = 0;
    for(int i = head[v];i;i=g[i].next)
        if(g[i].to != fa && !vis[g[i].to])
        {
            getroot(g[i].to,v);
            son[v] += son[g[i].to];
            f[v] = max(f[v],son[g[i].to]);
        }
    f[v] = max(f[v],sum - son[v]);
    if(f[v] < f[root]) root = v;
}

void getdeep(int v,int fa)
{
    deep[++deep[0]] = d[v];
    for(int i = head[v];i;i=g[i].next)
        if(g[i].to != fa && !vis[g[i].to])
        {
            d[g[i].to] = d[v] + g[i].c;
            getdeep(g[i].to,v);
        }
}

int cal(int v,int cost)
{
    d[v] = cost; deep[0] = 0;
    getdeep(v,0);
    sort(deep+1,deep+deep[0]+1);
    int l = 1,r = deep[0],sum = 0;
    while(l < r) {
        if(deep[l]+deep[r] <= k) {
            sum += r-l;
            l++;
        } else r--;
    }
    return sum;
}

void solve(int v)
{
    ans += cal(v,0);
    vis[v] = 1;
    for(int i = head[v];i;i=g[i].next)
        if(!vis[g[i].to])
        {
            ans -= cal(g[i].to,g[i].c);
            sum = son[g[i].to];
            root = 0;
            getroot(g[i].to,0);
            solve(root);
        }
}

int main()
{
    int u,v,w;
    while(scanf("%d%d",&n,&k) && n && k)
    {
        ans = root = m = 0;
        memset(vis,0,sizeof(vis));
        memset(head,0,sizeof(head));
        for(int i = 1;i < n;i++)
        {
            scanf("%d%d%d",&u,&v,&w);
            add_edge(u,v,w);
            add_edge(v,u,w);
        }
        f[0] = inf;
        sum = n;
        getroot(1,0);
        solve(root);
        printf("%d\n",ans);
    }
    return 0;
}

【补】:若是距离等于k,cal可以改成:

int cal(int v,int cost)
{
    d[v] = cost; deep[0] = 0;
    getdeep(v,0);
    sort(deep+1,deep+deep[0]+1);
    int r = deep[0],res = 0;
    for(int l = 1;l < r;l++)
        while(deep[l]+deep[r] >= k) {
            if(deep[l] + deep[r] == k) res++;
            r--;
        }
    return res;
}

三、【BZOJ 2152】

由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。
【题解】:
  感觉这道更好处理,不用快排,也不用去重。我们对于当前的树,直接找到重心V,然后从V出发,搜索与V相邻的点,计算边长的余数分别是是0,1,2的情况数,用t[0],t[1],t[2]分别表示。
  显然答案就是 t[1]*t[2]*2+t[0]*t[0]。
【代码】:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

#define N 20010
struct node{int to,c,next;}e[N*2];
int head[N],m;
int ans,root,t[4],d[N],son[N],f[N],sum;
bool vis[N];

void add_edge(int from,int to,int cost)
{
    e[++m].next = head[from];
    head[from] = m;
    e[m].to = to; e[m].c = cost;
}

void getroot(int v,int fa)
{
    son[v] = 1;f[v] = 0;
    for(int i = head[v];i;i = e[i].next)
        if(!vis[e[i].to] && e[i].to != fa)
        {
            getroot(e[i].to,v);
            son[v] += son[e[i].to];
            f[v] = max(f[v],son[e[i].to]);
        }
    f[v] = max(f[v],sum-son[v]);
    if(f[v] < f[root]) root = v;
}

void getdeep(int v,int fa)
{
    t[d[v]]++;
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to] && e[i].to != fa)
        {
            d[e[i].to] = (d[v] + e[i].c)%3;
            getdeep(e[i].to,v);
        }
}

int cal(int v,int w)
{
    t[0] = t[1] = t[2] = 0;
    d[v] = w;
    getdeep(v,0);
    return t[1]*t[2]*2+t[0]*t[0];
}

void solve(int v)
{
    ans += cal(v,0); vis[v] = 1;
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to])
        {
            ans -= cal(e[i].to,e[i].c);
            root = 0; sum = son[e[i].to];
            getroot(e[i].to,0);
            solve(root);
        }
}

inline int gcd(int a,int b){return b == 0 ? a : gcd(b,a%b);}
int main()
{
    int n,u,v,w;
    scanf("%d",&n);
    for(int i = 1;i < n;i++)
    {
        scanf("%d%d%d",&u,&v,&w);
        w %= 3;
        add_edge(u,v,w); add_edge(v,u,w);
    }
    sum = n;f[0] = n;
    root = ans = 0;
    getroot(1,0);
    solve(root);
    int x = gcd(ans,n*n);
    printf("%d/%d\n",ans/x,n*n/x);
    return 0;
}

先到这里,我下去逛逛。未完待续……

好了,我又回来了

四、【BZOJ 2599】

给一棵树,每条边有权.求一条简单路径,权值和等于K,且边的数量最小.N <= 200000, K <= 1000000
【题解】:
参考黄学长的题解啊。
  开一个100W的数组t,t[i]表示权值为i的路径最少边数
  找到重心分成若干子树后, 得出一棵子树的所有点到根的权值和x,到根a条边,用t[k-x]+a更新答案,全部查询完后,再用所有a更新t[x],这样可以保证不出现点分治中的不合法情况。
  把一棵树的所有子树搞完后再遍历所有子树恢复T数组,如果用memset应该会比较慢
  
看的稀里糊涂的,但还是好像懂了一点啊。
  d数组 表示已经有几条边
  dis数组 表示子树中的点到根的距离
  add函数用于更新和初始化(好像有这个功能吧)
【代码】:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

#define N 200010
#define inf 1000000000
struct node{int to,c,next;}e[N*2];
int head[N],m,k;
bool vis[N];
int t[1000010];
int sum,f[N],dis[N],d[N],son[N],root,ans;

void add_edge(int from,int to,int cost)
{
    e[++m].next = head[from];
    head[from] = m;
    e[m].to = to;e[m].c = cost;
}

void getroot(int v,int fa)
{
    son[v] = 1;f[v] = 0;
    for(int i = head[v];i;i=e[i].next)
        if(e[i].to != fa && !vis[e[i].to]) {
            getroot(e[i].to,v);
            son[v] += son[e[i].to];
            f[v] = max(f[v],son[e[i].to]);
        }
    f[v] = max(f[v],sum-son[v]);
    if(f[v] < f[root]) root = v;
}

void cal(int v,int fa)
{
    if(dis[v] <= k) ans = min(ans,d[v]+t[k-dis[v]]);
    for(int i = head[v];i;i=e[i].next)
        if(e[i].to != fa && !vis[e[i].to]) {
            d[e[i].to] = d[v] + 1;
            dis[e[i].to] = dis[v] + e[i].c;
            cal(e[i].to,v);
        }
}

void add(int v,int fa,bool flag)
{
    if(dis[v] <= k) {
        if(flag) t[dis[v]] = min(t[dis[v]],d[v]);
        else t[dis[v]] = inf;
    }
    for(int i = head[v];i;i=e[i].next)
        if(e[i].to != fa && !vis[e[i].to])
            add(e[i].to,v,flag);
}

void solve(int v)
{
    vis[v] = 1;t[0] = 0;
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to]) {
            d[e[i].to] = 1;
            dis[e[i].to] = e[i].c;
            cal(e[i].to,0);
            add(e[i].to,0,1);
        }
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to]) add(e[i].to,0,0);
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to]) {
            root = 0;
            sum = son[e[i].to];
            getroot(e[i].to,0);
            solve(root);
        }
}

int main()
{
    int n,u,v,w;;
    scanf("%d%d",&n,&k);
    for(int i = 1;i <= k;i++) t[i] = n;
    for(int i = 1;i < n;i++) {
        scanf("%d%d%d",&u,&v,&w);
        u++; v++;
        add_edge(u,v,w); add_edge(v,u,w);
    }
    ans = sum = f[0] = n;
    root = 0;
    getroot(1,0);
    solve(root);
    if(ans != n) printf("%d\n",ans);
        else puts("-1");
    return 0;
}

啊!
今天对点分治的学习差不多就到这里了。
笔记结束,开始刷水题玩喽。

补:

五、【BZOJ 1316】

一棵n个点的带权有根树,有p个询问,每次询问树中是否存在一条长度为Len的路径,如果是,输出Yes否输出No.
【题解】:
  运用点分治统计点到重心的距离,再两次二分查找距离,判断有多少条路径长度为k(这样为了方便去重)
  听说点分治的常数比较大,所以将所有询问在一次点分治中一起做。
【代码】:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

#define N 10010
struct node{int to,w,next;}e[N*2];
int head[N],m,p;
int q[110],f[N],son[N],sum,root,d[N],deep[N];
bool vis[N];

void add_edge(int from,int to,int cost)
{
    e[++m].next = head[from];
    head[from] =m;
    e[m].w = cost; e[m].to = to;
}

void getroot(int v,int fa)
{
    son[v] = 1;f[v] = 0;
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to] && e[i].to != fa) {
            getroot(e[i].to,v);
            son[v] += son[e[i].to];
            f[v] = max(f[v],son[e[i].to]);
        }
    f[v] = max(f[v],sum-son[v]);
    if(f[v] < f[root]) root = v;
}

void getdeep(int v,int fa)
{
    deep[++deep[0]] = d[v];
    for(int i = head[v];i;i=e[i].next)
        if(e[i].to != fa && !vis[e[i].to]) {
            d[e[i].to] = d[v] + e[i].w;
            getdeep(e[i].to,v);
        }
}

int findl(int L,int R,int k)
{
    int ans = 0;
    while(L <= R){
        int mid = (L+R)>>1;
        if(deep[mid] == k){ans = mid;R = mid-1;}
        else if(deep[mid] < k) L = mid + 1;
                else R = mid - 1;
    }
    return ans;
}

int findr(int L,int R,int k)
{
    int ans = -1;
    while(L <= R) {
        int mid = (L+R)>>1;
        if(deep[mid] == k){ans = mid;L = mid+1;}
        else if(deep[mid] < k) L = mid+1;
                else R = mid - 1;
    }
    return ans;
}

int cal(int v,int now,int k)
{
    d[v] = now; deep[0] = 0;
    getdeep(v,0);
    sort(deep+1,deep+deep[0]+1);
    int t = 0;
    for(int i = 1;i <= deep[0];i++) {
        if(deep[i] + deep[i] > k) break;
        int l = findl(i,deep[0],k-deep[i]);
        int r = findr(i,deep[0],k-deep[i]);
        t += r-l+1;
    }
    return t;
}

int ans[110];
void solve(int v)
{
    for(int i = 1;i <= p;i++) ans[i] += cal(v,0,q[i]);
    vis[v] = 1;
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to]) {
            for(int j = 1;j <= p;j++)
                ans[j] -= cal(e[i].to,e[i].w,q[j]);
            sum = son[e[i].to];
            root = 0;
            getroot(e[i].to,0);
            solve(root);
        }
}

int main()
{
    int n,u,v,w;
    scanf("%d%d",&n,&p);
    m = 0;
    for(int i = 1;i < n;i++)
    {
        scanf("%d%d%d",&u,&v,&w);
        add_edge(u,v,w); add_edge(v,u,w);
    }
    for(int i = 1;i <= p;i++) scanf("%d",&q[i]);
    sum = f[0] = n;
    root = 0;
    getroot(1,0);
    solve(root);
    for(int i = 1;i <= p;i++)
        if(ans[i]) puts("Yes"); else puts("No");
    return 0;
}

吾 点分治 之道路大概结束于此。


PS:4月3日,第四次更新。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值