谈虚树dp——bzoj2286与bzoj3572

Problem1

一棵n个节点的有边权树,m次询问,每次问k个点。要求删除边权总和最小,使得这k个点均不与1连通。

朴素Treedp

考虑一个dp,dp[i]表示i这棵子树中,所有关键点均不与i连通的最小代价。

有转移方程:(j是i的直系儿子)

(1)j如果是关键点:f[i] = min(f[i], val[edge(i<=>j)])

(2)otherwise,f[i] = min(f[i], f[j], val[edge(i<=>j)])

每次dp复杂度O(n),显然这m次dp会t。考虑正确解法。

虚树

忘了前面那道题吧(暂时)。引入一个虚树的概念——只留下一些重要的点,以及他们的lca(这样树形结构是不变的)。新的边权与老边权是等效的(这里要具体题目具体分析)。

(偷偷剧透,捂住双眼不要看:前面那题中,新的树中的两点间边权,为以前树中两点路径所含边权的最小值。具体为什么可以思考一下。因为其他点在询问中不起作用,所以当然是删掉越小的边越好啦。)

当然,说一句很重要的话,做法还是在线的,并不是一气把所有的节点都读进来再一起做的。所以单次dp操作(不算建虚树)的复杂度为O(k),总的dp操作复杂度O(sigma(k))。

那如何建虚树呢?

构造虚树

prepare:需要起码O(logn)的求lca、任意两点距离的算法(是不是没有任何难度呀)。还要事先求出每个点的深度(算深度时,每条边都看作1)。

开始建树!!!

1、给k个点以dfs序为key排序

2、把第1个点进栈

3、枚举下一个需要进栈的点i(循环,直到取完),计算u与栈顶(记为v)的lca

4、(这步是循环的,直至弹不了或者栈中元素不足2个为止才结束)设栈顶下面的点是w,若w的深度大于lca,就把v向w连边,并且弹掉v

5、若lca不是当前的栈顶v(lca的深度比v的深度小),那么就把lca向v连边,把v弹出。如果弹出v后,栈顶比lca的深度小(或者栈是空的),就把lca压入。(不管里面是否已经有lca了)

6、把u压入栈

7、3的那个循环结束

8、把栈顶v与栈顶下方的w连边,弹掉v,直到栈里只有1个点

9、栈里剩的点就是虚树的根

然后就可以dp啦~~~

Solution1

这个题,每次都保留1号节点作为根,然后每次都去虚树dp就好了。有一个小优化,如果k个点中存在x是y的祖先,那只保留x。(这个优化可不是每一道题都能用的)因此我并没有用这个优化。(哼才不是懒呢!!!)

Code1

#include <cstdio>
#include <algorithm>
#include <vector>
#include <stack>
#define N 250010
#define inf 0x3f3f3f3f
using namespace std;
typedef long long ll;
inline char gc() {
	static char now[1<<16], *S, *T;
	if(S == T) {T = (S = now) + fread(now, 1, 1<<16, stdin); if(S == T) return EOF;}
	return *S++;
}
inline int read() {
	int x = 0; char c = gc();
	while(c < '0' || c > '9') c = gc();
	while(c >= '0' && c <= '9') {x = x * 10 + c - 48; c = gc();}
	return x;
}
struct edge {int to, val, next;}e[N<<1];
int head[N], fa[N][18], dep[N], mn[N][18], dfn[N], h[500010];
bool mark[N], invec[N];
int n, cnt = 1, tim = 0, m;
vector<int> son[N], val[N];
stack<int> sta;
inline void ins(int x, int y, int z) {e[++cnt].to = y; e[cnt].val = z; e[cnt].next = head[x]; head[x] = cnt;}
void dfs(int x, int f, int d, int v) {
	fa[x][0] = f; dep[x] = d; mn[x][0] = v; dfn[x] = ++tim;
	for(int i = head[x]; i; i = e[i].next)
		if(e[i].to != f) dfs(e[i].to, x, d + 1, e[i].val);
}
inline bool cmp(int A, int B) {return dfn[A] < dfn[B];}
inline int asklca(int x, int y) {
	if(dep[x] > dep[y]) swap(x, y);
	for(int i = 17; i >= 0; --i)
		if(dep[y] - (1<<i) >= dep[x]) y = fa[y][i];
	if(x == y) return x;
	for(int i = 17; i >= 0; --i)
		if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
	return fa[x][0];
}
stack<int> sta1;
inline void add(int x, int y) {
	if(!invec[x]) sta1.push(x); if(!invec[y]) sta1.push(y);
	invec[x] = invec[y] = 1;
	if(dep[x] > dep[y]) swap(x, y);
	int v = inf;
	int xx = x, yy = y;
	for(int i = 17; i >= 0; --i)
		if(dep[y] - (1<<i) >= dep[x]) v = min(v, mn[y][i]), y = fa[y][i];
	if(x != y) {
		for(int i = 17; i >= 0; --i)
			if(fa[x][i] != fa[y][i]) v = min(v, min(mn[x][i], mn[y][i])), x = fa[x][i], y = fa[y][i];
		v = min(v, min(mn[x][0], mn[y][0]));
	}
	son[xx].push_back(yy); val[xx].push_back(v);
	son[yy].push_back(xx); val[yy].push_back(v);
}
ll work(int x, int f) {
	ll ret = 0;
	for(int i = 0; i < son[x].size(); ++i) {
		if(son[x][i] == f) continue;
		int y = son[x][i];
		if(mark[y]) ret+= (ll)val[x][i];
		else ret+= min((ll)val[x][i], work(y, x));
	}
	return ret;
}
int main() {
	n = read();
	for(int i = 1; i < n; ++i) {
		int u = read(), v = read(), w = read();
		ins(u, v, w); ins(v, u, w);
	}
	dfs(1, 0, 1, inf);
	for(int j = 1; j <= 17; ++j)
		for(int i = 1; i <= n; ++i) {fa[i][j] = fa[fa[i][j - 1]][j - 1]; mn[i][j] = min(mn[i][j - 1], mn[fa[i][j - 1]][j - 1]);}
	m = read(); h[0] = 1;
	for(int i = 1; i <= m; ++i) {
		int K = read();
		for(int j = 1; j <= K; ++j) h[j] = read(), mark[h[j]] = 1;
		sort(h+1, h+K+1, cmp);
		sta.push(1);
		for(int j = 1; j <= K; ++j) {
			int u = h[j], v = sta.top(), lca = asklca(u, v);
			while(sta.size() >= 2) {
				sta.pop(); int w = sta.top(); sta.push(v);
				if(dep[w] > dep[lca]) {
					add(v, w);
					sta.pop(); v = sta.top();
				}else break;
			}
			if(dep[lca] < dep[v]) {add(lca, v), sta.pop();}
			if(sta.empty() || dep[sta.top()] < dep[lca]) sta.push(lca);
			sta.push(u);
		}
		while(sta.size() >= 2) {
			int v = sta.top(); sta.pop(); int w = sta.top(); sta.push(v);
			add(v, w); sta.pop();
		}
		sta.pop();
		printf("%lld\n", work(1, 0));
		while(!sta1.empty()) {
			int now = sta1.top(); sta1.pop();
			son[now].clear(); val[now].clear();
			invec[now] = 0;
		}
		for(int j = 1; j <= K; ++j) mark[h[j]] = 0;
	}
	return 0;
}

