[BZOJ]3924 [ZJOI2015] 幻想乡战略游戏 树链剖分

3924: [Zjoi2015]幻想乡战略游戏

Time Limit: 100 Sec   Memory Limit: 256 MB
Submit: 1034   Solved: 478
[ Submit][ Status][ Discuss]

Description

 傲娇少女幽香正在玩一个非常有趣的战略类游戏,本来这个游戏的地图其实还不算太大,幽香还能管得过来,但是不知道为什么现在的网游厂商把游戏的地图越做越大,以至于幽香一眼根本看不过来,更别说和别人打仗了。 在打仗之前,幽香现在面临一个非常基本的管理问题需要解决。 整个地图是一个树结构,一共有n块空地,这些空地被n-1条带权边连接起来,使得每两个点之间有一条唯一的路径将它们连接起来。在游戏中,幽香可能在空地上增加或者减少一些军队。同时,幽香可以在一个空地上放置一个补给站。 如果补给站在点u上,并且空地v上有dv个单位的军队,那么幽香每天就要花费dv×dist(u,v)的金钱来补给这些军队。由于幽香需要补给所有的军队,因此幽香总共就要花费为Sigma(Dv*dist(u,v),其中1<=V<=N)的代价。其中dist(u,v)表示u个v在树上的距离(唯一路径的权和)。 因为游戏的规定,幽香只能选择一个空地作为补给站。在游戏的过程中,幽香可能会在某些空地上制造一些军队,也可能会减少某些空地上的军队,进行了这样的操作以后,出于经济上的考虑,幽香往往可以移动他的补给站从而省一些钱。但是由于这个游戏的地图是在太大了,幽香无法轻易的进行最优的安排,你能帮帮她吗? 你可以假定一开始所有空地上都没有军队。

PDF版试题:JudgeOnline/upload/201708/zjoi2015d1.pdf

Input

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

Output

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

Sample Input

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

Sample Output

0
1
4
5
6

HINT



Source

[ Submit][ Status][ Discuss]


