P4178 Tree (点分治)

题目链接

一:我们考虑树上两点之间的路径有什么情况
1:经过根节点(即在根节点的两端)
2:不经过根节点(完全在一颗子树的一侧)

二:我们考虑这两种路径是否可以归为一类
1:对于第一种的情况两点间路径长度dis( u , v ) 可以看为dis ( u , root ) + dis ( root , v ) 
2:而对于第二种情况我们可以找到u , v 路径所在的子树,将根节点变为子树的根节点,然后就变为了第一种情况
3:总之,所以我们可以不断的递归根节点将所有情况转化为第一种情况
设b[ u ] 表示u属于哪一棵子树(b数组在算法进阶指南里说了,但是代码好像没有体现,而是用了容斥原理)
那么b[ u ] = b[ v ] ,d[ u ] + d[ v ] ≤ k满足条件的第一类路径条数即为满足条件的点对数


三:下面我们来讨论如何计算满足条件的点对数:
1:将目前子树中的节点到根节点的权值放入数组len中,并排序
用两个指针 L,R 扫描数组,每找到一个合法的答案就加上R −L ,就让L++,否则让 R --
2:发现问题

在指针扫描的过程中我们会出现不合的情况

根据len的定义我们存放的一颗树里面所有的边,这是我们计算calc计算时,可能会加上两条边在子树的同一侧的情况,我们可以发现不合法的路径在当前根的一颗子树上,即没有跨越两棵子树(如果跨越了它就合法了),所以们可以在遍历的时候减去不合法的情况(容斥)

3:具体的方法
ans+ = calc( u , 0 ) 
ans − =calc ( v , dis ( u , v ) ) 

这样即可做到不重不漏

四:总结

点分治步骤
  • 找到树的重心
  • 将重心视为根节点,那么树上任意两点有两种情况
  • 路径经过根节点
  • 路径不经过根节点
  • 通过 calc  函数计算出第一种情况下的答案,把根节点从树中删去
  • 对每棵子树递归执行上面的操作

calc函数的计算方法

  • 计算出每个结点到根节点的距离 d [ i ]
  • 将树上的结点按照 d [ i ] 递增排序
  • 指针 l  指向 d [ 1 ] ,指针 r 指向 d [ n ]。
  • 若 l 与 r 指向结点的距离小于 k  ,则 ans + = r − l + 1 , l + + 。
  • 否则 r − −。
  • 当 l > = r  的时候退出循环。
  • 按照上面的方法,会把不经过根节点的路径也算入进去。利用容斥原理修正答案:

五:更具体的做法,见注释

