洛谷传送门
BZOJ传送门
题目描述
傲娇少女幽香正在玩一个非常有趣的战略类游戏,本来这个游戏的地图其实还不算太大,幽香还能管得过来,但是不知道为什么现在的网游厂商把游戏的地图越做越大,以至于幽香一眼根本看不过来,更别说和别人打仗了。
在打仗之前,幽香现在面临一个非常基本的管理问题需要解决。 整个地图是一个树结构,一共有 块空地,这些空地被 条带权边连接起来,使得每两个点之间有一条唯一的路径将它们连接起来。
在游戏中,幽香可能在空地上增加或者减少一些军队。同时,幽香可以在一个空地上放置一个补给站。 如果补给站在点 上,并且空地 上有 个单位的军队,那么幽香每天就要花费 的金钱来补给这些军队。
由于幽香需要补给所有的军队,因此幽香总共就要花费为 ,其中 )的代价。其中 表示 、 在树上的距离(唯一路径的权值和)。
因为游戏的规定,幽香只能选择一个空地作为补给站。在游戏的过程中,幽香可能会在某些空地上制造一些军队,也可能会减少某些空地上的军队,进行了这样的操作以后,出于经济上的考虑,幽香往往可以移动他的补给站从而省一些钱。
但是由于这个游戏的地图是在太大了,幽香无法轻易的进行最优的安排,你能帮帮她吗? 你可以假定一开始所有空地上都没有军队。
输入输出格式
输入格式:
第一行两个数 和 分别表示树的点数和幽香操作的个数,其中点从 到 标号。 接下来 行,每行三个正整数 ,表示 和 之间有一条边权为 的边。 接下来 行,每行两个数 ,表示幽香在点 上放了 单位个军队(如果 ,就相当于是幽香在 上减少了 单位个军队,说白了就是 )。数据保证任何时刻每个点上的军队数量都是非负的。
输出格式:
对于幽香的每个操作,输出操作完成以后,每天的最小花费,也即如果幽香选择最优的补给点进行补给时的花费。
输入输出样例
输入样例#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
说明
对于所有数据, 。非常神奇的是,对于所有数据,这棵树上的点的度数都不超过 ,且 。
解题分析
看到最后一句话多半就知道, 这道题不会存在菊花树之类的情况, 因此遍历与某一节点相连的节点复杂度是有保证的。
再仔细观察题目, 发现其实就是让我们求树上带权重心, 并且支持修改操作。如果没有修改操作显然瞎搞搞就过了, 但如果支持修改的话我们就可以用到动态点分治(也叫点分树)。
点分治和点分树有什么差别呢? 其实就是在每层分治的重心记录下管辖树上的联通块的信息,并通过树上log的深度来保证修改和查询的复杂度, 所谓的树就是每层分治重心和上层重心相连得到的。
在本题中, 因为每个点度数不超过20,我们就可以暴力算出每个点的答案, 比较向哪个方向转移更优, 而转移到子树中递归处理的操作也可以通过爬点分树做到
复杂度。修改带来的一系列影响, 我们也可以在
时间复杂度向上爬点分树来完成。
在这里, 我们设分治重心
管辖联通块内的
值总和为
, 总贡献为
, 那么我们如何计算答案呢?
注意,以下所示的树均为构建出来的分治树,与原树几乎没有关系
假设我们要计算以
为补给站的答案, 那么显然
所在联通块的答案是准确的, 现在我们考虑爬点分树统计答案。我们可以假设将
子树内其它点全部缩在B点上,即将不在
子树内,但在
子树内的
总数求出来,乘上
就行了。但这样做我们忽略了原来已有的一部分贡献, 例如
到
的贡献, 所以我们还要加上原来不在
子树内的贡献。 为了方便计算, 我们可以令
表示分治树上
所在子树在计算
时的贡献。 这样我们在计算
管辖联通块内的总贡献为:
向上爬的时候同理, 只不过再求一遍
。
因为点分树的深度为
层, 所以如果我们使用
一次计算是
复杂度的。又因为我们找到最优点最多在点分树上爬
次, 所以总复杂度为
。
然后简单介绍一下 。 我们可以利用欧拉序, 将树上问题转换为序列问题, 然后利用st表的思想, 预处理, 查询。(感觉什么倍增 都弱爆了有木有)
具体代码如下:
// 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: 我们发现:对于一个节点,当且仅当它的一棵子树中 值总和大于其它所有子树的时候,将重心转移到子树内更优。 所以其实可以 找到最优点, 但博主太菜了, 没调出来QAQ。