HOME Back

  这道题我就呵呵了, 坑我一晚上 + 一下午. 昨天晚上状态极不好边想这道题边去颓课件去了, 导致效率极低. 最后集中想了20min没想出来选择看题解, 发现题解清一色的抛结论... 于是自己证了一下, 由于有个sigma写错了导致我推了1年... 后面才发现是傻逼结论. 然而一个晚上就这么在颓废中过去了QAQ... 不过网上都用的点分治|树链剖分 + 点分治. 昨晚回寝室睡觉时想到了一个纯树链剖分的做法. 于是今天下午来实现... 40min写完没想到被坑了一下午.

  然而这道题是ZJOI2015 Day1 T1. 我选择go die. 讲真t3比t1简单啊(可能是因为我没往性质结论上想...

  这道题要求的东西实际上就是带权重心. 为什么呢? 我们设sum[u]为u子树的d之和(d详情请看题目定义). 那么如果u的儿子v, sum[v] * 2 >= sum[root]的话那么最优点一定在v的子树里. 证明: 我们考虑以u作为补给点的花费设为p, 那么我们会发现若以v作为补给点的话花费是 p + (sum[root] - sum[v] * 2) * w[u, v]. 因为走过(u, v)这条边会使除了v子树的其他点i的所有花费+d[i] * w(u, v), 那么就是(sum[root] - sum[v]) * w(u, v). 然后v子树里的所有点的花费会 - sum[v] * w(u, v). 加起来就是上式辣. 那么如果sum[v] * 2 >= sum[root]的话花费就会减少. 而且这样的v由于sum[v]>=sum[u]/2所以只会最多有一个. 为什么不在其他子树里呢? 因为其他子树譬如v1满足sum[v1] < sum[root]/2那么v1子树里的点sum更小显然都满足, 那么花费只会增加不会减少. 所以我们只需要顺着根往下判断来找这个最优点就行了(直到儿子都比自己劣那此时就是最优点).

  然后我们会发现会tle. 考虑怎么优化. 首先从根到最优点的路径是唯一确定的, 并且显然这条路径是垂直的链, 也就是说深度单增且dfs序单增. 并且从u要走到v的话肯定满足v是u中sum最大的. 那么我们用线段树维护dfs序区间max就可以啦. 每次判断即可(这个很好理解可以结合代码seg_query函数). 对于修改直接修改一条链的sum即可. 显然这可以用树链剖分来维护. 那怎么算答案呢?  我们知道每次移动从u到v花费差距是 (sum[root] - sum[v] * 2) * w[u, v]. 那么最优点与根的差距就是sum[root] * dis[v](到根距离) - 2 * (sum[v] * w(fa[v], v). 根的答案很好维护, 式子前面一项也很好维护. 第二项发现只跟v有关, 且只有sum一个在变. 那么树剖的时候顺带维护一下就好了. 因为每次修改链上都是区间加减, 对于一段dfs序区间的sum[v] * w(fa[v], v)之和(v属于这段区间)也就是加上了这段区间每个v到各自父亲的距离 * 当前改动的值. 详见代码(非常easy辣).

  然而我老是wa的原因就是之前推导的时候没考虑边权, 直接用的dep... 然而样例恰好边权都是1, 我...

  本来以为A了可以艹榜的, 发现只能在第二页(我的ID是lkq的, 我没有权限号...)... 我还是太naive了. 改天会了zkw线段树再回来艹一发.

  话说为什么加了const &和fread读优还没有没加快啊. 而且Claris神犇什么鬼畜压行怎么又写了树剖+线段树还写了点分治都只有80多行啊

#include<bits/stdc++.h>
using namespace std;
typedef long long lnt;
const int maxn = 2e5 + 5;
lnt ans, all;
int n, Q, num, idx;
int siz[maxn], fa[maxn], son[maxn], seq[maxn], in[maxn], h[maxn], top[maxn], dis[maxn], dep[maxn], par[maxn];
inline const int read() {
	register int x = 0, f = 1;
	register char ch = getchar();
	while (ch < '0' || ch > '9') {if (ch == '-') f = -1; ch = getchar();}
	while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
	return f * x;
}
struct edge {
	int nxt, v, w;
}e[maxn << 1];
inline void add(int u, int v, int w) {
	e[++ num].v = v, e[num].nxt = h[u], e[num].w = w, h[u] = num;
	e[++ num].v = u, e[num].nxt = h[v], e[num].w = w, h[v] = num;
}
void dfs1(int u, int f) {
	siz[u] = 1, fa[u] = f;
	for (int i = h[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (v == f) continue;
		par[v] = e[i].w;
		dep[v] = dep[u] + 1;
		dis[v] = dis[u] + e[i].w;
		dfs1(v, u);
		siz[u] += siz[v];
		if (siz[son[u]] < siz[v]) son[u] = v;
	}
}
void dfs2(int u, int tp) {
	top[u] = tp;
	in[u] = ++ idx, seq[idx] = u;
	if (son[u]) dfs2(son[u], tp);
	for (int i = h[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (v == fa[u] || v == son[u]) continue;
		dfs2(v, v);
	}
}
struct node {
	node *ls, *rs;
	int mid; lnt sum, flag, cmax, dsm;
	inline void pushdown(int lf, int rg) {
		if (flag) {
			mid = (lf + rg) >> 1;
			ls -> sum += flag * ls -> dsm;
			rs -> sum += flag * rs -> dsm;
			ls -> cmax += flag, rs -> cmax += flag;
			ls -> flag += flag, rs -> flag += flag;
			flag = 0;
		}
	}
	inline void update() {
		sum = ls -> sum + rs -> sum;
		cmax = (ls -> cmax > rs -> cmax) ? ls -> cmax : rs -> cmax;
	}
}pool[maxn << 1], *root, *tail = pool;
node* build(int lf, int rg) {
	node* bt = ++ tail;
	if (lf == rg) {
		bt -> dsm = par[seq[lf]];
		bt -> cmax = bt -> sum = 0;
		return bt;
	}
	int mid = (lf + rg) >> 1;
	bt -> ls = build(lf , mid), bt -> rs = build(mid + 1, rg);
	bt -> update();
	bt -> dsm = bt -> ls -> dsm + bt -> rs -> dsm;
	return bt;
}
void modify(node* bt, int lf, int rg,  int L, int R, int val) {
	if (L <= lf && rg <= R) {
		bt -> cmax += val;
		bt -> flag += val;
		bt -> sum += bt -> dsm * val;
		return;
	}
	bt -> pushdown(lf, rg);
	int mid = (lf + rg) >> 1;
	if (L <= mid) modify(bt -> ls, lf, mid, L, R, val);
	if (R > mid) modify(bt -> rs, mid + 1, rg, L, R, val);
	bt -> update(); 
}
lnt query(node* bt, int lf, int rg, int L, int R) {
	if (L <= lf && rg <= R) return bt -> sum;
	bt -> pushdown(lf, rg);
	int mid = (lf + rg) >> 1; lnt rt = 0;
	if (L <= mid) rt += query(bt -> ls, lf, mid, L, R);
	if (R > mid) rt += query(bt -> rs, mid + 1, rg, L, R);
	bt -> update();
	return rt;
}
int queryheart(node* bt, int lf, int rg) {
	if (lf == rg) return seq[lf];
	bt -> pushdown(lf, rg);
	int mid = (lf + rg) >> 1, rt = 0;
	if (all <= 2 * bt -> rs -> cmax) rt = queryheart(bt -> rs, mid + 1, rg);
	else rt = queryheart(bt -> ls, lf, mid);
	bt -> update();
	return rt;
}
inline void seg_modify(int u, int val) {
	while (u)
		modify(root, 1, n, in[top[u]], in[u], val), u = fa[top[u]];
}
inline lnt seg_query() {
	lnt rt = 0;
	int u = queryheart(root, 1, n), ori = u;
	while (u) 
		rt += query(root, 1, n, in[top[u]], in[u]), u = fa[top[u]];
	return all * dis[ori] - 2 * rt;
}
int main() {
	n = read(), Q = read();
	register int i, u, v, w;
	for (i = 1; i < n; ++ i)
		u = read(), v = read(), w = read(), add(u, v, w);
	dfs1(1, 0), dfs2(1, 1);
	root = build(1, n);
	for (i = 1; i <= Q; ++ i) {
		u = read(), w = read();
		seg_modify(u, w);
		all += w, ans += 1ll * dis[u] * w;
		lnt hh = (ans + seg_query());
		printf("%lld\n", hh);
	}
	return 0;
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述 有一个 $n$ 个点的棋盘,每个点上有一个数字 $a_i$,你需要从 $(1,1)$ 走到 $(n,n)$,每次只能往右或往下走,每个格子只能经过一次,路径上的数字和为 $S$。定义一个点 $(x,y)$ 的权值为 $a_x+a_y$,求所有满足条件的路径中,所有点的权值和的最小值。 输入格式 第一行一个整数 $n$。 接下来 $n$ 行,每行 $n$ 个整数,表示棋盘上每个点的数字。 输出格式 输出一个整数,表示所有满足条件的路径中,所有点的权值和的最小值。 数据范围 $1\leq n\leq 300$ 输入样例 3 1 2 3 4 5 6 7 8 9 输出样例 25 算法1 (树形dp) $O(n^3)$ 我们可以先将所有点的权值求出来,然后将其看作是一个有权值的图,问题就转化为了在这个图中求从 $(1,1)$ 到 $(n,n)$ 的所有路径中,所有点的权值和的最小值。 我们可以使用树形dp来解决这个问题,具体来说,我们可以将这个图看作是一棵树,每个点的父节点是它的前驱或者后继,然后我们从根节点开始,依次向下遍历,对于每个节点,我们可以考虑它的两个儿子,如果它的两个儿子都被遍历过了,那么我们就可以计算出从它的左儿子到它的右儿子的路径中,所有点的权值和的最小值,然后再将这个值加上当前节点的权值,就可以得到从根节点到当前节点的路径中,所有点的权值和的最小值。 时间复杂度 树形dp的时间复杂度是 $O(n^3)$。 C++ 代码 算法2 (动态规划) $O(n^3)$ 我们可以使用动态规划来解决这个问题,具体来说,我们可以定义 $f(i,j,s)$ 表示从 $(1,1)$ 到 $(i,j)$ 的所有路径中,所有点的权值和为 $s$ 的最小值,那么我们就可以得到如下的状态转移方程: $$ f(i,j,s)=\min\{f(i-1,j,s-a_{i,j}),f(i,j-1,s-a_{i,j})\} $$ 其中 $a_{i,j}$ 表示点 $(i,j)$ 的权值。 时间复杂度 动态规划的时间复杂度是 $O(n^3)$。 C++ 代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值