Problem2

这个题,就是著名的[HNOI2014]世界树。


3572: [Hnoi2014]世界树

Time Limit: 20 Sec   Memory Limit: 512 MB
Submit: 2227   Solved: 1204
[ Submit][ Status][ Discuss]

Description

世界树是一棵无比巨大的树,它伸出的枝干构成了整个世界。在这里,生存着各种各样的种族和生灵,他们共同信奉着绝对公正公平的女神艾莉森,在他们的信条里,公平是使世界树能够生生不息、持续运转的根本基石。
世界树的形态可以用一个数学模型来描述:世界树中有n个种族,种族的编号分别从1到n,分别生活在编号为1到n的聚居地上,种族的编号与其聚居地的编号相同。有的聚居地之间有双向的道路相连,道路的长度为1。保证连接的方式会形成一棵树结构,即所有的聚居地之间可以互相到达,并且不会出现环。定义两个聚居地之间的距离为连接他们的道路的长度;例如,若聚居地a和b之间有道路,b和c之间有道路,因为每条道路长度为1而且又不可能出现环,所卧a与c之间的距离为2。
出于对公平的考虑,第i年,世界树的国王需要授权m[i]个种族的聚居地为临时议事处。对于某个种族x(x为种族的编号),如果距离该种族最近的临时议事处为y(y为议事处所在聚居地的编号),则种族x将接受y议事处的管辖(如果有多个临时议事处到该聚居地的距离一样,则y为其中编号最小的临时议事处)。
现在国王想知道,在q年的时间里,每一年完成授权后,当年每个临时议事处将会管理多少个种族(议事处所在的聚居地也将接受该议事处管理)。 现在这个任务交给了以智慧著称的灵长类的你:程序猿。请帮国王完成这个任务吧。

Input

第一行为一个正整数n,表示世界树中种族的个数。
接下来n-l行,每行两个正整数x,y,表示x聚居地与y聚居地之间有一条长度为1的双
向道路。接下来一行为一个正整数q,表示国王询问的年数。
接下来q块,每块两行:
第i块的第一行为1个正整数m[i],表示第i年授权的临时议事处的个数。
第i块的第二行为m[i]个正整数h[l]、h[2]、…、h[m[i]],表示被授权为临时议事处的聚居地编号(保证互不相同)。

Output

输出包含q行,第i行为m[i]个整数,该行的第j(j=1,2…,,m[i])个数表示第i年被授权的聚居地h[j]的临时议事处管理的种族个数。

Sample Input

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

Sample Output

1 9
3 1 4 1 1
10
1 1 3 5
4 1 3 1 1

HINT

N<=300000, q<=300000,m[1]+m[2]+…+m[q]<=300000

Solution2

虚树dp的题吧,总是暗示得极其明显(或许HNOI2018毒瘤应该除外?),但难度依旧很大。

每次都建立虚树,新边权就是老边权的和。由于虚树由询问点和lca组成,那么我们可以先求出虚树上的点分别从属于谁。

具体做法就是dp两次。先用儿子更新父亲,再考虑父亲更新儿子(直接反着做?dfs序是个好东西啊,必须多多研究一下)。

然后就要考虑所有非虚树节点了。

这些点有两种情况:

1、处于某两点的路径上。(比较麻烦哦)

2、otherwise(就是那种如果我不告诉你这里有点,你都不知道的那种),直接全部归属于其原子树根。(理解一下,相当于一棵树,砍掉一根大树枝的那种)这种情况十分好处理。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值