树链剖分-点的分治(点数为k且距离最长的点对)

HDOJ 4871 | 最短路径树问题
解决给定连通无向图的最短路径树问题,包括构造方法、路径长度计算及包含特定节点数的路径计数。

hdu4871

Shortest-path tree

Time Limit: 6000/3000 MS (Java/Others)    Memory Limit: 130712/130712 K (Java/Others)
Total Submission(s): 382    Accepted Submission(s): 115


Problem Description
Given a connected, undirected graph G, a shortest-path tree rooted at vertex v is a spanning tree T of G, such that the path distance from root v to any other vertex u in T is the shortest path distance from v to u in G.
We may construct a shortest-path tree using the following method:
We consider a shortest-path tree rooted at node 1. For every node i in the graph G, we choose a shortest path from root to i. If there are many shortest paths from root to i, we choose the one that the sequence of passing nodes' number is lexicographically minimum. All edges on the paths that we chose form a shortest-path tree.
Now we want to know how long are the longest simple paths which contain K nodes in the shortest-path tree and how many these paths? Two simple paths are different if the sets of nodes they go through are different.
 

Input
The first line has a number T (T <= 10), indicating the number of test cases.
For each test case, the first line contains three integers n, m, k(1<=n<=30000,1<=m<=60000,2<=k<=n), denote the number of nodes, the number of edges and the nodes of required paths.
Then next m lines, each lines contains three integers a, b, c(1<=a, b<=n, 1<=c<=10000),denote there is an edge between a, b and length is c.
 

Output
For each case, output two numbers, denote the length of required paths and the numbers of required paths.
 

Sample Input
  
1 6 6 4 1 2 1 2 3 1 3 4 1 2 5 1 3 6 1 5 6 1
 

Sample Output
  
3 4
题意:

题意其实就是给你一个图,然后让你转换成一棵树,这棵树满足的是根节点1到其余各点的间隔都是图里的最短间隔,并且为了包管这棵树的独一性,路径也必须是最小的。转化成树的办法其实就是跑一次spfa。spfa的时辰记下所有到这个的前驱的边,然后这些边集反向的边补上就是构成所有最短路的边。然后在这些边上跑一次dfs,跑前将边遵守达到点的序号由小到大排序,重视dfs搜的下一个点的间隔必须是最短的才搜,不然的话搜出来的图就是不合错误的。


至此图的项目组转化完了,剩下的就是求一个图里包含了k个点的路径的最长间隔,以及有几许条,类似的题目还有有几许条路径的乘积=k,有几许条路径的和>k,有几许条路径的乘积是完全立方数。。。做法就是典范的树分治。


具体的做法是找出重心,对重心外的项目组递归求解,归并的时辰列举到重心的所有路径,列举的时辰可以用一个全局的map[s].k和map[s].dis分别记录不包含根节点的顶点个数是s的链的个数和该链的最长距离,时刻更新,然后用一个去列举新的项目组的路径,然后经由过程与mp存的信息对比更新答案;

网络赛的解题报告:

首先构造最短路径树。先求根节点到其他节点的最短路径,然后从根节点开始进行深度优先遍历,先遍历节点编号较小的没遍历过的儿子,这样就能处理处最短路径树。

之后找节点数为K的树链。可以用树分治进行求解,在树分治求解过程中,对于每个中心点,处理出该子树中所有节点到中心点的树链,然后枚举每条树链,比如某条树链节点为a,长度为b,则找之前遍历过的树链中节点数位K-a的长度最长的树链,将这两条树链拼起来可以得到节点数为K的树链,用其更新答案。最好一个一个分支分别处理,可以避免考虑同一个分支来的两条树链。这样枚举并且更新信息,还要存方案数。

 

程序:

