[Luogu P3345] [BZOJ 3924] [ZJOI2015]幻想乡战略游戏

洛谷传送门

BZOJ传送门

题目描述

傲娇少女幽香正在玩一个非常有趣的战略类游戏,本来这个游戏的地图其实还不算太大,幽香还能管得过来,但是不知道为什么现在的网游厂商把游戏的地图越做越大,以至于幽香一眼根本看不过来,更别说和别人打仗了。

在打仗之前,幽香现在面临一个非常基本的管理问题需要解决。 整个地图是一个树结构,一共有 n 块空地,这些空地被 n 1 条带权边连接起来,使得每两个点之间有一条唯一的路径将它们连接起来。

在游戏中,幽香可能在空地上增加或者减少一些军队。同时,幽香可以在一个空地上放置一个补给站。 如果补给站在点 u 上,并且空地 v 上有 d v 个单位的军队,那么幽香每天就要花费 d v d i s t ( u ,   v ) 的金钱来补给这些军队。

由于幽香需要补给所有的军队,因此幽香总共就要花费为 v ( d v d i s t ( u ,   v ) ,其中 1 V N )的代价。其中 d i s t ( u ,   v ) 表示 u v 在树上的距离(唯一路径的权值和)。

因为游戏的规定,幽香只能选择一个空地作为补给站。在游戏的过程中,幽香可能会在某些空地上制造一些军队,也可能会减少某些空地上的军队,进行了这样的操作以后,出于经济上的考虑,幽香往往可以移动他的补给站从而省一些钱。

但是由于这个游戏的地图是在太大了,幽香无法轻易的进行最优的安排,你能帮帮她吗? 你可以假定一开始所有空地上都没有军队。

输入输出格式

输入格式:

第一行两个数 n Q 分别表示树的点数和幽香操作的个数,其中点从 1 n 标号。 接下来 n 1 行,每行三个正整数 a , b , c ,表示 a b 之间有一条边权为 c 的边。 接下来 Q 行,每行两个数 u , e ,表示幽香在点 u 上放了 e 单位个军队(如果 e < 0 ,就相当于是幽香在 u 上减少了 | e | 单位个军队,说白了就是 d u d u + e )。数据保证任何时刻每个点上的军队数量都是非负的。

输出格式:

对于幽香的每个操作,输出操作完成以后,每天的最小花费,也即如果幽香选择最优的补给点进行补给时的花费。

输入输出样例

输入样例#1:

10 5
1 2 1
2 3 1
2 4 1
1 5 1
2 6 1
2 7 1
5 8 1
7 9 1
1 10 1
3 1
2 1
8 1
3 1
4 1

输出样例#1:

0
1
4
5
6

说明

对于所有数据, 1 c 1000 , 0 | e | 1000 , n 10 5 , Q 10 5 。非常神奇的是,对于所有数据,这棵树上的点的度数都不超过 20 ,且 N Q 1

解题分析

看到最后一句话多半就知道, 这道题不会存在菊花树之类的情况, 因此遍历与某一节点相连的节点复杂度是有保证的。
再仔细观察题目, 发现其实就是让我们求树上带权重心, 并且支持修改操作。如果没有修改操作显然瞎搞搞就过了, 但如果支持修改的话我们就可以用到动态点分治(也叫点分树)。
点分治和点分树有什么差别呢? 其实就是在每层分治的重心记录下管辖树上的联通块的信息,并通过树上log的深度来保证修改和查询的复杂度, 所谓的树就是每层分治重心和上层重心相连得到的。
在本题中, 因为每个点度数不超过20,我们就可以暴力算出每个点的答案, 比较向哪个方向转移更优, 而转移到子树中递归处理的操作也可以通过爬点分树做到 l o g ( N ) 复杂度。修改带来的一系列影响, 我们也可以在 l o g ( N ) 时间复杂度向上爬点分树来完成。
在这里, 我们设分治重心 i 管辖联通块内的 d 值总和为 d s i , 总贡献为 u p i , 那么我们如何计算答案呢?

