zoukankan      html  css  js  c++  java
  • CSP2019 提高组题解

    (sroce : 100 + 100 + 10 + 12 + 24 + 40 = 286),还需继续加油鸭!

    题解顺序按本人心中的难度顺序升序排序。

    D1T1. 格雷码

    题目链接:Link

    • 一道简单的分治。
    1. 1 位格雷码由两个 1 位二进制串组成,顺序为:0,1。
    2. (n + 1) 位格雷码的前 (2^n) 个二进制串,可以由依次算法生成的 (n) 位格雷码(总共 (2^n)(n) 位二进制串)按顺序排列,再在每个串前加一个前缀 0 构成。
    3. (n + 1) 位格雷码的后 (2^n) 个二进制串,可以由依次算法生成的 (n) 位格雷码(总共 (2^n)(n) 位二进制串)按逆序排列,再在每个串前加一个前缀 1 构成。
    • 通过上面的这段 " 一种格雷码的生成算法 " 我们可以知道,对于任意的 (n(n geq 2))(n) 位格雷码总是可以由 (n - 1) 位格雷码加一个前缀 0 或前缀 1 构成,考虑分治。

    • (calc(n, k)) 表示 (n) 位格雷码中的 (k) 号二进制串。

    • 首先是递归边界 (n = 1),此时若 (k = 0),则 (calc(n, k) = 0);若 (k = 1),则 (calc(n, k) = 1)

    • 对于任意的 (n(n geq 2)),此时有两种情况:

      • (k < 2^{n - 1}),则 (calc(n, k) = 0 + calc(n - 1, k))
      • (k geq 2^{n - 1}),则 (calc(n, k) = 1 + calc(n - 1, 2^n - 1 - k))
    • 时间复杂度 (mathcal{O(n)})

    • 记得开 unsigned long long

    • 题外话

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <string>
    
    using namespace std;
    
    int n;
    unsigned long long k;
    
    string calc(int n, unsigned long long k) {
    	if (n == 1) return k == 0 ? "0" : "1";
    	else {
    		unsigned long long S = 1ull << (n - 1);
    		if (k < S) return "0" + calc(n - 1, k);
    		else return "1" + calc(n - 1, 2 * S - 1 - k);
    	}
    }
    
    int main() {
    	cin >> n >> k;
    	cout << calc(n, k) << endl;
    
    	return 0;
    }
    

    D1T2. 括号树

    题目链接:Link

    • 注意到答案的形式是 ((1 imes k_1) ext{xor} (2 imes k_2) ext{xor} (3 imes k_3) ext{xor} ⋯ ext{xor} (n imes k_n)),这使得我们难以对答案进行一些分析,只能乖乖地把 (k_1, k_2, k_3, ..., k_n) 都求出来,再计算出答案。

    • 为了方便叙述,约定变量:

      • (fa_x)(x) 号节点在树上的父亲编号。
      • (lst_x):在 (1 sim x) 的路径中,能成功与 (x) 号节点上的括号匹配的深度最大的节点,当不存在能成功与 (x) 号节点上的括号匹配的节点时,(lst_x = -1)
      • (cnt_x):在 (1 sim x) 的路径中,以 (x) 号节点为结尾最多能数出多少个连续的括号块,例如 ()(())((())) 就能数出 (3) 个连续的括号块。
    • 考虑对树进行一次广度优先遍历,一开始只有 (1) 号节点。

    • 每次从队头取出节点 (u),考虑 (u) 的每条出边 ((u, v)),考虑计算 (k_v)

    • 首先答案可以先继承,即我们可以让 (k_v = k_u),然后再考虑一下将节点 (v) 上的括号加进 (s_u) 时产生的贡献。

    • 首先当节点 (v) 上的括号为 ( 时,不会产生贡献。

    • 考虑当节点 (v) 上的括号为 ) 时产生的贡献,令 (p = u),我们考虑让 (p) 去暴力跳 (lst) 数组,来找出与节点 (v) 上的 ) 匹配的 ( 的位置:

      • (lst_p = -1) 时,此时 (p) 不能继续跳了。
        若节点 (p) 上的括号是 (,则匹配成功,结束匹配。
        否则匹配失败,令 (p = 0)
      • (lst_p eq -1) 时,令 (p = fa_{lst_p})
      • (p = 0) 时,表示匹配失败,结束匹配。
    • (p eq 0) 时,我们就找到了与节点 (v) 上的 ) 匹配的 ( 的位置 (p)

    • (lst_v = p)(cnt_v = cnt_{fa_p} + 1),而此时加入节点 (v) 上的括号对答案贡献也即为 (cnt_v) 了,因为以节点 (v) 为结尾的连续的括号块的每一个后缀,都是合法括号串,一共有 (cnt_v) 个后缀。

    • 注意到在跳的过程中,每一个括号块只会被经过一次(要么匹配成功,并建立了一个跨度更大的括号块,要么匹配失败,跳的时候就不会再涉及到该括号块),故时间复杂度 (O(n))

    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <queue>
    
    using namespace std;
    
    const int N = 500100;
    
    int n;
    
    char s[N];
    
    int tot, head[N], ver[N], Next[N];
    
    void add(int u, int v) {
    	ver[++ tot] = v;    Next[tot] = head[u];    head[u] = tot;
    }
    
    int fa[N];
    int lst[N];
    int cnt[N];
    long long k[N];
    
    void bfs() {
    	queue<int> q;
    	q.push(1);
    
    	memset(lst, -1, sizeof(lst));
    
    	while (q.size()) {
    		int u = q.front(); q.pop();
    		for (int i = head[u]; i; i = Next[i]) {
    			int v = ver[i];
    			q.push(v);
    			k[v] = k[u];
    			if (s[v] == '(') continue;
    			int p = u;
    			while (p) {
    				if (lst[p] == -1) {
    					if (s[p] == ')') p = 0;
    					break;
    				}
    				else p = fa[lst[p]];
    			}
    			if (p) {
    				lst[v] = p;
    				cnt[v] = cnt[fa[p]] + 1;
    				k[v] += cnt[v];
    			}
    		}
    	}
    }
    
    int main() {
    	scanf("%d", &n);
    	scanf("%s", s + 1);
    
    	for (int i = 2; i <= n; i ++) {
    		scanf("%d", &fa[i]);
    		add(fa[i], i);
    	}
    
    	bfs();
    
    	long long ans = 0;
    	for (int i = 1; i <= n; i ++)
    		ans ^= k[i] * i;
    
    	printf("%lld
    ", ans);
    
    	return 0;
    }`
    

    D2T1. Emiya 家今天的饭

    题目链接:Link

    • 在不考虑每种主要食材至多在一半的菜中被使用时,答案即为:

    [prodlimits_{i = 1}^n(1 + sumlimits_{j = 1}^m a_{i, j}) - 1 ]

    • 我们可以简单容斥一下,先求出在 " 存在一种主要食材使用次数大于菜的一半 " 情况下的方案数,再与上式做个差即可求出答案。

    • 注意到有且仅有一种主要食材使用次数大于菜的一半,我们可以枚举这个主要食材,记我们枚举的主要食材的编号为 (col),对于每一个主要食材,考虑 dp。

    • (f_{i, j, k}) 表示:在前 (i) 个烹饪方法中,做了 (j) 道菜,且第 (col) 种主要食材用了 (k) 个时的方案数。

    • (S_i = sumlimits_{j = 1}^m a_{i, j}),根据题意,转移有三种:

      1. 不使用第 (i) 个烹饪方法做菜。此时可以从 " 在前 (i - 1) 个烹饪方法中,做了 (j) 道菜,且第 (col) 种主要食材用了 (k) 个 " 转移而来,故该种转移的方案数为 (f_{i - 1, j, k})

      2. 使用第 (i) 个烹饪方法做菜,使用第 (col) 种主要食材。此时可以从 " 在前 (i - 1) 个烹饪方法中,做了 (j - 1) 道菜,且第 (col) 种主要食材用了 (k - 1) 个 " 转移而来,使用第 (i) 个烹饪方法且使用第 (j) 个主要食材可以制作出 (a_{i, col}) 道菜,根据乘法原理,该种转移的方案数为 (f_{i - 1, j - 1, k - 1} ast a_{i, col})

      3. 使用第 (i) 个烹饪方法做菜,不使用第 (col) 种主要食材。此时可以从 " 在前 (i - 1) 个烹饪方法中,做了 (j - 1) 道菜,且第 (col) 种主要食材用了 (k) 个 " 转移而来,使用第 (i) 个烹饪方法且不使用第 (j) 个主要食材可以制作出 (S_i - a_{i, col}) 道菜,根据乘法原理,该种转移的方案数为 (f_{i - 1, j - 1, k} ast (S_i - a_{i, col}))

    • 故有状态转移方程:

    [f_{i, j, k} = f_{i - 1, j, k} + f_{i - 1, j - 1, k - 1} ast a_{i, col} + f_{i - 1, j - 1, k} ast (S_i - a_{i, col}) ]

    • 初态:(f_{i, 0, 0} = 1)
    • 目标:(sumlimits_{j = 1}^nsumlimits_{k = leftlfloorfrac{j}{2} ight floor + 1}^j f_{n, j, k})
    • 直接做 dp 的时间复杂度 (mathcal{O(n^3 m)}),考虑进一步优化。
    • 考虑维度合并,注意到我们只关心 (leftlfloorfrac{j}{2} ight floor)(k) 的差值,并不关心 (j)(k) 的值具体是多少,于是我们可以将 (j) 这一维和 (k) 这一维进行合并。
    • (f_{i, j}) 表示:在前 (i) 个烹饪方法中," 使用第 (col) 种主要食材做的菜数 " 减去 " 不使用第 (col) 种主要食材做的菜数 " 的差值为 (j) 时的方案数。
    • 转移依旧是上述的三种。
    • 简单分析即可得到状态转移方程:

    [f_{i, j} = f_{i - 1, j} + f_{i - 1, j - 1} ast a_{i, col} + f_{i - 1, j + 1} ast (S_i - a_{i, col}) ]

    • 初态:(f_{0, 0} = 1)
    • 目标:(sumlimits_{j = 1}^n f_{n, j})
    • 时间复杂度 (mathcal{O(n^2 m)})
    • 注意到差值 (j) 也有可能是负数,所以我们需要用一个偏移量 (base),使得值域变为非负整数域后再进行处理。
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    inline int read() {
    	int x = 0, f = 1; char s = getchar();
    	while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
    	while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
    	return x * f;
    }
    
    const int N = 110, M = 2010, base = 100;
    
    const int mod = 998244353;
    
    int n, m;
    
    int a[N][M];
    int S[N];
    
    int f[N][N * 2];
    
    int main() {
    	n = read(), m = read();
    
    	for (int i = 1; i <= n; i ++)
    		for (int j = 1; j <= m; j ++)
    			a[i][j] = read();
    
    	for (int i = 1; i <= n; i ++)
    		for (int j = 1; j <= m; j ++)
    			S[i] = (S[i] + a[i][j]) % mod;
    
    	int ans = 1;
    	for (int i = 1; i <= n; i ++)
    		ans = 1ll * ans * (S[i] + 1) % mod;
    	ans = ((ans - 1) % mod + mod) % mod;
    
    	for (int col = 1; col <= m; col ++) {
    		memset(f, 0, sizeof(f));
    		f[0][0 + base] = 1;
    		for (int i = 1; i <= n; i ++)
    			for (int j = -i + base; j <= i + base; j ++) {
    				int val = 0;
    				val = (val + f[i - 1][j]) % mod;
    				if (j) val = (val + 1ll * f[i - 1][j - 1] * a[i][col]) % mod;
    				val = (val + 1ll * f[i - 1][j + 1] * (S[i] - a[i][col])) % mod;
    				val = (val % mod + mod) % mod;
    				f[i][j] = val;
    			} 
    		for (int j = 1 + base; j <= n + base; j ++)
    			ans = ((ans - f[n][j]) % mod + mod) % mod;
    	}
    
    	printf("%d
    ", ans);
    
    	return 0;
    }
    

    D2T2. 划分

    题目链接:Link

    • 考虑 dp,记 (S_i = sumlimits_{j = 1}^i a_j)
    • (f_{i, j}) 表示:考虑到前 (i) 项,划分的最后一段区间为 ((j, i]) 时,能取得的最小的平方和。
    • 显然有状态转移方程:

    [f_{i, j} = minlimits_{0 leq k < j, S_j - S_k leq S_i - S_j} { f_{k, j} + (S_i - S_j)^2 } ]

    • 直接做的时间复杂度为 (mathcal{O(n^3)})
    • 有一个结论:定义决策点 (k) 若满足 (S_j - S_k leq S_i - S_j),则被称为 " 合法 ",对于合法的两个决策点 (k_1, k_2),不妨设 (k_1 < k_2),则决策点 (k_2) 不劣于 (k_1)
    • 证明略,但是看起来就比较显然对不对。
    • 我们可以得知 (f_{i, j}) 的最优决策点是 " 合法 " 的所有决策点中,位置最靠后的那个决策点。
    • 也就是说,当最后若干段尽量小时,能取得的平方和会尽量小。
    • 于是我们大力 dp。
    • 约定变量:
      • (f_i):考虑到前 (i) 项时,能取得的最小的平方和。
      • (dec_i)(f_i) 的最优决策点。
      • (suf_i)(f_i) 中最后一段划分的区间和,其实就是 (S_i - S_{dec_i})
    • 显然有状态转移方程:

    [f_i = minlimits_{0 leq j < i, suf_j leq S_i - S_j} { f_j + (S_i - S_j)^2 } ]

    • 我们重新定义:当决策点 (j) 若满足 (suf_j leq S_i - S_j) ,则被称为 " 合法 ",移项得 (suf_j + S_j leq S_i)
    • 考虑 (f_i) 的最优决策点 (dec_i),根据上述结论,我们知道最优决策点 (dec_i) 是 " 合法 " 的所有决策点中,位置最靠后的那个决策点,注意到 (suf_j + S_j leq S_i) 中的 (S_i) 是单调递增的,说明决策集合只增大不减小,我们可以用一个变量 (p) 表示处理到前 (i) 项时," 合法 " 的所有决策中,位置最靠后的那个。考虑单调队列维护决策,我们维护一个下标 (j) 递增,(suf_j + S_j) 也递增的单调队列。
    • (i = 1 sim n),对于每个 (i) 执行以下三个步骤。
      1. 判断队头决策 (j) 是否满足 (suf_j + S_j leq S_i)。若满足,则令 (p = max(p, j)),将队头出队。
      2. 此时 (p) 就是 (f_i) 的最优决策点 (dec_i)
      3. 不断删除队尾决策 (j),直到决策 (j) 满足 (suf_j + S_i leq suf_j + S_j),然后把 (i) 作为一个新的决策入队。
    • 注意到样例 3 输出 4972194419293431240859891640 ...
    • 一看就是要打高精,时空复杂度都比较紧张,我们在转移的时候,并不用计算出 dp 值 (f_i),只需记录 (f_i) 的最优决策点 (dec_i) 即可,最后从 (n) 倒推回去并计算答案。
    • 发现其实 (S, dec, suf) 数组都还可以开的下,而且 (suf) 也并不必要开数组存。
      只是最后计算答案的时候需要用到高精。
    • 时间复杂度 (mathcal{O(n)})
    • 我比较懒,用的 __int128大家还是好好打高精吧(:
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    inline int read() {
    	int x = 0, f = 1; char s = getchar();
    	while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
    	while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
    	return x * f;
    }
    
    inline void print(__int128 x) {
    	if (x > 9) print(x / 10);
    	putchar('0' + x % 10);
    }
    
    const int N = 40001000, M = 100100;
    
    int n, type;
    
    int a[N];
    
    void makedata() {
    	static int x, y, z, b[N], m, p[M], l[M], r[M], mod = (1 << 30);
    	x = read(), y = read(), z = read(), b[1] = read(), b[2] = read(), m = read();
    	p[0] = 0;
    	for (int i = 1; i <= m; i ++)
    		p[i] = read(), l[i] = read(), r[i] = read();
    	for (int i = 3; i <= n; i ++)
    		b[i] = (1ll * x * b[i - 1] + 1ll * y * b[i - 2] + z) % mod;
    	for (int j = 1; j <= m; j ++)
    		for (int i = p[j - 1] + 1; i <= p[j]; i ++)
    			a[i] = (b[i] % (r[j] - l[j] + 1)) + l[j];
    }
    
    long long S[N];
    
    int l, r;
    int q[N];
    
    int dec[N];
    
    long long suf(int x) {
    	return S[x] - S[dec[x]];
    }
    
    int main() {
    	n = read(), type = read();
    
    	if (type == 1) makedata();
    	else
    		for (int i = 1; i <= n; i ++)
    			a[i] = read();
    
    	for (int i = 1; i <= n; i ++)
    		S[i] = S[i - 1] + a[i];
    
    	l = 1, r = 1;
    	q[1] = 0;
    
    	int p = 0;
    	for (int i = 1; i <= n; i ++) {
    		while (l <= r && S[q[l]] + suf(q[l]) <= S[i]) p = max(p, q[l ++]);
    		dec[i] = p;
    		while (l <= r && S[q[r]] + suf(q[r]) >= S[i] + suf(i)) r --;
    		q[++ r] = i;
    	} 
    
    	__int128 ans = 0;
    	int x = n;
    	while (x) {
    		ans += (__int128) suf(x) * suf(x);
    		x = dec[x];
    	}
    
    	print(ans);
    
    	return 0;
    } 
    

    D2T3. 树的重心

    题目链接:Link

    部分分还是有一些细讲的价值的。

    Test (1 sim 8)

    特殊性质:(n leq 2000)

    • 暴力莽,暴力枚举每一条边的时间复杂度是 (mathcal{O(n)}),暴力求重心的时间复杂度是 (mathcal{O(n)})

    • 暴力求重心大家应该都会吧 ...

    • 时间复杂度 (mathcal{O(n^2)}),这档分还是要吃掉的。

    Test (9 sim 11)

    特殊性质:树的形态为一条链。

    • 当树的形态为一条链的时候,显然该树的重心就是链的中间点,1 个或 2 个。
    • 这时候,我们删掉一条边,树会被分解成两部分,每部分都是一条链,于是取这两部分的中间点计入答案即可。
    • 具体的,设链上第 (i) 个节点的编号为 (p_i)
    • (i = 1 sim n - 1),这时候我们要删掉 ((p_i, p_{i + 1})) 这条边。
      考虑先求 (1 sim i) 部分的重心,对 (i) 的奇偶性讨论一下:
      • (i) 是奇数时,(p_{frac{i + 1}{2}}) 是重心。
      • (i) 是偶数时,(p_{frac{i}{2}})(p_{frac{i}{2} + 1}) 是重心。
    • (i + 1 sim n) 部分的重心,翻转一下 (p) 再做一次即可。
    • 时间复杂度 (mathcal{O(n)}),吃掉这档分还是比较简单的。

    Test (12 sim 15)

    特殊性质:树的形态为满二叉树。

    • 当树的形态为满二叉树的时候,删掉一条边 ((u, v)),不妨设 (u)(v) 的父亲。
    • (v) 的子树内的重心还是很好分析的,显然 (v) 的子树也是满二叉树,故重心即为 (v)
    • 考虑在原树中刨去 (v) 的子树后,重心的分布。不妨设树的深度为 (d)

    photo.png

    • 上图是一棵满二叉树,我们先对 ((u, v)) 在满二叉树的右侧时讨论,左侧同理。
      我们把树分成了 四个部分一个点,设 (v) 的深度为 (p),则各个部分的节点数为:

      • (color{red}A color{black}: 2^{d-2} - 1)
      • (color{yellow}B color{black}: 2^{d-2} - 1)
      • (color{green}C color{black}: 2^{d - 1} - 2^{d - p + 1} + 1)
      • (color{blue}D color{black}: 2^{d - p + 1} - 1)
    • 首先,重心显然不会出现在 (color{red}A)(color{yellow}B) 里,也并不可能出现在 (color{green}C) 中除了根节点以外的节点中。

    • 我们对 (2) 号点(根的其中一个儿子)与根节点进行重点讨论:

      • 删除 (2) 号点后剩下的最大子树:(x = max(2^{d - 2} - 1, 2^{d - 1} - 2^{d - p + 1} + 1))
      • 删除根节点后剩下的最大子树:(y = max(2^{d - 1} - 1, 2^{d - 1} - 2^{d - p + 1}))
    • (p < d) 时,有 (x < 2^{d - 1} - 1)(y = 2^{d - 1} - 1)(x < y),此时 (2) 号点是重心。

    • (p = d) 时,有 (x = 2^{d - 1} - 1)(y = 2^{d - 1} - 1)(x = y),此时 (2) 号点与根节点都是重心。

    • 设根节点为 (a),根节点的左儿子为 (b),根节点的右儿子为 (c)。我们发现,当 ((u, v)) 在满二叉树的右侧时,这条边对答案有 (b) 的贡献,当 ((u, v)) 在满二叉树的左侧时,这条边对答案有 (c) 的贡献,特别地,当 ((u, v)) 中的 (v) 为叶节点时,这条边对答案还有额外的 (a) 的贡献。

    • 经过上述分析,故答案为:

    [frac{n * (n + 1)}{2} - a + (2^{d - 1} - 1) * b + (2^{d - 1} - 1) * c + 2^{d - 1} * a ]

    Solution

    • 不难想到,可以对于每个点 (u),计算 (u) 成为重心时,对答案的贡献。

    • 我们钦定点 (u) 为整棵树的根,现在有一个 (u) 的子节点 (v),我们要从 (v) 的子树中再删去一个大小为 (x) 的小子树,使得 (u) 成为重心。

    • 约定变量:

      • (size_v):以 (u) 为根时,(v) 的子树大小。

      • (size'_v):经过删边后的 (v) 的子树大小,这里实际上 (size'_v = size_v - x)

      • (s):在 (u) 的所有子树中,刨去 (v) 的子树后的总节点数,这里实际上 (s = n - 1 - size_v)

      • (m):在 (u) 的所有子树中,除 (v) 之外的最大子树大小。

    • 我们来分析一下,经过删边后的 (size'_v) 的取值范围:

      • (1)(v) 的子树大小不能比 (u) 的其他子树大小的和加 (1) 还要大,否则重心取 (v) 会更加平衡。则有:

      [size_v' leq s + 1 ]

      • (2)除 (v) 之外的最大子树大小不能比 (u) 其他子树大小的和加 (1) 还要大,否则重心取最大子树的根会更加平衡。则有:

      [m leq s - m + size'_v + 1 ]

    • 经过上述分析,我们可以知道,经过删边后的 (size'_v in [2 ast m - s - 1, s + 1])

    [size_v - x in [2 ast m - s - 1, s + 1] ]

    [-x in [2 ast m - s - 1 - size_v, s + 1 - size_v] ]

    [x in [size_v - 1 - s, size_v + 1 + s - 2 ast m] ]

    [x in [2 ast size_v - n, n - 2 ast m] ]

    • 于是问题转化为 (v) 的子树内有多少个点的子树大小在某个区间范围内,线段树合并直接可以 rush 掉。
    • 显然不能每次都以 (u) 为根重新做一遍线段树合并,我们钦定 (1) 为整棵树的根。
    • 对于 (v)(u) 的子节点,我们可以线段树合并简单统计一下。
      对于 (v)(u) 的父亲节点时,我们再讨论一下边的分布。
    • (l = 2 ast (n - size_u) - n)(r = n - 2 ast m)
    • 对于 (1 sim u) 的路径中的边 ((a, b)),不妨设 (a)(b) 的父亲,当删去 ((a, b)) 这条边时,包含 (a) 的这一块的子树大小为 (n - size_b),反过来我们可以得到,删去 ((a, b)) 这条边会使得 (u) 成为重心当且仅当 (size_b in [n - r, n - l])
    • 对于非 (1 sim u) 的路径中的边 ((a, b)),我们可以简单容斥一下,该类型边的边数即为: " 整棵树中子树大小在 ([l, r]) 内的点的个数 " 减去 " (u) 的子树中子树大小在 ([l, r]) 内的点的个数 " 减去 " (1 sim u) 的路径中子树大小在 ([l, r]) 内的点的个数 "。
    • 结合上述两种情况,我们可以得知,当 (v)(u) 的父亲节点时,满足删去 ((a, b)) 后会使得 (u) 成为重心的边数为:" 整棵树中子树大小在 ([l, r]) 内的点的个数 " 减去 " (u) 的子树中子树大小在 ([l, r]) 内的点的个数 " 减去 " (1 sim u) 的路径中子树大小在 ([l, r]) 内的点的个数 " 加上 " (1 sim u) 的路径中子树大小在 ([n - r, n - l]) 内的点的个数 "。
    1. 对于整棵树的区间数点问题,我们可以预处理出前缀和 (sum_i) 表示 " 在整棵树中,子树大小在 ([1, i]) 内的节点个数 ",从而转化为前缀做差的形式。

    2. 对于子树内的区间数点问题,我们可以用顺手处理的线段树合并计算。

    3. 对于根节点到 (u) 点路径上的区间数点问题,我们可以用一个树状数组实时维护根节点到 (u) 点的子树大小,还是可以用前缀做差的形式计算。

    • 时间复杂度 (mathcal{O(n log n)}),空间复杂度 (mathcal{O(n log n)})
    • 但是好像常数有点大。
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    inline int read() {
    	int x = 0, f = 1; char s = getchar();
    	while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
    	while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
    	return x * f;
    } 
    
    const int N = 300100, M = 600100, MLOGN = 10000000;
    
    int n;
    
    int ovo, head[N], ver[M], Next[M];
    
    void addedge(int u, int v) {
    	ver[++ ovo] = v;    Next[ovo] = head[u];    head[u] = ovo;
    }
    
    // BIT part
    
    int c[N];
    
    void add(int x, int val) {
    	for (; x <= n; x += x & -x) c[x] += val;
    } 
    
    int calc(int x) {
    	int ans = 0;
    	for (; x; x -= x & -x) ans += c[x];
    	return ans;
    }
    
    // SegmentTree part
    
    int tot, root[N];
    struct SegmentTree {
    	int lc, rc;
    	int cnt;
    } t[MLOGN];
    
    int New() {
    	tot ++;
    	t[tot].lc = t[tot].rc = t[tot].cnt = 0;
    	return tot;
    }
    
    void insert(int &p, int l, int r, int delta, int val) {
    	if (!p) p = New();
    	t[p].cnt += val;
    	if (l == r) return;
    	int mid = (l + r) / 2;
    	if (delta <= mid)
    		insert(t[p].lc, l, mid, delta, val);
    	else
    		insert(t[p].rc, mid + 1, r, delta, val);
    }
    
    int merge(int p, int q) {
    	if (!p || !q)
    		return p ^ q;
    	t[p].cnt += t[q].cnt;
    	t[p].lc = merge(t[p].lc, t[q].lc);
    	t[p].rc = merge(t[p].rc, t[q].rc);
    	return p;
    }
    
    int ask(int p, int l, int r, int s, int e) {
    	if (s <= l && r <= e)
    		return t[p].cnt;
    	int mid = (l + r) / 2;
    	int val = 0;
    	if (s <= mid)
    		val += ask(t[p].lc, l, mid, s, e);
    	if (mid < e)
    		val += ask(t[p].rc, mid + 1, r, s, e);
    	return val; 
    }
    
    // solve part
    
    long long ans;
    
    int size[N];
    
    void search(int u, int fa) {
    	size[u] = 1;
    	for (int i = head[u]; i; i = Next[i]) {
    		int v = ver[i];
    		if (v == fa) continue;
    		search(v, u);
    		size[u] += size[v]; 
    	}
    }
    
    long long sum[N];
    
    void dfs(int u, int fa) {
    	int firv = 0, secv = 0;
    
    	for (int i = head[u]; i; i = Next[i]) {
    		int v = ver[i];
    		if (v == fa) continue;
    		if (size[v] > firv) secv = firv, firv = size[v];
    		else if (size[v] > secv) secv = size[v];
    	}
    
    	if (n - size[u] > firv) secv = firv, firv = n - size[u];
    	else if (n - size[u] > secv) secv = n - size[u];
    
    	for (int i = head[u]; i; i = Next[i]) { 
    		int v = ver[i];
    		if (v == fa) continue;
    		add(size[v], 1), dfs(v, u), add(size[v], -1);
    		int m = size[v] == firv ? secv : firv;
    		int l = 2 * size[v] - n, r = n - 2 * m;
    		if (l > n || r < 1 || l > r) {
    			root[u] = merge(root[u], root[v]);
    			continue;
    		}
    		if (l < 1) l = 1;
    		if (r > n) r = n;
    		ans += 1ll * ask(root[v], 1, n, l, r) * u;
    		root[u] = merge(root[u], root[v]);
    	}
    
    	if (u == 1)
    		return;
    
    	int m = n - size[u] == firv ? secv : firv;
    	int l = 2 * (n - size[u]) - n, r = n - 2 * m;
    
    	if (l > n || r < 1 || l > r) {
    		insert(root[u], 1, n, size[u], 1);
    		return;
    	}
    
    	if (l < 1) l = 1;
    	if (r > n) r = n;
    
    	int cnt = 0;
    	cnt += sum[r] - sum[l - 1];
    	cnt -= ask(root[u], 1, n, l, r);
    	cnt -= calc(r) - calc(l - 1);
    
    	l = n - l, r = n - r, swap(l, r);
    	if (l < 1) l = 1;
    	if (r > n) r = n;
    
    	cnt += calc(r) - calc(l - 1);
    
    	ans += 1ll * cnt * u;
    
    	insert(root[u], 1, n, size[u], 1);
    }
    
    void work() {
    	memset(head, 0, sizeof(head));
    	memset(c, 0, sizeof(c));
    	memset(sum, 0, sizeof(sum));
    	memset(root, 0, sizeof(root));
    	ovo = 0, tot = 0, ans = 0;
    
    	n = read();
    
    	for (int i = 1; i < n; i ++) {
    		int u = read(), v = read();
    		addedge(u, v), addedge(v, u); 
    	}
    
    	search(1, 0);
    
    	for (int i = 2; i <= n; i ++)
    		sum[size[i]] ++;
    	for (int i = 2; i <= n; i ++)
    		sum[i] += sum[i - 1];
    
    	dfs(1, 0);
    
    	printf("%lld
    ", ans);
    }
    
    int main() {
    	int T = read();
    
    	while (T --)    work();
    
    	return 0;
    } 
    

    D1T3. 树上的数

    题目链接:Link

    咕咕咕。

    我是不会告诉您其实我不会做这道题的 233。

  • 相关阅读:
    linux 查看安装软件位置(持续跟新)
    POJ 3661 Running (DP思想)
    stringstream的用法
    关于实验室搬家
    POJ 搜索(2)
    Treap的插入删除实现
    碰到一道不错的扩展欧几里德题目 whu 1338
    SRM 144
    关于无向图双连通问题
    POJ 动态规划(2)
  • 原文地址:https://www.cnblogs.com/cjtcalc/p/13580531.html
Copyright © 2011-2022 走看看