#include"stdio.h"
#include"string.h"
#include"iostream"
#include"map"
#include"string"
#include"queue"
#include"stdlib.h"
#include"math.h"
#define M 30009
#define eps 1e-10
#define inf 1000000000
#define mod 1000000000
using namespace std;
struct node
{
    int u,w,v,next;
}edge[M*4];
struct Edge
{
    int u,v,w;
}p[M*4];
struct st
{
    int dis;
    __int64 k;
}mp[M];
int t,k,cnt,maxi,MN,ID;
__int64 ans;
int head[M],use[M],dis[M],id[M],num[M],son[M],limit[M];
int cmp(const void *a,const void *b)
{
    if((*(struct node*)a).u==(*(struct node*)b).u)
        return (*(struct node*)b).v-(*(struct node*)a).v;
    else
        return (*(struct node*)b).u-(*(struct node*)a).u;
}
void init()
{
    t=0;
    memset(head,-1,sizeof(head));
}
void add(int u,int v,int w)
{
    edge[t].u=u;
    edge[t].v=v;
    edge[t].w=w;
    edge[t].next=head[u];
    head[u]=t++;
}
void spfa(int s,int n)
{
    queue<int>q;
    int i;
    for(i=1;i<=n;i++)
        dis[i]=inf;
    memset(use,0,sizeof(use));
    dis[s]=0;
    use[s]=1;
    q.push(s);
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        use[u]=0;
        for(int i=head[u];i!=-1;i=edge[i].next)
        {
            int v=edge[i].v;
            if(dis[v]>dis[u]+edge[i].w)
            {
                dis[v]=dis[u]+edge[i].w;
                p[v].u=u;
                p[v].w=edge[i].w;
                if(!use[v])
                {
                    use[v]=1;
                    q.push(v);
                }
            }
        }
    }
}
void dfs_size(int u,int f)
{
    son[u]=1;
    limit[u]=0;
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(f!=v&&!use[v])
        {
            dfs_size(v,u);
            son[u]+=son[v];
            limit[u]=max(limit[u],son[v]);
        }
    }
}
void dfs_root(int root,int u,int f)
{
    if(son[root]-son[u]>limit[u])
        limit[u]=son[root]-son[u];
    if(MN>limit[u])
    {
        MN=limit[u];
        ID=u;
    }
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(f!=v&&!use[v])
        {
            dfs_root(root,v,u);
        }
    }
}
void get_root(int root,int u,int f)
{
    dfs_size(u,f);
    MN=inf;
    dfs_root(root,u,f);
}
void dfs_dis(int u,int f,int Rank,int w)
{
    id[cnt++]=u;
    num[u]=Rank;
    dis[u]=w;
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(f!=v&&!use[v])
        {
            dfs_dis(v,u,Rank+1,w+edge[i].w);
        }
    }
}
void cal(int u,int f)
{
    cnt=0;
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(f==v||use[v])continue;
        int last=cnt;
        dfs_dis(v,u,2,edge[i].w);
        for(int j=last;j<cnt;j++)
        {
            if(num[id[j]]==k)//根节点的子链点的个数刚好是k
            {
                if(maxi<dis[id[j]])//更新最长距离,同时更新数量
                {
                    maxi=dis[id[j]];
                    ans=1;
                }
                else if(maxi==dis[id[j]])//相等的话ans++;
                {
                    ans++;
                }
            }
            else if(num[id[j]]<k&&mp[k-num[id[j]]].k)//当当前子链点数小于k,则寻找k-num的子链,
            {
                if(maxi<dis[id[j]]+mp[k-num[id[j]]].dis)//更新最长距离
                {
                    maxi=dis[id[j]]+mp[k-num[id[j]]].dis;
                    ans=mp[k-num[id[j]]].k;
                }
                else if(maxi==dis[id[j]]+mp[k-num[id[j]]].dis)//有相等的话ans加上k-num的链的个数
                {
                    ans+=mp[k-num[id[j]]].k;
                }
            }
        }
        for(int j=last;j<cnt;j++)//转化为对立链(即不包含根节点的链)
        {
            if(mp[num[id[j]]-1].dis<dis[id[j]])
            {
                mp[num[id[j]]-1].dis=dis[id[j]];
                mp[num[id[j]]-1].k=1;
            }
            else if(mp[num[id[j]]-1].dis==dis[id[j]])
            {
                mp[num[id[j]]-1].k++;
            }
        }
    }
    for(int i=0;i<cnt;i++)//注意此时更新mp不要写成memset的形式,否则会超时
        mp[i].k=mp[i].dis=0;
}
void dfs_ans(int u,int f)
{
    get_root(u,u,f);
    cal(ID,ID);
    use[ID]=1;
    for(int i=head[ID];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(!use[v])
        {
             dfs_ans(v,v);
        }
    }
}
void solve()
{
    memset(mp,0,sizeof(mp));
    memset(use,0,sizeof(use));
    ans=maxi=0;
    dfs_ans(1,1);
    printf("%d %I64d\n",maxi,ans);
}
int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        int n,m,i;
        scanf("%d%d%d",&n,&m,&k);
        for(i=0;i<m*2;i+=2)
        {
            scanf("%d%d%d",&p[i].u,&p[i].v,&p[i].w);
            p[i^1].u=p[i].v;
            p[i^1].v=p[i].u;
            p[i^1].w=p[i].w;
        }
        init();
        qsort(p,m*2,sizeof(p[0]),cmp);
        for(i=0;i<m*2;i++)
            add(p[i].u,p[i].v,p[i].w);
        spfa(1,n);
        init();
        for(i=2;i<=n;i++)
        {
            add(i,p[i].u,p[i].w);
            add(p[i].u,i,p[i].w);
        }
        solve();
    }
}




