树链剖分+线段树+离线(广州网选赛第八题hdu5029)

本文深入探讨了信息技术领域的核心内容,包括但不限于前端开发、后端开发、移动开发、游戏开发、大数据开发、开发工具、嵌入式硬件、嵌入式电路知识、嵌入式开发环境、音视频基础、音视频直播流媒体、图像处理AR特效、AI音视频处理、测试、基础运维、DevOps、操作系统、云计算厂商、自然语言处理、区块链、隐私计算、文档协作与知识管理、版本控制、项目管理与协作工具、有监督学习、无监督学习、半监督学习、强化学习、数据安全、数据挖掘、数据结构、算法等。文章详细介绍了各领域的关键概念、技术和应用实例,旨在为读者提供全面深入的技术知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://acm.hdu.edu.cn/showproblem.php?pid=5029

Relief grain

Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 100000/100000 K (Java/Others)
Total Submission(s): 405    Accepted Submission(s): 95


Problem Description
The soil is cracking up because of the drought and the rabbit kingdom is facing a serious famine. The RRC(Rabbit Red Cross) organizes the distribution of relief grain in the disaster area.

We can regard the kingdom as a tree with n nodes and each node stands for a village. The distribution of the relief grain is divided into m phases. For each phases, the RRC will choose a path of the tree and distribute some relief grain of a certain type for every village located in the path.

There are many types of grains. The RRC wants to figure out which type of grain is distributed the most times in every village.
 

Input
The input consists of at most 25 test cases.

For each test case, the first line contains two integer n and m indicating the number of villages and the number of phases.

The following n-1 lines describe the tree. Each of the lines contains two integer x and y indicating that there is an edge between the x-th village and the y-th village.
  
The following m lines describe the phases. Each line contains three integer x, y and z indicating that there is a distribution in the path from x-th village to y-th village with grain of type z. (1 <= n <= 100000, 0 <= m <= 100000, 1 <= x <= n, 1 <= y <= n, 1 <= z <= 100000)

The input ends by n = 0 and m = 0.
 

Output
For each test case, output n integers. The i-th integer denotes the type that is distributed the most times in the i-th village. If there are multiple types which have the same times of distribution, output the minimal one. If there is no relief grain in a village, just output 0.
 

Sample Input
  
2 4 1 2 1 1 1 1 2 2 2 2 2 2 2 1 5 3 1 2 3 1 3 4 5 3 2 3 3 1 5 2 3 3 3 0 0
 

Sample Output
  
1 2 2 3 3 0 2

题意:给出一棵树,然后有一系列的操作,每次操作有a,b,c代表在a到b的路径上的每个点都给一个c类型的数字;最后问所有操作完成后树上的每个节点拥有的数量最多的类型是多少,如果存在多个输出较小类型的数字;

分析:首先这种在树上的操作,很容易想到是树链剖分的题目,但是具体该怎么写呢:先不考虑树的问题,可以把题意抽象出一个普通的连续区间【1,n】,然后在不同区间进行分配东西,最后问每个点的结果,对于区间【L,R】上分配一个数字Z,那么就在L位置上标记+Z,代表从此时以后的每个节点都拥有一个Z,然后在R+1的位置上标记一个-Z,代表从该节点开始没有分配Z这个数字;所以先把m个操作记录下来,对于每个标记的节点,可以用一个vector容器记录所标记的东西,接下来我们可以建立一个权值线段树区间【1,100000】;每个点分别代表数字Z的类型,px数组代表每个类型数字当前出现的次数;tree[i].maxi代表i区间的最大值,从左向右遍历区间【1,n】,对于每个节点更新px的值,如果标记是+Z,则px[z]++;否则px[-z]--;然后query即可;

树链剖分也是同样的道理,轻重链的划分不详说了,对于一个树上的两点<u,v>,把它转化为多条连续的子链利用线段树操作即可;具体程序如下:

