zoukankan      html  css  js  c++  java
  • NOI2021 SDPTT D2T1 我已经完全理解了 DFS 序线段树 题解

    题目来源:2021山东省队一轮集训D2T1。

    原题编译选项:-lm -O2 -std=c++11

    题目描述

    给定一棵有 (n) 个结点的有根树,根结点为 (1) 号点。
    每个点有权值 (a_i),初始时均为 (0),以及花费 (v_i),表示对它进行一次操作时要花费的代价。

    对点 (u) 进行一次参数为 (x) 的操作,即是在树中将它子树中的所有点的权值都加上 (x)

    要让每个叶子的权值互不相同且非零,即当 (i,j(i eq j)) 为叶子时要满足 (a_i eq 0)(a_i eq a_j)

    你需要构造一个总花费尽量小的操作序列。

    输入格式

    第一行,一个正整数 (n),表示树的结点数。

    第二行,(n) 个正整数 (v_1,v_2,...,v_n),依次表示每个结点的花费。

    接下来 (n−1) 行,每行两个正整数 (u,v,)表示树中存在一条连接结点 (u,v) 的边。

    输出格式

    第一行,一个数 (q),表示操作序列的长度,你需要保证 (0leq qleq 2n)

    接下来 (q) 行,每行两个数 (u,x),表示对点 (u) 进行了一次参数为 (x) 的操作,你需要保证 (x) 是正整
    数且 (1leq xleq 10^9)

    你需要保证每次操作合法,且总花费是所有情况中最小的。

    本题使用自定义校验器检验你的答案是否正确,因此若有多种满足条件的方案,你只需要输出任意一种

    【数据范围与提示】

    对于所有测试点:(2leq nleq 2 imes 10^5,1leq v_ileq 10^9)
    每个测试点的具体限制见下表:

    测试点编号 (nleq) 特殊限制
    (1 ∼ 4) (18)
    (5 ∼ 6) (2 imes 10^5) (A)
    (7 ∼ 9) (2 imes 10^5) (B)
    (10 ∼ 13) (2 imes 10^5) (C)
    (14 ∼ 25) (2 imes 10^5)

    (mathcal{Solution})

    有很显然的树形dp可以dp出最小花费以及要覆盖哪些节点。

    但是我直接随机的操作参数被生日悖论干爆了。

    显然一个点不会被操作多次,先不管操作的参数,假设他是一个很大范围内的随机数,现在一个方案合法当且仅当每个节点到根的被操作的点组成的集合非空且两两不同。

    如果根节点被操作,则所有叶子的点集都不是非空的了,但如果存在一个叶子的点集只有根一个元素,这个叶子一定不超过 (1) 个,多了就重复了。

    除了这个叶子节点所在的子树的其他子树的叶子节点,还需要他们的点集除去根节点是非空的,这是若干个子问题。

    至此,我们可以得出一个比较显然的树形dp:

    (ans_{x,0}) 代表 (x) 子树内所有叶子节点的点集非空且两两不同的最小代价(也就是 (x) 子树的答案);

    (ans_{x,1}) 代表 (x) 子树内所有叶子节点的点集两两不同的最小代价(也就是可以为空)。

    (x) 为叶子时:(ans_{x,0}=v_x,ans_{x,1}=0)

    (x) 不是叶子时(其中 (v)(x) 的儿子):

    [ans_{x,0}=minleft{egin{matrix} sumlimits_{v}ans_{v,0} (不选x的情况) \ v_x+sumlimits_{v}ans_{v,0}+minlimits_{v}{ans_{v,1}-ans_{v,0}} (选x的情况) end{matrix} ight. ]

    [ans_{x,1}=sum_v ans_{v,0}+minlimits_v {ans_{v,1}-ans_{v,0}} ]

    可以根据 (ans_{x,0/1}) 从哪里转移而来得到最优的方案是填哪些数。

    上面的 (dp) 部分还是比较常规的,下面的构造方案我认为比较巧妙。

    可以发现一个性质:每个叶子祖先链中第一个(最深的)被操作的结点是互不相同的。

    进而发现,每个叶子唯一对应一个最深结点,每个被操作结点也唯一对应一个叶子。

    可以钦定每个叶子权值必须同余它自己的编号模 (n) ,这样可以保证没有两个叶子权值相同。

    如何构造方案?自上而下 dfs 一下就可以了,假设到了结点 (x) 发现要给 (x) 一个权值 (col_x),而 (x) 对应的叶子节点编号是 (vis_x)(x) 的祖先链中的被操作节点的参数和为 (sum) ,则应该满足 (vis_xequiv sum+col_xpmod n),所以 (col_x=vis_x-sumpmod n)(由于要求非 (0),可以默认为模后 (+1))。

    这样子就能构造出一组合法的方案了,操作的参数的范围仅在 ([1,n]),足以通过此题。

    由于要求操作参数 (x) 范围为 ([1,10^9]),如果你的做法跑得较快常数较小,可以直接随机权值,多跑几遍说不定能跑过去,不过笔者并没有冲过去,赛时跑随机仅得到 (50) 分。

    代码是考场代码直接改的,所以会稍微有点乱。

    参考代码:

    //Code by do_while_true
    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    typedef long long ll;
    template <typename T> T Max(T x, T y) { return x > y ? x : y; }
    template <typename T> T Min(T x, T y) { return x < y ? x : y; }
    template <typename T>
    T &read(T &r) {
    	r = 0; bool w = 0; char ch = getchar();
    	while(ch < '0' || ch > '9') w = ch == '-' ? 1 : 0, ch = getchar();
    	while(ch >= '0' && ch <= '9') r = r * 10 + (ch ^ 48), ch = getchar();
    	return r = w ? -r : r;
    }
    const int N = 200010;
    const ll INF = 0x3ffffffffffffff;
    int n, ent, head[N], v[N], fa[N];
    struct Edge {
    	int next, to;
    }e[N << 1];
    inline void add(int x, int y) {
    	e[++ent].to = y; e[ent].next = head[x]; head[x] = ent;
    }
    namespace AC {
    	int che[N], mnpos[N], cnt, fa[N], vis[N];
    	bool leaf[N];
    	ll ans0[N], ans1[N];
    	void dfs1(int x, int f) {
    		leaf[x] = 1;
    		for(int i = head[x]; i; i = e[i].next) {
    			int v = e[i].to;
    			if(v == f) continue;
    			leaf[x] = 0;
    			dfs1(v, x);
    		}
    		if(leaf[x]) ans0[x] = v[x], ans1[x] = 0;
    		else {
    			ll ans01 = 0, ans02 = v[x], mn = INF;
    			for(int i = head[x]; i; i = e[i].next) {
    				int v = e[i].to;
    				if(v == f) continue;
    				ans01 += ans0[v];
    				ans02 += ans0[v];
    				if(mn > ans1[v] - ans0[v]) mn = ans1[v] - ans0[v], mnpos[x] = v;
    				ans1[x] += ans0[v];
    			}
    			ans02 += mn;
    			if(ans01 < ans02) che[x] = 0;
    			else che[x] = 1;
    			ans0[x] = Min(ans01, ans02);
    			ans1[x] += mn;
    		}
    	}
    	void dfs2(int x, int f, int t) {
    		fa[x] = f;
    		if(leaf[x]) {
    			if(t == 0) vis[x] = 1;
    			return ;
    		}
    		if(t == 1) {
    			dfs2(mnpos[x], x, 1);
    			for(int i = head[x]; i; i = e[i].next) {
    				int v = e[i].to;
    				if(v == f || v == mnpos[x]) continue;
    				dfs2(v, x, 0);
    			}
    			return ;
    		}
    		if(!che[x]) {
    			for(int i = head[x]; i; i = e[i].next) {
    				int v = e[i].to;
    				if(v == f) continue;
    				dfs2(v, x, 0);
    			}
    			return ;
    		}
    		vis[x] = 1;
    		dfs2(mnpos[x], x, 1);
    		for(int i = head[x]; i; i = e[i].next) {
    			int v = e[i].to;
    			if(v == f || v == mnpos[x]) continue;
    			dfs2(v, x, 0);
    		}
    	}
    	void dfs3(int x, ll sum) {
    		if(vis[x])
    			vis[x] = ((vis[x] - sum - 1) % n + n) % n + 1,
    			sum += vis[x];
    		for(int i = head[x]; i; i = e[i].next) {
    			int v = e[i].to;
    			if(v == fa[x]) continue;
    			dfs3(v, sum);
    		}
    	}
    	void mian() {
    		dfs1(1, 0);
    		dfs2(1, 0, 0);
    		for(int i = 1; i <= n; ++i)
    			if(leaf[i]) {
    				int x = i;
    				while(!vis[x]) x = fa[x];
    				vis[x] = i;
    				++cnt;
    			}
    		dfs3(1, 0);
    		printf("%d
    ", cnt);
    		for(int i = 1; i <= n; ++i)
    			if(vis[i])
    				printf("%d %d
    ", i, vis[i]);
    		return ;
    	}
    }
    int main() {
    	freopen("segtree.in", "r", stdin);
    	freopen("segtree.out", "w", stdout);
    	read(n);
    	for(int i = 1; i <= n; ++i) read(v[i]);
    	for(int i = 1, u, v; i < n; ++i) {
    		read(u); read(v);
    		add(u, v); add(v, u);
    	}
    	AC::mian();
    	fclose(stdin);
    	fclose(stdout);
    	return 0;
    }
    

    粉兔的std是写的 Kruskal

    考虑一个子树 (i) 里的叶子对应一个区间(“DFS 序线段树”)。
    假设这个区间是 ([l,r)),连边 (lleftrightarrow r),权值为 (v_i)
    求最小生成树,树边对应的结点就是一组合法操作结点集。
    挺趣味的做法。

    笔者并未搞懂,虽然此做法代码较短但用时较大(不清楚是复杂度的关系还是常数的关系),故仅做参考。

    参考代码:

    #include <bits/stdc++.h>
    using namespace std;
    
    mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
    template<typename T>
    inline T range(T l, T r) {
    	return uniform_int_distribution<T>(l, r)(rng);
    }
    
    const int Maxn = 200005;
    int n, m, w[Maxn], fa[Maxn], siz[Maxn], dfn[Maxn], faz[Maxn], cho[Maxn];
    long long ans;
    vector <int> G[Maxn];
    struct Edg
    {
    	int x, y, w, id;
    	bool operator < (const Edg &tmp) const
    	{
    		return w < tmp.w;
    	}
    } E[2 * Maxn];
    int get_fa(int x)
    {
    	return x == fa[x] ? x : fa[x] = get_fa(fa[x]);
    }
    void merge(int x, int y)
    {
    	int a = get_fa(x), b = get_fa(y);
    	fa[a] = b;
    }
    void kruskal(void)
    {
    	sort(E + 1, E + 1 + n);
    	for (int i = 1; i <= n; i++)
    		if (get_fa(E[i].x) != get_fa(E[i].y))
    			merge(E[i].x, E[i].y), ans += E[i].w,
    			cho[E[i].id] = 1;
    }
    void init_dfs(int u, int fa)
    {
    	if (u != 1 && G[u].size() == 1) siz[u] = 1, dfn[u] = ++dfn[0];
    	else dfn[u] = 0x3f3f3f3f;
    	for (auto to : G[u])
    		if (to != fa)
    			init_dfs(to, u), siz[u] += siz[to], dfn[u] = min(dfn[u], dfn[to]);
    	faz[u] = fa;
    }
    void dfs(int u, int fa, long long sum)
    {
    	if (cho[u])
    		cho[u] = ((cho[u] - sum) % n + n) % n + 1,
    		sum += cho[u];
    	for (auto to : G[u])
    		if (to != fa)
    			dfs(to, u, sum);
    }
    int main(int argc, char ** argv)
    {
    	freopen("segtree.in", "r", stdin);
    	freopen("segtree.out", "w", stdout);
    	scanf("%d", &n);
    	for (int i = 1; i <= n; i++)
    		scanf("%d", &w[i]), fa[i] = i;
    	for (int i = 1; i < n; i++)
    	{
    		int x, y;
    		scanf("%d%d", &x, &y);
    		G[x].push_back(y), G[y].push_back(x);
    	}
    	init_dfs(1, 0);
    	for (int i = 1; i <= n; i++)
    		E[i] = (Edg){dfn[i], dfn[i] + siz[i], w[i], i};
    	kruskal();
    	for (int i = 2; i <= n; ++i) if ((int)G[i].size() == 1) {
    		int x = i;
    		while (!cho[x]) x = faz[x];
    		cho[x] = i;
    	}
    	dfs(1, 0, 0);
    	printf("%d
    ", dfn[0]);
    	for (int i = 1; i <= n; ++i) if (cho[i])
    		printf("%d %d
    ", i, cho[i]);
    	return 0;
    }
    
  • 相关阅读:
    机器人
    仙岛求药(一)
    YZM 二分查找
    珠心算测验升级版
    博客正在施工
    【其他】16年12月博客阅读索引^_^
    博客有新家了!
    POJ No.3617【B008】
    POJ No.2386【B007】
    【刷题记录】部分和问题
  • 原文地址:https://www.cnblogs.com/do-while-true/p/14730313.html
Copyright © 2011-2022 走看看