【洛谷】1600:天天爱跑步【LCA】【开桶】【容斥】【推式子】

P1600 天天爱跑步

题目描述

小c同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。《天天爱跑步》是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。

这个游戏的地图可以看作一一棵包含 n个结点和 n1条边的树, 每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从1n的连续正整数。

现在有m个玩家,第i个玩家的起点为 Si,终点为 Ti 。每天打卡任务开始时,所有玩家在第00秒同时从自己的起点出发, 以每秒跑一条边的速度, 不间断地沿着最短路径向着自己的终点跑去, 跑到终点后该玩家就算完成了打卡任务。 (由于地图是一棵树, 所以每个人的路径是唯一的)

小c想知道游戏的活跃度, 所以在每个结点上都放置了一个观察员。 在结点j的观察员会选择在第Wj秒观察玩家, 一个玩家能被这个观察员观察到当且仅当该玩家在第Wj秒也理到达了结点 j 。 小C想知道每个观察员会观察到多少人?

注意: 我们认为一个玩家到达自己的终点后该玩家就会结束游戏, 他不能等待一 段时间后再被观察员观察到。 即对于把结点j作为终点的玩家: 若他在第Wj秒前到达终点,则在结点j的观察员不能观察到该玩家;若他正好在第Wj秒到达终点,则在结点j的观察员可以观察到这个玩家。

输入输出格式

输入格式:

 

第一行有两个整数n和m 。其中n代表树的结点数量, 同时也是观察员的数量, m代表玩家的数量。

接下来 n1行每行两个整数u和 v,表示结点 u到结点 v有一条边。

接下来一行 n个整数,其中第j个整数为Wj , 表示结点j出现观察员的时间。

接下来 m行,每行两个整数Si,和Ti,表示一个玩家的起点和终点。

对于所有的数据,保证1Si,Tin,0Wjn 。

 

输出格式:

 

输出1行 n个整数,第j个整数表示结点j的观察员可以观察到多少人。

 

输入输出样例

输入样例#1: 复制
6 3
2 3
1 2 
1 4 
4 5 
4 6 
0 2 5 1 2 3 
1 5 
1 3 
2 6 
输出样例#1: 复制
2 0 0 1 1 1 
输入样例#2: 复制
5 3 
1 2 
2 3 
2 4 
1 5 
0 1 0 3 0 
3 1 
1 4
5 5 
输出样例#2: 复制
1 2 1 0 1 

说明

【样例1说明】

对于1号点,Wi=0,故只有起点为1号点的玩家才会被观察到,所以玩家1和玩家2被观察到,共有2人被观察到。

对于2号点,没有玩家在第2秒时在此结点,共0人被观察到。

对于3号点,没有玩家在第5秒时在此结点,共0人被观察到。

对于4号点,玩家1被观察到,共1人被观察到。

对于5号点,玩家1被观察到,共1人被观察到。

对于6号点,玩家3被观察到,共1人被观察到。

【子任务】

每个测试点的数据规模及特点如下表所示。 提示: 数据范围的个位上的数字可以帮助判断是哪一种数据类型。

【提示】

如果你的程序需要用到较大的栈空问 (这通常意味着需要较深层数的递归), 请务必仔细阅读选手日录下的文本当rumung:/stact.p″, 以了解在最终评测时栈空问的限制与在当前工作环境下调整栈空问限制的方法。

在最终评测时,调用栈占用的空间大小不会有单独的限制,但在我们的工作环境中默认会有 8 MB8MB 的限制。 这可能会引起函数调用层数较多时, 程序发生栈溢出崩溃。

我们可以使用一些方法修改调用栈的大小限制。 例如, 在终端中输入下列命令 ulimit -s 1048576

此命令的意义是,将调用栈的大小限制修改为 1GB。

例如,在选手目录建立如下 sample.cpp 或 sample.pas

将上述源代码编译为可执行文件 sample 后,可以在终端中运行如下命令运行该程序

./sample

如果在没有使用命令“ ulimit -s 1048576”的情况下运行该程序, sample会因为栈溢出而崩溃; 如果使用了上述命令后运行该程序,该程序则不会崩溃。

特别地, 当你打开多个终端时, 它们并不会共享该命令, 你需要分别对它们运行该命令。

请注意, 调用栈占用的空间会计入总空间占用中, 和程序其他部分占用的内存共同受到内存限制。


Solution

简直是一个心结!!!终于在考前4天解决了!!

在去年以前被誉为是noip最难的神题了QAQ(然而如今我还是这样认为)

最做不来就是树上开桶+容斥的题,现在来好好分析一下。

对于一条路径,可以求出起点和终点的LCA,将路径分为上行部分和下行部分:

然后对于上行路段的U节点,如果它想观察到这个人,那么显然$dep[u]+w[u]=dep[s]$,同理对于下行路段的V,必须要满足$dep[s]-dep[lca]+dep[v]-dep[lca]=w[v]$,化简得$w[v]-dep[v]=dis[s,t]-dep[t]$。