#pragma comment(linker, "/STACK:1024000000,1024000000")
#include"stdio.h"
#include"string.h"
#include"iostream"
#include"map"
#include"string"
#include"queue"
#include"stdlib.h"
#include"math.h"
#define M 110009
#define eps 1e-10
#define inf 1000000000
#define mod 1000000000
#define INF 1000000000
using namespace std;
struct node
{
    int u,v,next;
}edge[M*2];
struct Lnode
{
    int v;
    Lnode(int vv)
    {
        v=vv;
    }
};
vector<Lnode>e[M];
int t,pos,head[M],son[M],fa[M],num[M],top[M],ID,deep[M],p[M],fp[M],px[M];
void init()
{
    t=pos=0;
    memset(head,-1,sizeof(head));
    memset(son,-1,sizeof(son));
    memset(fa,-1,sizeof(fa));
}
void add(int u,int v)
{
    edge[t].u=u;
    edge[t].v=v;
    edge[t].next=head[u];
    head[u]=t++;
}
void dfs(int u,int f,int d)
{
    deep[u]=d;
    fa[u]=f;
    num[u]=1;
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(v==f)continue;
        dfs(v,u,d+1);
        num[u]+=num[v];
        if(son[u]==-1||num[son[u]]<num[v])
            son[u]=v;
    }
}
void getpos(int u,int sp)
{
    top[u]=sp;
    p[u]=pos++;
    fp[p[u]]=u;
    if(son[u]==-1)return;
    getpos(son[u],sp);
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(v==fa[u]||v==son[u])continue;
        getpos(v,v);
    }
}
/**********以上是划分轻重链**************/
struct Tree
{
    int l,r,maxi;
}tree[M*4];
void make(int l,int r,int i)//线段树建树操作
{
    tree[i].l=l;
    tree[i].r=r;
    if(l==r)
    {
        tree[i].maxi=0;
        return ;
    }
    int mid=(l+r)/2;
    make(l,mid,i*2);
    make(mid+1,r,i*2+1);
    tree[i].maxi=max(tree[i*2].maxi,tree[i*2+1].maxi);
}
void updata(int p,int q,int i)//单点更新操作
{
    if(tree[i].l==p&&tree[i].r==p)
    {
        tree[i].maxi=q;
        return;
    }
    int mid=(tree[i].l+tree[i].r)/2;
    if(p<=mid)updata(p,q,i*2);
    else updata(p,q,i*2+1);
    tree[i].maxi=max(tree[i*2].maxi,tree[i*2+1].maxi);
}
void query(int l,int r,int i)//区间查找最大值对应的标,若有多个查找编号最小的记录到ID中
{
    if(tree[i].maxi<tree[1].maxi)
        return;
    if(tree[i].l==tree[i].r)
    {
        if(tree[i].l<ID)
            ID=tree[i].l;
        return;
    }
    int mid=(tree[i].l+tree[i].r)/2;
    if(r<=mid)
        query(l,r,i*2);
    else if(l>mid)
        query(l,r,i*2+1);
    else
    {
        query(l,mid,i*2);
        query(mid+1,r,i*2+1);
    }
}
void Insert(int u,int v,int w)//树与线段树的转化操作
{
    int f1=top[u];
    int f2=top[v];
    while(f1!=f2)
    {
        if(deep[f1]<deep[f2])
        {
            swap(f1,f2);
            swap(u,v);
        }
        e[p[f1]].push_back(Lnode(w));
        e[p[u]+1].push_back(Lnode(-w));
        u=fa[f1];
        f1=top[u];
    }
    if(u==v)
    {
        e[p[u]].push_back(Lnode(w));
        e[p[u]+1].push_back(Lnode(-w));
        return;
    }
    if(deep[u]>deep[v])
        swap(u,v);
    e[p[u]].push_back(Lnode(w));
    e[p[v]+1].push_back(Lnode(-w));
    return;
}
int s[M];
int main()
{
    int n,m,i,j,a,b,c,u,v;
    while(scanf("%d%d",&n,&m),m||n)
    {
        init();
        for(i=0;i<=n;i++)
            e[i].clear();
        for(i=1;i<n;i++)
        {
            scanf("%d%d",&u,&v);
            add(u,v);
            add(v,u);
        }
        dfs(1,1,1);
        getpos(1,1);
        for(i=1;i<=m;i++)
        {
            scanf("%d%d%d",&a,&b,&c);
            Insert(a,b,c);
        }
        make(1,100000,1);
        memset(px,0,sizeof(px));
        for(i=0;i<pos;i++)
        {
            for(j=0;j<(int)e[i].size();j++)
            {
                int v=e[i][j].v;
                if(v>0)
                {
                    px[v]++;
                    updata(v,px[v],1);
                }
                else
                {
                    px[-v]--;
                    updata(-v,px[-v],1);
                }
            }
            if(tree[1].maxi==0)
            {
                 s[fp[i]]=0;
                 continue;
            }
            ID=100000;
            query(1,100000,1);
            s[fp[i]]=ID;
            //printf("%d\n",ID);
        }
        for(i=1;i<=n;i++)
            printf("%d\n",s[i]);
    }
    return 0;
}