<think>好的,我现在需要帮助用户了解“树上差分、树链剖分树上分治树上DUS”这几个概念。首先,用户之前已经询问过“树上统计”,现在继续深入询问更具体的算法和技术,说明他们可能正在学习算法竞赛或高级数据结构,特别是树相关的问题。 首先,我需要确定每个术语的正确含义和常见中文翻译。用户提到的“树上DUS”可能是指Disjoint Set Union(并查集)在树上的应用,但“DUS”可能是拼写错误,正确缩写应为DSU(Disjoint Set Union),也就是并查集。因此,我需要确认这一,并在回答时做出说明。 接下来,每个术语的解释需要准确易于理解。用户可能希望了解这些技术的基本思想、应用场景以及可能的代码示例。考虑到用户之前的问题中有代码示例,可能期望这里也有类似的示例,但需要根据复杂度决定是否提供。 对于“树上差分”,需要解释其与数组差分的区别,以及在树结构中的应用,比如处理路径更新和子树查询。常见的例子是离线处理多次路径修改后的节值统计。 树链剖分(重链剖分)主要用于将树分割成链,以便高效处理路径查询。需要解释其原理、步骤(如重儿子选择、DFS序划分)以及应用,比如结合线段树处理路径上的操作。 树上分治可能指的是分治或边分治,用于处理树上路径问题,比如统计满足某些条件的路径数量。分治通过选择重心分解树,降低问题复杂度,需要说明其步骤和优势,比如处理O(n log n)复杂度的问题。 “DUS”如果是DSU的话,通常用于处理连通性问题,但在树上的应用可能涉及离线查询,比如Tarjan算法求LCA。需要说明如何利用并查集处理树上的离线查询,如连接节并处理查询。 在组织回答时,需要结构清晰,每个部分分说明,包括基本思想、应用场景、实现步骤和示例。同时,要注意术语的正确性,例如确认“DUS”的正确性,并在回答中做出提示,避免误解。 用户可能的深层需求是准备算法竞赛,或者解决实际问题中的树结构相关问题,因此需要强调每个技术的适用场景和优势,帮助他们选择合适的方法。此外,可能需要提供参考资料或例题链接,如LeetCode或OJ题目,方便用户进一步练习。 需要检查每个解释是否准确,比如树链剖分是否确实用于路径查询,分治是否用于路径统计等。同时,确保语言简洁,避免过于技术化的术语,必要时用比喻或示例帮助理解。 最后,确保回答覆盖用户提到的所有四个术语,没有遗漏,并对可能的拼写错误进行纠正说明,以提升回答的准确性和帮助性。</think>以下是关于 **树上差分、树链剖分树上分治树上DUS** 的详细介绍,结合它们的核心思想和应用场景: --- ## **1. 树上差分** ### **基本思想** - 树上差分是 **处理树上的区间/路径修改** 的高效方法,类似于数组的差分思想。通过标记路径的起和终,最后通过一次遍历(如DFS)累加标记,得到最终结果。 - **典型场景**:多次对树上的某条路径进行增减操作(如边权或节权值的修改),最后统一查询每个节的最终值。 ### **实现步骤** 1. **路径标记**:对于路径 `u → v`,在差分数组中将 `u` 和 `v` 的权值增加,同时将它们的最近公共祖先(LCA)的权值减少两次(抵消多余贡献)。 2. **后序遍历累加**:通过DFS从叶子到根累加差分值,得到每个节的最终值。 ### **示例问题** - **题目**:给一棵树,多次操作在路径 `u→v` 上所有节权值 +k,最后查询每个节的值。 - **代码片段**: ```python def tree_diff(u, v, lca, k, diff): diff[u] += k diff[v] += k diff[lca] -= k if parent[lca] exists: diff[parent[lca]] -= k ``` --- ## **2. 树链剖分(重链剖分)** ### **基本思想** - 将树分割为若干条 **重链**(Heavy Path),使得树上任意路径的查询/修改可转化为对 `O(log n)` 条链的操作,从而结合线段树等数据结构高效处理。 - **核心概念**: - **重儿子**:子树大小最大的子节- **轻边**:连接非重儿子的边。 - **DFS序**:按遍历顺序将节映射到连续区间,便于线段树维护。 ### **应用场景** - 路径查询:求路径上的最大值、和、或修改路径上的权值。 - 子树查询:子树权值统计。 ### **代码实现(简化步骤)** ```python def dfs(u, parent): size[u] = 1 for v in children[u]: if v != parent: dfs(v, u) size[u] += size[v] if size[v] > size[heavy[u]]: heavy[u] = v # 标记重儿子 def decompose(u, head): top[u] = head # 当前链的顶端 if heavy[u] exists: decompose(heavy[u], head) # 优先处理重儿子(形成重链) for v in light_children[u]: decompose(v, v) # 轻边开启新链 ``` --- ## **3. 树上分治分治/边分治)** ### **基本思想** - **分治**:通过选择树的重心(删除后最大子树最小的节)将树分解为多个子树,递归处理每个子树的路径问题,合并时统计跨重心的路径- **边分治**:类似分治,但选择一条边分割树(较少用,因树可能需重构为二叉树)。 ### **应用场景** - 统计满足条件的路径数量(如路径长度 ≤ k)。 - 经典问题:树的重心分解后处理所有路径的组合。 ### **分治步骤** 1. 找到当前树的重心,删除重心后分裂为多个子树。 2. 计算经过重心的所有路径贡献。 3. 递归处理每个子树。 ### **示例问题** - **题目**:求树上路径长度等于 k 的路径数(LeetCode [437. 路径总和 III](https://leetcode.cn/problems/path-sum-iii/) 的扩展)。 --- ## **4. 树上DUS(Disjoint Set Union,并查集)** ### **基本思想** -树上应用并查集(DSU),通常用于 **离线处理连通性问题** 或 **路径压缩查询**。例如: - **Tarjan离线LCA算法**:通过DFS和并查集,预处理所有查询的最近公共祖先。 - **动态连通性**:处理树的边删除或动态连接。 ### **实现示例(Tarjan求LCA)** ```python def tarjan(u): visited[u] = True for v in children[u]: tarjan(v) union(u, v) # 将子节合并到当前节 for query_v in queries[u]: if visited[query_v]: lca = find(query_v) # 通过并查集找到LCA ``` --- ## **对比与应用场景** | 方法 | 核心用途 | 时间复杂度 | 典型问题 | |--|----------------------------| | 树上差分 | 路径批量修改 + 单次查询 | O(n + m) | 多次路径修改后统计节值 | | 树链剖分 | 路径/子树频繁查询或修改 | O(log n) 单次 | 路径最大值、和 | | 树上分治 | 统计所有满足条件的路径 | O(n log n) | 路径长度等于k的路径数 | | 树上DUS | 离线LCA、连通性问题 | O(n α(n)) | Tarjan算法求LCA | -- 如果需要具体问题的代码实现或进一步解释,可以告诉我场景哦!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值