点分治模板、例题整理

1.Acwing252. 树

传送门

1.题意

求一个树中距离小于等于k的点对的个数。

1 ≤ N ≤ 1 0 4 1≤N≤10^4 1N104

2.分析

点分治算法:

树的重心

树的重心是指:删除该点后,最大子树(的点数)最小的点。
关于重心的结论:删除重心之后,最大子树的点数小于等于总点数/2。

于是:我们可以将一个树上问题删去重心点之后分解成若干个子树内部的问题和子树之间的问题。这样可以保证是log级的时间复杂度。(边分治则不行,会被菊花图卡成n)

关于本题

我们先将重心找出来,然后路径小于等于k的点对可以分成如下:
1.两点在同一颗子树中。(递归到下一层求解)
2.两点在不同子树中。(在本层求解)
3.有一点是该层的重心。(在本层求解)

现在考虑如何处理本层的信息:
1.预处理出该联通块所有点到该层重心的距离。
2.先将同一颗子树中到重心距离和小于等于k的点对个数统计出来,删去(容斥原理)。
3.遍历所有点对,统计答案(无论是不是在同一颗子树中)。
4.统计 有一点是该层的重心 的个数。

具体实现

1.递归出口
定义一个bool st[]数组,记录哪些点还没被作为重心删掉,如果递归到一个st[u]==false的点,则return。

if (st[u]) return;

2.计算该连通块中到重心的距离(每次都要重新计算)
无需建立点于距离的对应关系,dist直接保存在q数组中就行了。

void get_dist(int u, int fa, int dist, int& qt)
{
    if (st[u]) return;
    q[qt ++ ] = dist;
    for (int i = h[u]; ~i; i = ne[i])
        if (e[i] != fa)
            get_dist(e[i], u, dist + w[i], qt);
}

3.求该连通块的重心
递归进该层时,重新计算该层的重心,并返回该层的点数
注意:这里求出的重心并不一定是真正的重心,只是满足删除该点之后,最大子树的点数小于等于总点数/2。

int get_wc(int u, int fa, int tot, int &wc) //求重心,返回子树大小
{
    if (st[u])
        return 0;
    int sum = 1, ms = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa)
            continue;
        int t = get_wc(j, u, tot, wc);
        ms = max(ms, t); //ms是最大子树size
        sum += t;
    }
    ms = max(ms, tot - sum);
    if (ms <= tot / 2)
        wc = u;
    return sum;
}

4.通过已经储存下来的dist数组,统计答案

int get(int a[], int len) //统计答案,和小于等于k的点对个数
{
    sort(a, a + len);
    int res = 0;
    for (int i = len - 1, j = -1; i >= 0; i--)
    {
        while (j + 1 < i && a[j + 1] + a[i] <= k)
            j++;
        j = min(j, i - 1);
        res += j + 1;
    }
    return res;
}

具体代码

对着y总的代码码了一遍

#include <bits/stdc++.h>

using namespace std;
//-----pre_def----
const double PI = acos(-1.0);
const int INF = 0x3f3f3f3f;
typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef pair<double, double> PDD;
#define fir(i, a, b) for (int i = (a); i <= (b); i++)
#define rif(i, a, b) for (int i = (a); i >= (b); i--)
#define endl '\n'
#define init_h memset(h, -1, sizeof h), idx = 0;
#define lowbit(x) x &(-x)

//---------------
//点分支:在树上分支,每次找到的点是当前子树重心
//问题:一颗树中长度小于等于k的路径条数
const int N = 1e4 + 10;
int n, k;
int h[N], e[N << 1], w[N << 1], ne[N << 1], idx;
bool st[N];
int q[N], p[N];
void add(int a, int b, int c)
{
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx++;
}
int get_size(int u, int fa) //返回该子树的大小
{
    if (st[u])
        return 0;
    int res = 1;
    for (int i = h[u]; ~i; i = ne[i])
    {
        if (e[i] != fa)
            res += get_size(e[i], u);
    }
    return res;
}
void get_dist(int u, int fa, int dist, int &qt) //重新计算每个点到root的dist 存在q数组中
{
    if (st[u])
        return;
    q[qt++] = dist;
    for (int i = h[u]; ~i; i = ne[i])
        if (e[i] != fa)
            get_dist(e[i], u, dist + w[i], qt);
}
int get_wc(int u, int fa, int tot, int &wc) //求重心,返回子树大小
{
    if (st[u])
        return 0;
    int sum = 1, ms = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa)
            continue;
        int t = get_wc(j, u, tot, wc);
        ms = max(ms, t); //ms是最大子树size
        sum += t;
    }
    ms = max(ms, tot - sum);
    if (ms <= tot / 2)
        wc = u;
    return sum;
}
int get(int a[], int kk) //统计答案
{
    sort(a, a + kk);
    int res = 0;
    for (int i = kk - 1, j = -1; i >= 0; i--)
    {
        while (j + 1 < i && a[j + 1] + a[i] <= k)
            j++;
        j = min(j, i - 1);
        res += j + 1;
    }
    return res;
}
int calc(int u)
{
    if (st[u])
        return 0;                      //已经作为重心被删去了
    get_wc(u, -1, get_size(u, -1), u); //找到重心,此时u就是重心
    //求解该层中两点不在一颗子树中的情况
    st[u] = true;
    int res = 0;
    int pt = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i], qt = 0;
        get_dist(j, -1, w[i], qt);      //求出该子树每个点到重心的距离
        res -= get(q, qt);              //?
        for (int kk = 0; kk < qt; kk++) //统计有一个点是重心的情况
        {
            if (q[kk] <= k)
                res++;
            p[pt++] = q[kk];
        }
    }
    res += get(p, pt);

    for (int i = h[u]; ~i; i = ne[i])
        res += calc(e[i]);
    return res;
}