<think>我们使用树链剖分(重剖分)将树分割成,然后利用DFS序(实际上是剖分后的DFS序)将树结构转化为线性序列,然后使用线段树维护序列上的权值。这样,子树查询就转化为区间查询,节点更新就转化为单点更新。 树链剖分的DFS序:在剖分DFS中,我们优先遍历重儿子,这样保证重上的节点在DFS序中是连续的。同时,每个子树在DFS序中也是连续的(因为DFS遍历子树时是连续的)。因此,子树查询可以转化为区间查询。 步骤: 1. 第一次DFS:计算每个节点的父节点、深度、重儿子、子树大小。 2. 第二次DFS:确定DFS序(时间戳),同时记录每个节点所在的顶端节点(用于路径查询,但本只需要子树查询,所以这一步可以简化,但我们还是按标准剖分来做)。 3. 建立线段树:在DFS序上建立线段树,支持单点更新和区间求和。 子树查询:对于节点u,其子树对应的区间为[in[u], out[u]](即DFS进入和退出的时间戳)。注意:在树链剖分中,由于优先遍历重儿子,子树节点在DFS序中仍然是连续的。 因此,我们可以使用线段树来维护这个区间和。 伪代码(Python风格)如下: ```python import sys sys.setrecursionlimit(200000) class SegmentTree: def __init__(self, data): self.n = len(data) self.size = 1 while self.size < self.n: self.size *= 2 self.tree = [0] * (2 * self.size) # 构建线段树,初始数据 for i in range(self.n): self.tree[self.size + i] = data[i] for i in range(self.size - 1, 0, -1): self.tree[i] = self.tree[2*i] + self.tree[2*i+1] def update(self, index, value): # 单点更新:将位置index的值改为value(注意:这里是直接赋值,如果是增加则需要调整) # 但通常我们支持增加一个差值,这里按需求,我们假设是更新为新的值,所以需要知道旧值?或者我们设计为增加一个增量? # 根据问,节点改变权值,我们可以用增量更新。但为了通用,这里我们实现为单点设置值,但需要知道原值?或者我们设计为传入增量(更符合动态更新)。 # 这里我们实现为增量更新(delta) # index: 原始数组中的位置(0-indexed) pos = index + self.size self.tree[pos] += value # 增加一个增量 while pos > 1: pos //= 2 self.tree[pos] = self.tree[2*pos] + self.tree[2*pos+1] def query(self, l, r): # 区间查询 [l, r] (闭区间) l += self.size r += self.size res = 0 while l <= r: if l % 2 == 1: res += self.tree[l] l += 1 if r % 2 == 0: res += self.tree[r] r -= 1 l //= 2 r //= 2 return res # 树链剖分部分 n = 100000 graph = [[] for _ in range(n+1)] # 第一次DFS:计算父节点、深度、子树大小、重儿子 parent = [0] * (n+1) depth = [0] * (n+1) size = [0] * (n+1) heavy = [-1] * (n+1) # 重儿子,初始化为-1 def dfs1(u, p, d): parent[u] = p depth[u] = d size[u] = 1 max_size = 0 for v in graph[u]: if v == p: continue dfs1(v, u, d+1) size[u] += size[v] if size[v] > max_size: max_size = size[v] heavy[u] = v # 第二次DFS:确定DFS序(时间戳)和重的顶端 head = [0] * (n+1) # 的顶端节点 pos = [-1] * (n+1) # 节点在DFS序中的位置(时间戳) cur_time = 0 def dfs2(u, h): global cur_time head[u] = h pos[u] = cur_time cur_time += 1 # 如果有重儿子,先遍历重儿子 if heavy[u] != -1: dfs2(heavy[u], h) for v in graph[u]: if v == parent[u] or v == heavy[u]: continue dfs2(v, v) # 轻儿子,自己作为新的顶端 # 初始化 def init_tree(root): dfs1(root, 0, 0) dfs2(root, root) # 初始化一个长度为n(节点数)的数组,初始权值,假设为0,或者根据实际输入 arr = [0] * n # 注意:节点从1开始,时间戳从0到n-1 seg_tree = SegmentTree(arr) return seg_tree, pos, head # 返回线段树和位置数组 # 更新节点u的权值(增加delta) def update_node(seg_tree, u, delta): idx = pos[u] # 节点u在线段树中的位置 seg_tree.update(idx, delta) # 查询子树u的权值和:子树u对应的区间为 [pos[u], pos[u]+size[u]-1] ?注意:在树链剖分的DFS序中,子树u的节点在区间[pos[u], pos[u]+size[u]-1]内吗? # 实际上,在第二次DFS中,我们优先遍历重儿子,然后轻儿子。子树u的DFS序区间是连续的,因为递归完子树后才会遍历下一个子树。所以子树u的区间为 [pos[u], pos[u] + size[u] - 1]。 def query_subtree(seg_tree, u): l = pos[u] r = pos[u] + size[u] - 1 return seg_tree.query(l, r) # 注意:这里线段树的区间是[0, n-1],所以直接使用pos[u]和pos[u]+size[u]-1即可。 ``` ### 注意点: - 树链剖分的DFS序保证了每个子树是连续的,所以我们可以用区间[pos[u], pos[u]+size[u]-1]表示子树。 - 线段树支持区间求和和单点更新(增量更新)。如果节点权值改变为新的值,则需要计算增量(新值-旧值)然后更新。 ### 时间复杂度: - 预处理:两次DFS,O(n) - 更新:O(log n) - 查询:O(log n) ### 与树状数组+DFS序的对比: - 树状数组+DFS序(普通DFS序)同样可以做到,且树状数组常数更小,代码更简单。 - 树链剖分的主要优势在于可以同时支持路径查询(而不仅仅是子树查询)。如果问需要查询路径上的权值和,树链剖分就更有优势。 ### 总结: 对于子树查询,树状数组+普通DFS序已经足够高效。树链剖分+线段树也可以实现,但代码稍复杂,但扩展性强(支持路径查询)。本要求子树查询,两种方法均可,但树链剖分在这里有点“大材小用”。 §§相关问§§ 1. 树链剖分中重和轻的作用是什么?为什么优先遍历重儿子? 2. 除了子树查询树链剖分还能支持哪些类型的查询和操作? 3. 如果树的结构是动态变化的(如添加或删除边),如何维护树链剖分? 4. 线段树和树状数组在实现子树查询时各自的优缺点是什么?</think>对于树结构不变、节点数不超过 100,000 的场景,使用 **线段树 + 树链剖分** 是高效支持子树权值查询和节点权值更新的解决方案。树链剖分将树结构转化为线性序列,线段树则提供高效的区间查询和单点更新能力。 ### 解决方案步骤: 1. **树链剖分预处理**: - **第一次 DFS**:计算每个节点的父节点、深度、子树大小(`size[u]`)和重儿子(子树最大的子节点) - **第二次 DFS**:生成 DFS 序(优先遍历重儿子),记录节点入序时间戳 `dfn[u]` 和子树结束时间 `end[u]`(子树节点在 DFS 序中连续) 2. **线段树构建**: - 基于 DFS 序构建线段树,叶子节点存储原始节点权值 - 支持单点更新(节点权值修改)和区间查询(子树权值和) ### 代码实现(Python 风格) ```python import sys sys.setrecursionlimit(200000) class SegmentTree: def __init__(self, n): self.n = n self.size = 1 while self.size < n: self.size *= 2 self.tree = [0] * (2 * self.size) def update(self, index, delta): """单点更新:位置 index 增加 delta""" i = index + self.size self.tree[i] += delta while i > 1: i //= 2 self.tree[i] = self.tree[2*i] + self.tree[2*i+1] def query(self, l, r): """区间查询 [l, r] 的和""" l += self.size r += self.size res = 0 while l <= r: if l % 2 == 1: res += self.tree[l] l += 1 if r % 2 == 0: res += self.tree[r] r -= 1 l //= 2 r //= 2 return res # 树链剖分预处理 n = 100000 # 最大节点数 graph = [[] for _ in range(n+1)] parent = [0] * (n+1) # 父节点 depth = [0] * (n+1) # 深度 size = [0] * (n+1) # 子树大小 heavy = [-1] * (n+1) # 重儿子 dfn = [0] * (n+1) # DFS 入序时间戳 end = [0] * (n+1) # 子树结束时间戳 cur_time = 0 def dfs1(u, p, d): """第一次 DFS:计算 size/heavy""" parent[u] = p depth[u] = d size[u] = 1 max_size = 0 for v in graph[u]: if v == p: continue dfs1(v, u, d+1) size[u] += size[v] if size[v] > max_size: max_size = size[v] heavy[u] = v def dfs2(u, head): """第二次 DFS:生成 DFS 序""" global cur_time dfn[u] = cur_time cur_time += 1 # 优先遍历重儿子 if heavy[u] != -1: dfs2(heavy[u], head) # 重儿子继承当前头 for v in graph[u]: if v == parent[u] or v == heavy[u]: continue dfs2(v, v) # 轻儿子作为新头 end[u] = cur_time - 1 # 子树结束时间 # 初始化树链剖分 def init_tree(root=1): dfs1(root, 0, 0) dfs2(root, root) # 根节点作为第一条头 return SegmentTree(cur_time) # 创建线段树 # 更新节点 u 的权值 (新值 new_val) def update_node(seg_tree, u, new_val, old_val): delta = new_val - old_val seg_tree.update(dfn[u], delta) # 查询子树 u 的权值和 def query_subtree(seg_tree, u): return seg_tree.query(dfn[u], end[u]) # 使用示例 if __name__ == "__main__": # 假设树已构建在 graph 中,root=1 seg_tree = init_tree() # 初始化节点权值 (假设存储在 node_val 数组) for u in range(1, n+1): seg_tree.update(dfn[u], node_val[u]) # 示例:更新节点 5 权值为 10 (旧值假设为 7) update_node(seg_tree, 5, 10, 7) # 示例:查询子树 3 的权值和 print(query_subtree(seg_tree, 3)) ``` ### 时间复杂度分析 | 操作 | 时间复杂度 | 说明 | |--------------|------------|--------------------------| | 树链剖分预处理 | O(n) | 两次 DFS 遍历 | | 单点权值更新 | O(log n) | 线段树单点更新 | | 子树权值查询 | O(log n) | 线段树区间查询 | ### 优势与适用场景 1. **树链剖分优势**: - 将子树查询转化为 **连续区间查询**(`[dfn[u], end[u]]`) - DFS 序连续性由重优先遍历保证 2. **线段树优势**: - O(log n) 高效区间求和 - 支持动态点更新 3. **扩展性**: - 可扩展支持路径查询(通过跳查询) - 可支持其他聚合操作(最大值、最小值等) 此方案在节点数 100,000 时完全可行,预处理 O(n)每次操作 O(log n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值