【洛谷3345_BZOJ3924】[ZJOI2015]幻想乡战略游戏(点分树)
大概有整整一个月没更博客了 ……
4 月为省选爆肝了一个月,最后压线进 B 队,也算给 NOIP2018 翻车到 316 分压线省一这个折磨了五个月的 debuff 画上了一个不算太差的句号。结果省选后技能点全点到红警上了,OI 迅速变菜,GG 。
题目:
分析:
为什么我觉得这题网上大部分题解都讲的很麻烦,看了一上午还没看懂,有一种被拐到沟里的感觉 …… 我这个思路自认为比较好理解。
先考虑一个比较弱的问题:把原问题的「寻找最优补给站」改为「每次询问钦定一个补给站,求此时的花费」。
这很明显是个点分树裸题,建议先充分理解 BZOJ3730 震波(此处应有本人博客链接,无限咕咕中)。非常类似于震波的做法,对每个结点 \(u\) 维护 \(sum_u\) 、 \(sumd_u\) 、 \(sumf_u\) 、 \(sumdf_u\) ,分别表示点分树上结点 \(u\) 「管辖范围」(可以理解为点分治时 get_path 函数遍历的那棵子树)中的点权之和、「点权乘深度(到根节点的边权之和,下同)」之和、对点分树上父节点贡献的点权之和、对点分树上父节点贡献的「点权乘深度」之和。
P.S. 写博客的时候突然发现 \(sum_u\) 和 \(sumf_u\) 其实是同一个东西,但我还是要为了「对称美」以及套震波的板子把它们分开了(滑稽)。
代码如下,如果充分理解了 BZOJ3730 的做法应该很好懂。代码中用 \(sum_{u+n}\) 和 \(sumd_{u+n}\) 表示 \(sumf_u\) 和 \(sumdf_u\) 。用 ST 表求 LCA ,单次修改和查询都是 \(O\left(\log n\right)\) 的。
void modify(const int u, const int x)
{
using LCA::get_dis;
int tmp = u;
wtot += x;
d[u] += x;
sum[u] += x;
while (fa[tmp])
{
int d = get_dis(u, fa[tmp]);
sum[fa[tmp]] += x;
sum[tmp + n] += x;
sumd[fa[tmp]] += (ll)x * d;
sumd[tmp + n] += (ll)x * d;
tmp = fa[tmp];
}
}
ll query(const int u)
{
using LCA::get_dis;
int tmp = u;
ll ans = sumd[u];
while (fa[tmp])
{
int d = get_dis(u, fa[tmp]);
ans += sumd[fa[tmp]] - sumd[tmp + n] + (ll)d * (sum[fa[tmp]] - sum[tmp + n]);
tmp = fa[tmp];
}
return ans;
}
暂时忘掉点分树(现在它的作用只是在 \(O(\log n)\) 的时间内求把补给站设在某个点的答案),只想原树,考虑一种贪心的做法:先随便站在一个点上,称为 \(u\) 。\(v\) 是一个与 \(u\) 的一个直接相连的结点(以下称为「儿子」)。如果 \(v\) 的答案比 \(u\) 更优,则从 \(u\) 走到 \(v\) 。如此往复,最终到一个无法再走的点,那么这个点就是最优点。口胡的不严谨证明如下:
用 \(sum_v\) 表示以 \(u\) 为根时 \(v\) 子树中点的点权之和, \(tot\) 表示全部 \(n\) 个点的点权之和,\(w\) 是边 \((u,v)\) 的权。那么,从 \(u\) 走到 \(v\) 后 \(v\) 子树中的所有点 \(p\) 的贡献减少 \(w\cdot d_p\) ,\(v\) 子树外的所有点 \(q\) 的贡献增加 \(w\cdot d_q\) ,那么答案变化量 \(\Delta=w\cdot (tot-sum_v)-w\cdot sum_v=w\cdot (tot-2sum_v)\) 。也就是说,只有当 \(2sum_v>tot\) ,答案才会减少,即 \(v\) 的答案比 \(u\) 更优。很明显,\(u\) 最多只能有一个儿子 \(v\) 满足 \(2sum_v>tot\) ,所以如果能移动,一定只有唯一的一种移动方案。并且,如果 \(2sum_v>tot\) ,则 \(2(tot-sum_v)\) (即以 \(v\) 为根时的 \(sum_u\) 的两倍)一定不大于 \(tot\) ,所以不可能往回移动。综上,这样一定能找到最优解。
如果专门造数据卡,上述贪心每次最多能走 \(n-1\) 步(考虑一条长链,中间全是 \(0\) ,两端轮流在 \(1\) 和 \(0\) 之间切换,最优解轮流出现在两个端点上),单次修改最坏 \(O(n\log n)\) ,会 TLE 。然而,这个贪心给我们一个重要的启示:对于相邻两点 \(u\) 和 \(v\) ,如果 \(u\) 比 \(v\) 优,那么答案 一定在 \(v\) 的子树中 (以 \(u\) 为树根)。换句话说,就是如果断掉边 \((u,v)\) ,则 答案一定在 \(v\) 所在的连通块中 。也就是说,我即使现在不走到 \(v\) ,只要走到(更形象地说,「跳到」) \(v\) 的子树中任意一点,也都能保证最终找到最优解。
那么我们每次不是走到 \(v\) ,而是走到 \(v\) 这棵子树的「重心」。从点分树的角度来说,记 \(near_v\) 表示从 \(v\) 的点分树父亲 \(u\) 到 \(v\) 的原树路径上除了 \(u\) 以外的第一个点。一开始站在根上,从 \(u\) 走到 \(v\) 的条件是 \(near_v\) 比 \(u\) 更优,最终无路可走了就是答案。由于点分树深度是 \(O(\log n)\) ,每次最多走深度步,每走一步要 \(O(\log n)\) 查询若干个点的答案,所以单次查询时间复杂度为 \(O(\log^2 n)\) (由于要遍历所有儿子,所以要乘上最大度数 \(20\) 的常数)。
代码:
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <vector>
using namespace std;
namespace zyt
{
template<typename T>
inline bool read(T &x)
{
char c;
bool f = false;
x = 0;
do
c = getchar();
while (c != EOF && c != '-' && !isdigit(c));
if (c == EOF)
return false;
if (c == '-')
f = true, c = getchar();
do
x = x * 10 + c - '0', c = getchar();
while (isdigit(c));
if (f)
x = -x;
return true;
}
template<typename T>
inline void write(T x)
{
static char buf[20];
char *pos = buf;
if (x < 0)
putchar(' '), x = -x;
do
*pos++ = x % 10 + '0';
while (x /= 10);
while (pos > buf)
putchar(*--pos);
}
typedef long long ll;
const int N = 1e5 + 10, B = 20, INF = 0x3f3f3f3f;
struct edge
{
int to, w, next;
}e[N << 1];
int n, head[N], ecnt, d[N];
void add(const int a, const int b, const int c)
{
e[ecnt] = (edge){b, c, head[a]}, head[a] = ecnt++;
}
namespace LCA
{
int dis[N], dfn[N], euler[N << 1], dfncnt;
namespace ST
{
int lg2[N << 1], st[B][N << 1];
const int *w;
int min(const int a, const int b)
{
return w[a] < w[b] ? a : b;
}
void build(const int *_w, const int n)
{
w = _w;
int tmp = 0;
for (int i = 1; i <= n; i++)
{
lg2[i] = tmp;
if (i == (1 << (tmp + 1)))
++tmp;
}
for (int i = 1; i <= n; i++)
st[0][i] = i;
for (int i = 1; i < B; i++)
for (int j = 1; j + (1 << i) - 1 <= n; j++)
st[i][j] = min(st[i - 1][j], st[i - 1][j + (1 << (i - 1))]);
}
int query(const int l, const int r)
{
int len = lg2[r - l + 1];
return min(st[len][l], st[len][r - (1 << len) + 1]);
}
}
void dfs(const int u, const int f)
{
dfn[u] = ++dfncnt;
euler[dfncnt] = u;
for (int i = head[u]; ~i; i = e[i].next)
{
int v = e[i].to;
if (v == f)
continue;
dis[v] = dis[u] + e[i].w;
dfs(v, u);
euler[++dfncnt] = u;
}
}
void init()
{
dfncnt = 0;
dfs(1, 0);
ST::build(euler, dfncnt);
}
int lca(const int a, const int b)
{
return euler[ST::query(min(dfn[a], dfn[b]), max(dfn[a], dfn[b]))];
}
int get_dis(const int a, const int b)
{
return dis[a] + dis[b] - (dis[lca(a, b)] << 1);
}
}
namespace Point_Divide_Tree
{
int f[N], near[N], rot, size[N], tot, fa[N];
ll sum[N << 1], sumd[N << 1], wtot;
bool vis[N];
vector<int> g[N];
void find_rot(const int u, const int fa)
{
size[u] = 1, f[u] = 0;
for (int i = head[u]; ~i; i = e[i].next)
{
int v = e[i].to;
if (vis[v] || v == fa)
continue;
find_rot(v, u);
size[u] += size[v];
f[u] = max(f[u], size[v]);
}
f[u] = max(f[u], tot - size[u]);
if (f[u] < f[rot])
rot = u;
}
int get_size(const int u, const int f)
{
int ans = 1;
for (int i = head[u]; ~i; i = e[i].next)
{
int v = e[i].to;
if (v == f || vis[v])
continue;
ans += get_size(v, u);
}
return ans;
}
void solve(const int u)
{
vis[u] = true;
for (int i = head[u]; ~i; i = e[i].next)
{
int v = e[i].to;
if (vis[v])
continue;
tot = get_size(v, u);
f[0] = INF, rot = 0;
find_rot(v, u);
fa[rot] = u, g[u].push_back(rot);
near[rot] = v;
solve(rot);
}
}
void modify(const int u, const int x)
{
using LCA::get_dis;
int tmp = u;
wtot += x;
d[u] += x;
sum[u] += x;
while (fa[tmp])
{
int d = get_dis(u, fa[tmp]);
sum[fa[tmp]] += x;
sum[tmp + n] += x;
sumd[fa[tmp]] += (ll)x * d;
sumd[tmp + n] += (ll)x * d;
tmp = fa[tmp];
}
}
ll query(const int u)
{
using LCA::get_dis;
int tmp = u;
ll ans = sumd[u];
while (fa[tmp])
{
int d = get_dis(u, fa[tmp]);
ans += sumd[fa[tmp]] - sumd[tmp + n] + (ll)d * (sum[fa[tmp]] - sum[tmp + n]);
tmp = fa[tmp];
}
return ans;
}
ll find(const int u)
{
ll now = query(u);
for (vector<int>::iterator it = g[u].begin(); it != g[u].end(); it++)
{
int v = *it;
if (query(near[v]) < now)
return find(v);
}
return now;
}
}
int work()
{
using namespace Point_Divide_Tree;
int q;
read(n), read(q);
memset(head, -1, sizeof(int[n + 1]));
for (int i = 1; i < n; i++)
{
int a, b, c;
read(a), read(b), read(c);
add(a, b, c), add(b, a, c);
}
LCA::init();
tot = n;
f[rot = 0] = INF;
find_rot(1, 0);
int root = rot;
solve(rot);
while (q--)
{
int u, e;
read(u), read(e);
modify(u, e);
write(find(root)), putchar('\n');
}
return 0;
}
}
int main()
{
return zyt::work();
}
【洛谷3345_BZOJ3924】[ZJOI2015]幻想乡战略游戏(点分树)的更多相关文章
- 洛谷 P3345 [ZJOI2015]幻想乡战略游戏 解题报告
P3345 [ZJOI2015]幻想乡战略游戏 题目描述 傲娇少女幽香正在玩一个非常有趣的战略类游戏,本来这个游戏的地图其实还不算太大,幽香还能管得过来,但是不知道为什么现在的网游厂商把游戏的地图越做 ...
- bzoj3924 [Zjoi2015]幻想乡战略游戏 点分树,动态点分
[BZOJ3924][Zjoi2015]幻想乡战略游戏 Description 傲娇少女幽香正在玩一个非常有趣的战略类游戏,本来这个游戏的地图其实还不算太大,幽香还能管得过来,但是不知道为什么现在的网 ...
- 洛谷P3345 [ZJOI2015]幻想乡战略游戏(动态点分治,树的重心,二分查找,Tarjan-LCA,树上差分)
洛谷题目传送门 动态点分治小白,光是因为思路不清晰就耗费了不知道多少时间去gang这题,所以还是来理理思路吧. 一个树\(T\)里面\(\sum\limits_{v\in T} D_vdist(u,v ...
- 洛谷P3345 [ZJOI2015]幻想乡战略游戏 [动态点分治]
传送门 调了两个小时,终于过了-- 凭啥人家代码80行我180行啊!!! 谁叫你大括号换行 谁叫你写缺省源 思路 显然,补给点所在的位置就是这棵树的带权重心. 考虑size已知时如何找重心:一开始设答 ...
- 2018.08.28 洛谷P3345 [ZJOI2015]幻想乡战略游戏(点分树)
传送门 题目就是要求维护带权重心. 因此破题的关键点自然就是带权重心的性质. 这时发现直接找带权重心是O(n)的,考虑优化方案. 发现点分树的树高是logn级别的,并且对于以u为根的树,带权重心要么就 ...
- AC日记——[ZJOI2015]幻想乡战略游戏 洛谷 P3345
[ZJOI2015]幻想乡战略游戏 思路: 树剖暴力转移: 代码: #include <bits/stdc++.h> using namespace std; #define maxn 1 ...
- [ZJOI2015]幻想乡战略游戏——动态点分治
[ZJOI2015]幻想乡战略游戏 带修改下,边点都带权的重心 随着变动的过程中,一些子树内的点经过会经过一些公共边.考虑能不能对这样的子树一起统计. 把树上贡献分块. 考虑点分治算法 不妨先把题目简 ...
- BZOJ3924 ZJOI2015 幻想乡战略游戏 【动态点分治】
BZOJ3924 ZJOI2015 幻想乡战略游戏 Description 傲娇少女幽香正在玩一个非常有趣的战略类游戏,本来这个游戏的地图其实还不算太大,幽香还能管得过来,但是不知道为什么现在的网游厂 ...
- 【BZOJ3924】[Zjoi2015]幻想乡战略游戏 动态树分治
[BZOJ3924][Zjoi2015]幻想乡战略游戏 Description 傲娇少女幽香正在玩一个非常有趣的战略类游戏,本来这个游戏的地图其实还不算太大,幽香还能管得过来,但是不知道为什么现在的网 ...
随机推荐
- MT6753/MT6755 呼吸灯功能添加
利用mtk pmic自带的呼吸灯模式: 主要修改代码: kernel-3.10/drivers/misc/mediatek/leds/mt6755/leds.c int mt_brightness ...
- ci框架(codeigniter)Email发送邮件、收件人、附件、Email调试工具
ci框架(codeigniter)Email发送邮件.收件人.附件.Email调试工具 Email 类 CodeIgniter 拥有强大的 Email 类来提供如下的功能: 多 ...
- P1614 爱与愁的心痛
洛谷——P1614 爱与愁的心痛 题目背景 (本道题目隐藏了两首歌名,找找看哪~~~) <爱与愁的故事第一弹·heartache>第一章 <我为歌狂>当中伍思凯神曲<舞月 ...
- 【天道酬勤】 腾讯、百度、网易游戏、华为Offer及笔经面经(转)
应届生上泡了两年,一直都是下资料,下笔试题,面试题.一直都在感谢那些默默付出的人.写这个帖子花了我两 个夜晚的时间,不是为了炫耀,只是为了能给那些“迷惘”的学弟学妹,一点点建议而已.大家何必那么认真, ...
- Ubuntu 16.04安装UML工具StarUML 2
StarUML 2是一个商业软件,但是没有时间限制,就像Sublime Text 3一样.而且具有跨平台,支持Mac.Windows. 这个软件曾经08年的时候在老D的博客上有推荐过,参考:http: ...
- Redis基于Java的客户端SDK收集
如果要找这类的SDK,第一反应应该直奔官网,找一下看下有什么推荐.先找最权威的回答,找不到再尝试民间方案. 就Redis来说,官方已经提供了一个列表包括市面上绝大多数语言的SDK,可以参考以下网址看J ...
- 智能眼镜技术科普:VR、AR、MR的区别
前段时间, 获得谷歌5亿美元融资的技术公司Magic Leap在WSJD展会中放出了一段实录视频,引起不小骚动.如今,也有媒体称他们为MR公司,那么VR.AR.MR之间到底有什么区别呢. VR.AR. ...
- Vuzzer自动漏洞挖掘工具简单分析附使用介绍
Vuzzer 是由计算机科学机构 Vrije Universiteit Amsterdam.Amsterdam Department of Informatics 以及 International ...
- FlashChart json数据配置 中文文档
http://www.riaos.com/ria/2274 FlashChart json数据配置说明 有朋友要用flashchart,感觉这个还不错.就整理了一份文档. 基本包括了所有json配置的 ...
- Project Perfect让Swift在server端跑起来-Perfect in Visual Studio Code (四)
编者语 : 本系列文章已经被Perfect官方引用了,这样的感觉非常好.感恩!Thx all ! Visual Studio Code是一个轻量级的编辑器,但也功能丰富,通过插件你能够完毕如Cordo ...