void init()
{
    init_h;
    memset(st, 0, sizeof st);
}
int main()
{
#ifndef ONLINE_JUDGE
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
    int StartTime = clock();
#endif
    while (scanf("%d%d", &n, &k), n || k)
    {
        init();
        for (int i = 0; i < n - 1; i++)
        {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            add(a, b, c), add(b, a, c);
        }
        printf("%d\n", calc(0));
    }
#ifndef ONLINE_JUDGE
    printf("Run_Time = %d ms\n", clock() - StartTime);
#endif
    return 0;
}

2.Acwing. 权值

传送门

题意

求长度等于k的路径中,点数最少的点数。

分析

树上的统计答案问题,而且可以将大问题拆分成子问题。
可以用点分治做。

代码

#include <bits/stdc++.h>

using namespace std;
//-----pre_def----
const double PI = acos(-1.0);
const int INF = 0x3f3f3f3f;
typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef pair<double, double> PDD;
#define fir(i, a, b) for (int i = (a); i <= (b); i++)
#define rif(i, a, b) for (int i = (a); i >= (b); i--)
#define endl '\n'
#define init_h memset(h, -1, sizeof h), idx = 0;
#define lowbit(x) x &(-x)

//---------------
//距离等于k的点对:
//1.两点不在同一颗子树
//2.两点在同一颗子树
//3.一点是重心

const int N = 2e5 + 10;
int n, k;
int h[N], e[N << 1], w[N << 1], ne[N << 1], idx;
int f[1000010]; //f用于记录答案,类似dp。f[i]表示距离为i的点数,初始化成inf,可以转移
int ans = INF;
bool st[N];
PII q[N], p[N]; //大小数组模板?
void add(int a, int b, int c)
{
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx++;
}

int get_size(int u, int fa) //仅需要返回连通块点数就行了
{
    if (st[u])
        return 0;
    int res = 1;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int t = e[i];
        if (t == fa)
            continue;
        res += get_size(t, u);
    }
    return res;
}

int get_wc(int u, int fa, int tot, int &wc) //得到重心点,并返回子树大小
{
    if (st[u])
        return 0;
    int sum = 1, ms = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int t = e[i];
        if (t == fa)
            continue;
        int tt = get_wc(t, u, tot, wc);
        sum += tt;
        ms = max(ms, tt);
    }
    ms = max(ms, tot - sum);
    if (ms <= tot / 2)
        wc = u;
    return sum;
}

void get_dist(int u, int fa, int dist, int bian, int &top)
{
    if (st[u] || dist > k)
        return;
    q[top++] = {dist, bian};
    for (int i = h[u]; ~i; i = ne[i])
    {
        int t = e[i];
        if (t == fa)
            continue;
        get_dist(t, u, dist + w[i], bian + 1, top);
    }
}

void calc(int u)
{
    if (st[u])
        return;
    get_wc(u, -1, get_size(u, -1), u); //找重心
    st[u] = true;
    //处理该层不同子树之间的答案信息
    int pt = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int t = e[i], qt = 0;
        get_dist(t, u, w[i], 1, qt); //对于该点所对应的子树,先在q数组中存储下
        fir(j, 0, qt - 1)
        {
            auto &tt = q[j];
            // 计算一个点是重心的答案
            if (tt.first == k)
                ans = min(ans, tt.second);
            //计算两点在不同子树之间的答案
            ans = min(ans, f[k - tt.first] + tt.second);
            p[pt++] = tt;
        }
        fir(j, 0, qt - 1) //利用q数组更新f数组,为下一颗子树做准备
        {
            auto &tt = q[j];
            f[tt.first] = min(f[tt.first], tt.second);
        }
    }
    // 利用大p数组,将f数组还原成全是inf的初始化状态(memset会超时)
    fir(i, 0, pt - 1)
    {
        f[p[i].first] = INF;
    }
    //处理子树内的答案信息
    for (int i = h[u]; ~i; i = ne[i])
    {
        calc(e[i]);
    }
}

int main()
{
#ifndef ONLINE_JUDGE
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
    int StartTime = clock();
#endif
    init_h;
    scanf("%d%d", &n, &k);
    fir(i, 1, n - 1)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
        add(b, a, c);
    }
    memset(f, 0x3f, sizeof f);
    calc(0);
    if (ans == INF)
        ans = -1;
    printf("%d\n", ans);
#ifndef ONLINE_JUDGE
    printf("Run_Time = %d ms\n", clock() - StartTime);
#endif
    return 0;
}

小结:
点分治用于处理树上答案,1.需要统计的答案的问题。2.可以转换成子问题的问题。
点分治给人感觉并不是一个特别优秀的算法。
每一层中,都要重新预处理出相关信息,处理答案在不同子树中的情况&&答案压在该层重心的情况。
但是因为重心的性质:整体的时间复杂度是log级的,递归函数熟练之后,写起来码量也不大。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值