NOIP2016提高组 天天爱跑步

(树上差分 + \(LCA\)) \(O(Mlog_2N)\)


调了两个小时,最后发现把\(lca\)里的\(y\)写成\(x\)了,当场去世。

首先下几个定义:

  1. \(dis[x]\)\(x\)到根节点的距离。由于边权都是\(1\),所以\(dis[x] = dep[x]\)
  2. \(LCA(x, y)\)\(x, y\) 的最近公共祖先
  3. \(LCA(x, y)\ down\)\(x, y\) 的最近公共祖先在往\(y\)的放下下去一格(这里不懂可以看下面的图)
  4. \(ans[x]\) 为这个点的答案
  5. 我们称玩家跑的路线为"一条路径"

对于第\(i\)个玩家,发现可以将从\(S_i\)\(T_i\)的路径分成两条链:

  1. \(S_i\)\(LCA(S_i, T_i)\)
  2. \(LCA(S_i, T_i)\ down\)\(T_i\) (这里不算\(LCA\),不然会重复两次 )

不太理解的同学看这张图,设\(S_i = 6, T_i = 8\)。其中$ LCA(6, 8)\ down = 5$

我们从每条路线对答案的贡献来看,进行分类讨论(最后的答案就是两个之和):

从起点出发,终点在第一条链上(上升链):

考虑一个玩家\(i\)对答案的贡献:

这条玩家的起点为\(S_i\) ,终点为 \(LCA(S_i, T_i)\)

考虑它对答案的贡献只存在于这条链上(因为它只走过这条链上的点):

设在这条链上存在一点\(x\),且到\(x\)点刚好为\(W_x\)秒,则有:

\(dis[S_i] - dis[x] = W_x\)

转换一下(把能从\(x\)直接得到的放在一边,需要统计的放在另一边):

\(dis[x] + W_x = dis[S_i]\)

考虑一个点得到的答案:

也就是说,当存在一个路径\(S_y, T_y\)使得:

\(dis[x] + W_x = dis[S_y]\)的时候,就看见了一个人\(ans[x]++\)


那我们只要建立一个数组\(d1\) , \(d1[x][a]\) 表示以x为节点的答案统计中, \(a\)这个值出现了多少次。

那么,\(ans[x] = d1[x][W_x]\)


但注意了,这条线到 \(LCA(S_y, T_y)\) 会拐弯,\(y\)这个人对 \(LCA(S_i, T_i)\)的上面的节点答案是没有贡献的。


我们总结一下:

对于一条线的上升部分的处理,我们需要:

对于\(S_i\)\(LCA(S_i, T_i)\) 这条链上的所有点\(b\),让\(d1[b][dis[S_i]]++\)

这不就是树上差分吗?

我们还发现\(d1\)的第一维可以滚动掉,然后用一遍\(dfs\)动态维护这个次数。

从起点出发,终点在第二条链上(下降链):

这里,我们可以用第一条链的思路进行思考,只不过改变一下细节。

考虑一个玩家\(i\)对答案的贡献:

这条玩家的起点为\(LCA(S_i, T_i)\ down\) ,终点为 \(T_i\)

考虑它对答案的贡献只存在于这条链上(因为它只走过这条链上的点):

设在这条链上存在一点\(x\),且到\(x\)点刚好为\(W_x\)秒,则有:

\(dis[S_i] + dis[x] - 2 * dis[LCA(S_i, T_i)] = W_x\)

转换一下:(把能从\(x\)直接得到的放在一边,需要统计的放在另一边):

\(W_x - dis[x] = dis[S_i] - 2 * dis[LCA(S_i, T_i)]\)

考虑一个点得到的答案:

也就是说,当存在一个条路径\(S_y,\)使得:

\(W_x - dis[x] = dis[S_y] - 2 * dis[LCA(S_y, T_y)]\)的时候,就看见了一个人\(ans[x]++\)


我们建立一个数组\(d2\) , \(d2[x][a]\) 以x为节点的答案统计中, \(a\)这个值出现了多少次。

那么,\(ans[x] = d2[x][W_x]\)


我们总结一下:

对于一条线的下降部分的处理,我们需要:

对于\(LCA(S_i, T_i)\ down\)\(T_i\) 的这条链上的所有点\(b\),让\(d2[b][dis[S_i] - 2 * dis[LCA(S_i, T_i)]]++\)

同理,我们这里也可以使用树上差分。

这里需要注意的一点是:\(dis[S_i] - 2 * dis[LCA(S_i, T_i)]\) 可能是负数,为了不让数组越界,我们可以加一个偏移量

注意事项

这种特殊(需要二维数组,滚动数组优化后)的树上差分如下:

  1. 存下所有点的操作序列。
  2. 每次到一个点的时候,把它的所有操作执行一遍。
  3. 注意一个特殊的点,这里可能会统计掉其他子树(旁支),所以只需开始进入的时候存一个,回溯的时候存一个,两数相减即为答案。

代码实现(我用的是倍增求LCA):

#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 300005, M = N << 1, L = 19;
int n, m, fa[N][L], dis[N];

//d1的下表值域是(0 ~ 2n)的
int d1[N << 1], d2[N << 1], ans[N], w[N];