注意,以下所示的树均为构建出来的分治树,与原树几乎没有关系

扫描二维码关注公众号,回复: 1732055 查看本文章

假设我们要计算以 A 为补给站的答案, 那么显然 A 所在联通块的答案是准确的, 现在我们考虑爬点分树统计答案。我们可以假设将 B 子树内其它点全部缩在B点上,即将不在 A 子树内,但在 B 子树内的 d 总数求出来,乘上 d i s t ( B ,   A ) 就行了。但这样做我们忽略了原来已有的一部分贡献, 例如 C B 的贡献, 所以我们还要加上原来不在 A 子树内的贡献。 为了方便计算, 我们可以令 d o w n i 表示分治树上 A 所在子树在计算 u p f a t i 时的贡献。 这样我们在计算 B 管辖联通块内的总贡献为:

t o t = u p A + ( d s B d s A ) d i s t ( A ,   B ) + u p B d o w n A

向上爬的时候同理, 只不过再求一遍 d i s t ( A ,   X )
因为点分树的深度为 l o g 层, 所以如果我们使用 R M Q   L C A 一次计算是 l o g ( N ) 复杂度的。又因为我们找到最优点最多在点分树上爬 l o g ( N ) 次, 所以总复杂度为 l o g 2 ( N )

然后简单介绍一下 R M Q   L C A 。 我们可以利用欧拉序, 将树上问题转换为序列问题, 然后利用st表的思想, N l o g ( N ) 预处理, O ( 1 ) 查询。(感觉什么倍增 L C A 都弱爆了有木有)

具体代码如下:

// luogu-judger-enable-o2
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <cctype>
#include <cstdlib>
#define R register
#define IN inline
#define W while
#define gc getchar()
#define ll long long
#define MX 200005
bool fu;
template <class T>
IN void in(T &x)
{
    fu = false;
    x = 0; R char c = gc;
    W (!isdigit(c)) 
    {if(c == '-') fu = true; c = gc;}
    W (isdigit(c))
    x = (x << 1) + (x << 3) + c - 48, c = gc;
    if(fu) x = -x;
}
struct Edge
{
    int to, nex, len;
}edge[MX << 1], G[MX << 1];
int dot, q, line, root, cot, cnt, deal;
int fat[MX], RMQ[20][MX << 1], eul[MX], fir[MX], lg[MX], dep[MX], head[MX], dist[MX];
int mx[MX], siz[MX], gnex[MX];
bool vis[MX];
ll ds[MX], up[MX], down[MX];
namespace LCA
{
    void DFS(const int &now, const int &fa)
    {
        fir[eul[++cot] = now] = cot;//fir存第一次dfs到当前点的编号
        for (R int i = head[now]; i; i = edge[i].nex)
        {
            if(edge[i].to == fa) continue;
            dist[edge[i].to] = dist[now] + edge[i].len;
            dep[edge[i].to] = dep[now] + 1;
            DFS(edge[i].to, now);
            eul[++cot] = now;
        }
    }
    IN void add(const int &from, const int &to, const int &len)
    {//原树加边
        edge[++cnt] = {to, head[from], len};
        head[from] = cnt;
    }
    IN void addedge(const int &from, const int &to, const int &nex)
    {//点分树加边
        G[++cnt] = {to, gnex[from], nex};
        gnex[from] = cnt;
    }
    void get_log()
    {//预处理log2大小
        lg[1] = 0;
        for (R int i = 2; i <= cot; ++i) 
        lg[i] = lg[i >> 1] + 1;
    }
    void get_st()
    {//类似st表预处理出LCA
        int bd, step;
        for (R int i = 1; i <= cot; ++i) RMQ[0][i] = eul[i];
        for (R int i = 1; i <= 18; ++i)
        {
            bd  = cot - (1 << i) + 1;
            step = 1 << i - 1;
            for (R int j = 1; j <= bd; ++j) RMQ[i][j] = dep[RMQ[i - 1][j]] < dep[RMQ[i - 1][j + step]] ? RMQ[i - 1][j] : RMQ[i - 1][j + step];
        }
    }
    IN int query(const int &x, const int &y)
    {
        int lef = fir[x], rig = fir[y];
        if(lef > rig) std::swap(lef, rig);
        int step = lg[rig - lef + 1], comb;
        if(dep[RMQ[step][lef]] > dep[RMQ[step][rig - (1 << step) + 1]]) comb = dist[RMQ[step][rig - (1 << step) + 1]];
        else comb = dist[RMQ[step][lef]];
        return dist[x] + dist[y] - (comb << 1);
    }
}
namespace Dot_Divide
{
    void getroot(const int &now, const int &fa)
    {
        siz[now] = 1; mx[now] = 0;
        for (R int i = head[now]; i; i = edge[i].nex)
        {
            if(edge[i].to == fa || vis[edge[i].to]) continue;
            getroot(edge[i].to, now);
            siz[now] += siz[edge[i].to];
            if(mx[now] < siz[edge[i].to]) mx[now] = siz[edge[i].to];
        }
        mx[now] = std::max(mx[now], deal - siz[now]);
        if(mx[now] < mx[root]) root = now;
    }
    void build(int now, int fa)//建立点分树
    {
        vis[now] = true; fat[now] = fa;
        for (R int i = head[now]; i; i = edge[i].nex)
        {
            if(vis[edge[i].to]) continue;
            deal = siz[edge[i].to], root = 0, mx[0] = siz[edge[i].to];
            getroot(edge[i].to, 0); LCA::addedge(now, root, edge[i].to);
            build(root, now);
        }
    }
    IN void modify(const int &now, const int &del)//修改操作
    {
        R int dist;
        ds[now] += del;
        for (R int i = now; fat[i]; i = fat[i])
        {
            dist = LCA::query(fat[i], now);
            up[fat[i]] += 1ll * dist * del;
            down[i] += 1ll * dist * del;
            ds[fat[i]] += del;
        }
    }
    IN ll cal(const int &now)
    {
        R int dis;
        ll ans = up[now];
        for (R int i = now; fat[i]; i = fat[i])
        {
            dis = LCA::query(fat[i], now);
            ans += up[fat[i]] - down[i];
            ans += (ds[fat[i]] - ds[i]) * dis;
        }
        return ans;
    }
    ll query(const int &now)
    {
        ll ans = cal(now);
        for (R int i = gnex[now]; i; i = G[i].nex)
        if(cal(G[i].len) < ans) return query(G[i].to);
        return ans;
    }
}
int main(void)
{
    int a, b, c;
    in(dot), in(q);
    for (R int i = 1; i < dot; ++i)
    {
        in(a), in(b), in(c);
        LCA::add(a, b, c), LCA::add(b, a, c);
    }
    cnt = 0;//别忘重置边的技术, 否则会RE
    mx[0] = dot;
    LCA::DFS(1, 0);
    LCA::get_log();
    LCA::get_st();
    LCA::query(2, 4);
    deal = dot;
    Dot_Divide::getroot(1, 0);
    int rt = root;
    Dot_Divide::build(rt, 0);
    root = rt;
    W (q--)
    {
        in(a), in(b);
        Dot_Divide::modify(a, b);
        printf("%lld\n", Dot_Divide::query(root));
    }
    return 0;
}

PS: 我们发现:对于一个节点,当且仅当它的一棵子树中 d 值总和大于其它所有子树的时候,将重心转移到子树内更优。 所以其实可以 O ( l o g ( N ) ) 找到最优点, 但博主太菜了, 没调出来QAQ。

猜你喜欢

转载自blog.csdn.net/lpa20020220/article/details/80640405
今日推荐