#include <bits/stdc++.h>
using namespace std;
#define pi acos(-1)
#define xx first
#define yy second
#define endl "\n"
#define lowbit(x) x & (-x)
#define int long long
#define ull unsigned long long
#define pb push_back
typedef pair<int, int> PII;
typedef pair<double, double> PDD;
#define max(a, b) (((a) > (b)) ? (a) : (b))
#define min(a, b) (((a) < (b)) ? (a) : (b))
#define Ysanqian ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
const int N = 1e6 + 10, M = 1010, inf = 0x3f3f3f3f, mod = 18446, P = 13331;
const double eps = 1e-8;
int n, k, root, sum, ans;
vector<PII> g[N];
int siz[N], max_part[N], d[N], b[N], len[N];
bool st[N];
void get_root(int u, int fa) // 找出重心
{
    siz[u] = 1, max_part[u] = 0; // siz[i]为其子树的大小,max_part[i]为去掉i后的最大连通块大小
    for (auto ed : g[u])
    {
        int v = ed.xx;
        if (v == fa || st[v])
            continue;
        get_root(v, u);
        siz[u] += siz[v];
        max_part[u] = max(max_part[u], siz[v]);
    }
    max_part[u] = max(max_part[u], sum - siz[u]);
    if (max_part[u] < max_part[root]) // 一开始root为0,且将其赋值为 n即为max
        root = u;
}
void get_dis(int u, int fa) // 找出这颗子树的距离。
{
    len[++len[0]] = d[u]; // len[0]存放的是子树节点个数,len[i] 表示子树中的路径长度
    for (auto ed : g[u])
    {
        int v = ed.xx, w = ed.yy;
        if (v == fa || st[v])
            continue;
        d[v] = d[u] + w; // d[i] 表示i到根节点的距离,由上而下的去跟新d数组,因为子节点距离有父节点跟新而来,故在计算距离时只更新父节点即可
        get_dis(v, u);
    }
}
int calc(int u, int w) // 计算以u为根的第一种情况的答案,也就是在树的两边的,但是对于只在一边得还未计算
{
    d[u] = w, len[0] = 0;            // 先给根节点一个值,如果为0说明在递归子树,不是0那就是再利用容斥出去冗余,将该子树的边数先归零
    get_dis(u, 0);                   // 计算改子树的距离根节点的距离
    sort(len + 1, len + len[0] + 1); // 将距离从小到大排序
    int res = 0;
    for (int l = 1, r = len[0]; l < r;) // 直到不论左移还是右移都大于k,if和else if 都进不去
    {
        if (len[l] + len[r] <= k) // l与最大的相加都小于k,其余也小于k
        {
            res += r - l; // 故加上r-l
            ++l;          // 右移左端点
        }
        else
            r--; // 否则左移右端点
    }
    return res; // 返回以u为根的所有情况的答案
}
void solve(int u)
{
    st[u] = 1;           // 标记删除的重心,防止重复计算
    ans += calc(u, 0);   // 加上以我为跟答案
    for (auto ed : g[u]) // 遍历子树
    {
        int v = ed.xx, w = ed.yy;
        if (st[v])
            continue;
        ans -= calc(v, w); // 减去不合法得情况,不合法的情况全都发生在一课子树上,这里为什么传w,因为我们要加上u与v的边权
        sum = siz[v];      // 不要忘记在以v为根节点时,要修改all_node 的大小
        root = 0;
        get_root(v, u); // 递归根节点,但是肯定会有重复
        solve(root);    // 递归解决
    }
}
signed main()
{
    Ysanqian;
    cin >> n;
    sum = n;
    max_part[0] = n; // 让max_part[0]为n当一个哨兵,以便跟新max_part[i]
    for (int i = 1; i < n; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        g[a].pb({b, c}); // 建边
        g[b].pb({a, c});
    }
    cin >> k;
    get_root(1, -1); // 先找重心
    solve(root);     //
    cout << ans << endl;
    return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
K-D Tree(K-Dimensional Tree算法是一种基于分治法的数据结构,用于高维空间的搜索和排序。它的基本思想是将多维空间中的以某种方式分割成更小的子空间,然后在每个子空间中递归地进行搜索。这样可以大大降低搜索的复杂度。 具体来说,K-D Tree算法可以分为以下几步: 1. 选择一个维度,将数据按照该维度的值进行排序。 2. 找到该维度的中位数,将其作为当前节,并将数据分为左右两个子集。 3. 递归地构建左子树和右子树,每次选择一个新的维度进行划分。 4. 最终得到一个K-D Tree。 在搜索时,我们可以从根节开始,按照一定的规则向下遍历,直到找到目标或者无法继续向下搜索。具体的规则是: 1. 如果目标在当前节的左子树中,则继续向左子树搜索。 2. 如果目标在当前节的右子树中,则继续向右子树搜索。 3. 如果目标和当前节在选定的维度上的值相等,则说明已经找到目标分治法是一种常见的算法思想,它将一个大规模的问题分解成若干个小规模的子问题,每个子问题独立地求解,然后将这些子问题的解合并起来得到原问题的解。分治法通常包含三个步骤:分解、求解、合并。 具体来说,分治法可以分为以下几步: 1. 分解:将原问题分成若干个子问题,每个子问题规模较小且结构与原问题相同。 2. 求解:递归地求解每个子问题,直到问题规模足够小可以直接求解。 3. 合并:将所有子问题的解合并成原问题的解。 分治法的优是可以有效地降低算法的时间复杂度。但是它的缺是需要额外的空间来存储子问题的解,而且分解和合并的过程也需要耗费一定的时间。因此,需要根据实际情况选择合适的算法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值