「USACO2012Dec」 Running Away From the Barn - 左偏树

本博客介绍了一个基于树形结构的算法问题,旨在解决FJ农场中奶牛的追踪难题。通过构建有根树并运用左偏树与大根堆策略,算法能够高效计算出每个牧场内奶牛可能达到的所有位置。详细解析了算法的设计思路、数据结构的选择与实现代码。

题目描述

又到了FJ农场的挤奶时间了,但是奶牛都跑了!FJ需要把它们全部抓回来,并且需要你的帮助。

FJ的农场是由N(1≤N≤200000)个牧场组成,编号为1到N,由N-1条无向边连通。谷仓位于牧场1,并且从谷仓出发可以到达任何一个牧场。

FJ的奶牛今天早上都在它们的牧场,但没有人知道它们现在跑到哪里了。它们只能往远离谷仓的方向跑,由于它们太懒了,它们最多只能跑不超过L的距离。FJ想知道对于每一个牧场的奶牛能跑到的牧场的个数。

简述题意:给出以1号点为根的一棵有根树,问每个点的子树中与它距离小于等于l的点有多少个。

输入格式

第一行两个整数N和L (1 <= N <= 200,000, 1 <= L <= 10^18)

第2到N行,第i行有两个整数pi和Li, pi (1 <= pi < i)是i到谷仓路径上的第一个牧场,Li(1<=Li<=10^12)是这两个牧场之间的长度。

输出格式

输出共N行,每行一个整数,第i行表示第i个牧场能够到达的牧场的个数。

数据范围

100%的数据1 <= N <= 200,000, 1 <= L <= 10^18

分析

首先可以用暴力的方法,对每个点跑一遍Dfs,可做,但复杂度最坏可到O(n2)O(n^2)O(n2)。想想其他算法?发现可以对每个点开一个左偏树,维护它的子树到这个该点的父亲节点的距离值,大根堆。在Dfs过程中,对于子树,先判断子树的堆里面的最大值是否大于L,若大于则出堆,直到小于等于;而后将该子树与该节点合并,并累加答案。递归完子树后,将该节点的左偏树的值全都加上该点到其父亲节点的距离,可以打标记,合并时下传标记。

这样有点麻烦,可以改为维护到根节点的距离值,子树判断时,条件改为堆顶的值-该节点的值是否大于L。这样就不需要打标记了。

代码

#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
const int N=200005;
typedef long long LL;
int nxt[N<<1],to[N<<1];
int h[N],cnt,n,ans[N],sz[N];
LL l,wei[N<<1],v[N],add[N];
int L[N],R[N],f[N],d[N];
void Add_Edge(int x,int y,LL z) {cnt++,nxt[cnt]=h[x],to[cnt]=y,wei[cnt]=z,h[x]=cnt;}
int Getf(int x) {return f[x]==x?x:f[x]=Getf(f[x]);}//找父亲
void Pushdown(int x) {v[L[x]]+=add[x],v[R[x]]+=add[x],add[L[x]]+=add[x],add[R[x]]+=add[x],add[x]=0;}
//标记下传
int Merge(int x,int y) {//合并子树
    if (!x||!y) return x+y;
    if (v[x]<v[y]) swap(x,y);
    Pushdown(x);
    Pushdown(y);
    R[x]=Merge(R[x],y);
    if (d[L[x]]<d[R[x]]) swap(L[x],R[x]);
    d[x]=d[R[x]]+1;
    return x;
}
void Dfs(int x,int fa,LL wp) {
    int now=x;
    for (int i=h[x];i;i=nxt[i]) {
        int y=to[i];
        if (y==fa) continue;
        Dfs(y,x,wei[i]);
        y=Getf(y);
        while (v[y]>l&&y) {//判断条件
            LL p=add[y],t=sz[y];
            f[y]=Merge(L[y],R[y]);
            f[f[y]]=f[y];
            y=f[y];
            if (f[y]) {
                v[y]+=p;
                add[y]+=p;
                sz[y]=t-1;
            }
        }
        y=Getf(to[i]);
        if (!y) continue;
        int t=sz[now]+sz[y];//合并子树
        f[now]=f[y]=Merge(now,y);
        now=f[now];
        sz[now]=t;
    }
    add[now]+=wp;//标记累加
    v[now]+=wp;
    ans[x]=sz[now];//更新答案 
}
int main() {
    scanf("%d%lld",&n,&l);
    for (int i=2;i<=n;i++) {
        int u;
        LL w;
        scanf("%d%lld",&u,&w);
        Add_Edge(i,u,w);
        Add_Edge(u,i,w);
    }
    d[0]=-1;
    for (int i=1;i<=n;i++) {
        f[i]=i;
        sz[i]=ans[i]=1;
        v[i]=d[i]=0;
    }
    Dfs(1,0,0);
    for (int i=1;i<=n;i++) {
        printf("%d\n",ans[i]);
    }
    return 0;
}
### Feeding the Cows B 问题解析 在《USACO 2022年12月比赛》的 Silver 组第二题中,题目要求解决一个关于奶牛喂养的问题。具体描述如下: 输入包括一个长度为 $ n $ 的字符串,表示一排奶牛,其中每个字符为 'C' 或 'W',分别表示该位置有一头奶牛或是一片草地。目标是通过最少的操作次数,使得每头奶牛('C')都能在其左侧或右侧至少有一个相邻的草地('W'),以便能够被喂养。每次操作可以将一个 'C' 变成 'W' 或者将一个 'W' 变成 'C'。 输出为最小的操作次数,若无法满足条件则输出 -1。 #### 问题分析 1. **问题条件**: - 每个奶牛必须在其左右至少有一个相邻的草地。 - 每次操作可以修改一个字符('C' <-> 'W')。 - 需要找出最小操作次数。 2. **贪心策略**: - 从左到右遍历字符串,当遇到一个奶牛('C')时,检查其右侧是否有一个草地('W'),如果存在,将该草地变为奶牛的“喂养点”。 - 如果右侧没有草地,则需要修改当前奶牛或其右侧的某个字符以满足条件。 3. **实现逻辑**: - 遍历字符串,维护一个指针,标记当前可以使用的草地位置。 - 如果当前字符是 'C',且其右侧没有可用草地,则需要修改一个字符。 - 记录每次操作,确保最终所有奶牛都能被喂养。 #### 示例代码实现 ```cpp #include <iostream> #include <string> using namespace std; int main() { int n; string s; cin >> n >> s; int operations = 0; int lastGrass = -1; for (int i = 0; i < n; ++i) { if (s[i] == 'C') { if (lastGrass == -1 || lastGrass < i - 1) { // Check if there is a grass to the right bool found = false; for (int j = i + 1; j < n; ++j) { if (s[j] == 'W') { lastGrass = j; found = true; break; } } if (!found) { cout << -1 << endl; return 0; } } lastGrass = i + 1; // Mark the next available grass position continue; } else if (s[i] == 'W') { lastGrass = i; } } cout << operations << endl; return 0; } ``` #### 时间复杂度 - 该算法的时间复杂度为 $ O(n) $,因为每个字符最多被访问两次(一次遍历,一次查找草地)。 #### 空间复杂度 - 空间复杂度为 $ O(1) $,仅使用了常量级的额外空间。 #### 注意事项 - 需要处理边界情况,例如字符串末尾没有草地。 - 如果无法满足条件(即存在无法喂养的奶牛),应输出 -1。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值