//op1, op2 分别是 d1, d2 的操作序列
//Pair(下表的值,要加的次数)
vector<PII> op1[N], op2[N]; 

//链式前向星
int head[N], numE = 0;
struct Edge{
    int next, to;
}e[M];
void addEdge(int from, int to){
    e[++numE].next = head[from];
    e[numE].to = to;
    head[from] = numE;
}
//预处理dfs
void dfs_(int u, int last){
    fa[u][0] = last;
    for(int i = 1; fa[u][i - 1]; i++)
        fa[u][i] = fa[fa[u][i - 1]][i - 1];
    for(int i = head[u]; i; i = e[i].next){
        int v = e[i].to;
        if(v == last) continue;
        dis[v] = dis[u] + 1;
        dfs_(v, u);
    }
}
//倍增求LCA
int lca(int x, int y){
    if(dis[x] < dis[y]) swap(x, y);
    for(int i = L - 1; ~i; i--)
        if(dis[x] - (1 << i) >= dis[y])
            x = fa[x][i];
    if(x == y) return x;
    for(int i = L - 1; ~i; i--)
        if(fa[x][i] != fa[y][i])
            x = fa[x][i], y = fa[y][i];
    return fa[x][0];
}
//添加操作序列
void update(int s, int t){
    int p = lca(s, t);
    op1[s].push_back(make_pair(dis[s], 1));
    op1[fa[p][0]].push_back(make_pair(dis[s], -1));
    op2[t].push_back(make_pair(dis[s] - 2 * dis[p] + n, 1));
    op2[p].push_back(make_pair(dis[s] - 2 * dis[p] + n, -1));
}
//最后一遍dfs求答案
void dfs(int u, int last){
    //v1, v2 就是我们寻找的值
    int v1 = w[u] + dis[u], v2 = w[u] - dis[u] + n;
    int res1 = d1[v1], res2 = d2[v2];
    for(int i = head[u]; i; i = e[i].next){
        int v = e[i].to;
        if(v == last) continue;
        dfs(v, u);
    }
    //加入操作
    for(int i = 0; i < op1[u].size(); i++)
        d1[op1[u][i].first] += op1[u][i].second;
    for(int i = 0; i < op2[u].size(); i++)
        d2[op2[u][i].first] += op2[u][i].second;
    ans[u] = (d1[v1] - res1) + (d2[v2] - res2);
}
int main(){
    scanf("%d%d", &n, &m);
    for(int i = 1; i < n; i++){
        int u, v; scanf("%d%d", &u, &v);
        addEdge(u, v); addEdge(v, u);
    }

    for(int i = 1; i <= n; i++)
        scanf("%d", w + i);
    dfs_(1, 0);
    for(int i = 1; i <= m; i++){
        int s, t; scanf("%d%d", &s, &t);
        update(s, t);
    }
    dfs(1, 0);
    for(int i = 1; i <= n; i++)
        printf("%d ", ans[i]);
    return 0;
}

鸣谢:

  1. 秦dalao的讲义
  2. MrWriter 画图软件
  3. SM.MS 的图床
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本火锅店点餐系统采用Java语言和Vue技术,框架采用SSM,搭配Mysql数据库,运行在Idea里,采用小程序模式。本火锅店点餐系统提供管理员、用户两种角色的服务。总的功能包括菜品的查询、菜品的购买、餐桌预定和订单管理。本系统可以帮助管理员更新菜品信息和管理订单信息,帮助用户实现在线的点餐方式,并可以实现餐桌预定。本系统采用成熟技术开发可以完成点餐管理的相关工作。 本系统的功能围绕用户、管理员两种权限设计。根据不同权限的不同需求设计出更符合用户要求的功能。本系统中管理员主要负责审核管理用户,发布分享新的菜品,审核用户的订餐信息和餐桌预定信息等,用户可以对需要的菜品进行购买、预定餐桌等。用户可以管理个人资料、查询菜品、在线点餐和预定餐桌、管理订单等,用户的个人资料是由管理员添加用户资料时产生,用户的订单内容由用户在购买菜品时产生,用户预定信息由用户在预定餐桌操作时产生。 本系统的功能设计为管理员、用户两部分。管理员为菜品管理、菜品分类管理、用户管理、订单管理等,用户的功能为查询菜品,在线点餐、预定餐桌、管理个人信息等。 管理员负责用户信息的删除和管理,用户的姓名和手机号都可以由管理员在此功能里看到。管理员可以对菜品的信息进行管理、审核。本功能可以实现菜品的定时更新和审核管理。本功能包括查询餐桌,也可以发布新的餐桌信息。管理员可以查询已预定的餐桌,并进行审核。管理员可以管理公告和系统的轮播图,可以安排活动。管理员可以对个人的资料进行修改和管理,管理员还可以在本功能里修改密码。管理员可以查询用户的订单,并完成菜品的安排。 当用户登录进系统后可以修改自己的资料,可以使自己信息的保持正确性。还可以修改密码。用户可以浏览所有的菜品,可以查看详细的菜品内容,也可以进行菜品的点餐。在本功能里用户可以进行点餐。用户可以浏览没有预定出去的餐桌,选择合适的餐桌可以进行预定。用户可以管理购物车里的菜品。用户可以管理自己的订单,在订单管理界面里也可以进行查询操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值