所以对于每个节点存在两个值$dep[u]+w[u]、w[u]-dep[u]$,对于每条路径存在两个值$dep[s]、dis[s,t]-dep[t]$,想让它们匹配起来,明显开桶即可。

遍历到u节点时,想要知道它可以观察到多少人,首先记录$ans0$,表示之前已经统计出的答案,在这个子树明显不能产生贡献,在最后统计的答案中要减去。

在回溯回来的过程中更新桶和答案,用邻接链表记录下以每个节点作为起点、终点和LCA的路径的标号,方便按照上式快速更新桶中的内容。记录下新的答案。

在最后,以u为LCA的路径就不能对它上面的节点做出贡献了,所以要把多余贡献减去。

最后还要注意,如果一条路径的LCA节点可以观察到它本身,意味着这个点计算了两次贡献,一次上行一次下行,需要减去一次。

其余细节看代码。

Code

#include<bits/stdc++.h>
using namespace std;

int n, m;

const int A = 300000;

struct Point {
    int s, t, len, lca;
} r[300005];

struct Node {
    int v, nex;
} Edge[600005], Edge_st[600005], Edge_ed[600005], Edge_lca[600005];

int h[300005], stot;
void add(int u, int v) {
    Edge[++stot] = (Node) {v, h[u]};
    h[u] = stot;
}

int hst[300005];
void add_st(int u, int id) {
    Edge_st[++stot] = (Node) {id, hst[u]};
    hst[u] = stot;
}

int hed[300005];
void add_ed(int u, int id) {
    Edge_ed[++stot] = (Node) {id, hed[u]};
    hed[u] = stot;
}

int hlca[300005];
void add_lca(int u, int id) {
    Edge_lca[++stot] = (Node) {id, hlca[u]};
    hlca[u] = stot;
}

int dep[300005], jum[300005][21];
void dfs(int u, int f) {
    dep[u] = dep[f] + 1;
    jum[u][0] = f;
    for(int p = 1; p <= 20; p ++)
        jum[u][p] = jum[jum[u][p - 1]][p - 1];
    for(int i = h[u]; i; i = Edge[i].nex) {
        int v = Edge[i].v;
        if(v == f)    continue;
        dfs(v, u);
    }
}

int LCA(int u, int v) {
    if(dep[u] < dep[v])    swap(u, v);
    int t = dep[u] - dep[v];
    for(int p = 0; t; t >>= 1, p ++)
        if(t & 1)    u = jum[u][p];
    if(u == v)    return u;
    for(int p = 20; p >= 0; p --)
        if(jum[u][p] != jum[v][p])    u = jum[u][p], v = jum[v][p];
    return jum[u][0];
}

int ans0[300005], ans[300005], w[300005], t1[600005], t2[600005];
void Dfs(int u, int f) {
    ans0[u] = t1[dep[u] + w[u]] + t2[w[u] - dep[u] + A];///之前的贡献 不算在这个点的范围 
    for(int i = h[u]; i; i = Edge[i].nex) {
        int v = Edge[i].v;
        if(v == f)    continue;
        Dfs(v, u);
    }//////回溯过程中更新答案 
    for(int i = hst[u]; i; i = Edge_st[i].nex)    t1[dep[u]] ++;
    for(int i = hed[u]; i; i = Edge_ed[i].nex)    t2[r[Edge_ed[i].v].len - dep[u] + A] ++;
    ans[u] = t1[dep[u] + w[u]] + t2[w[u] - dep[u] + A];////新增后的贡献(包括原来的) 
    for(int i = hlca[u]; i; i = Edge_lca[i].nex) {
        int id = Edge_lca[i].v;
        t1[dep[r[id].s]] --;
        t2[r[id].len - dep[r[id].t] + A] --;/////以u为lca的所有路径的贡献在回溯回去时都没用了 
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for(int i = 1; i < n; i ++) {
        int u, v;
        scanf("%d%d", &u, &v);
        add(u, v);    add(v, u);
    }
    dfs(1, 0);
    for(int i = 1; i <= n; i ++)    scanf("%d", &w[i]);
    for(int i = 1; i <= m; i ++) {
        int s, t;
        scanf("%d%d", &s, &t);
        r[i].s = s, r[i].t = t;
        r[i].lca = LCA(s, t);
        r[i].len = dep[s] + dep[t] - dep[r[i].lca] * 2;
    }
    stot = 0;
    for(int i = 1; i <= m; i ++)    add_st(r[i].s, i);
    stot = 0;
    for(int i = 1; i <= m; i ++)    add_ed(r[i].t, i);
    stot = 0;
    for(int i = 1; i <= m; i ++)    add_lca(r[i].lca, i);
    Dfs(1, 0);
    for(int i = 1; i <= m; i ++)
        if(dep[r[i].lca] + w[r[i].lca] == dep[r[i].s])    ans[r[i].lca] --;////如果lca可以观察到自己这条路,那它在两段路中多算了一次贡献 
    for(int i = 1; i <= n; i ++)    printf("%d ", ans[i] - ans0[i]);
    return 0;
} 

 

posted @ 2018-11-06 17:34  Wans_ovo  阅读(304)  评论(0编辑  收藏  举报