CSP2019 提升組題解

\(sroce : 100 + 100 + 10 + 12 + 24 + 40 = 286\),還需繼續加油鴨!ios

題解順序按本人心中的難度順序升序排序。c++

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\)spa

  • 對於任意的 \(n(n \geq 2)\),此時有兩種狀況:code

    • \(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 \times k_1) \ \text{xor} \ (2 \times k_2) \ \text{xor} \ (3 \times k_3) \ \text{xor} \ ⋯ \ \text{xor} \ (n \times 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 \neq -1\) 時,令 \(p = fa_{lst_p}\)
    • \(p = 0\) 時,表示匹配失敗,結束匹配。
  • \(p \neq 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\n", ans);

	return 0;
}`

D2T1. Emiya 家今天的飯

題目連接:Link

  • 在不考慮每種主要食材至多在一半的菜中被使用時,答案即爲:

\[\prod\limits_{i = 1}^n(1 + \sum\limits_{j = 1}^m a_{i, j}) - 1 \]

  • 咱們能夠簡單容斥一下,先求出在 " 存在一種主要食材使用次數大於菜的一半 " 狀況下的方案數,再與上式作個差便可求出答案。

  • 注意到有且僅有一種主要食材使用次數大於菜的一半,咱們能夠枚舉這個主要食材,記咱們枚舉的主要食材的編號爲 \(col\),對於每個主要食材,考慮 dp。

  • \(f_{i, j, k}\) 表示:在前 \(i\) 個烹飪方法中,作了 \(j\) 道菜,且第 \(col\) 種主要食材用了 \(k\) 個時的方案數。

  • \(S_i = \sum\limits_{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\)
  • 目標:\(\sum\limits_{j = 1}^n\sum\limits_{k = \left\lfloor\frac{j}{2}\right\rfloor + 1}^j f_{n, j, k}\)
  • 直接作 dp 的時間複雜度 \(\mathcal{O(n^3 m)}\),考慮進一步優化。
  • 考慮維度合併,注意到咱們只關心 \(\left\lfloor\frac{j}{2}\right\rfloor\)\(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\)
  • 目標:\(\sum\limits_{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\n", ans);

	return 0;
}

D2T2. 劃分

題目連接:Link

  • 考慮 dp,記 \(S_i = \sum\limits_{j = 1}^i a_j\)
  • \(f_{i, j}\) 表示:考慮到前 \(i\) 項,劃分的最後一段區間爲 \((j, i]\) 時,能取得的最小的平方和。
  • 顯然有狀態轉移方程:

\[f_{i, j} = \min\limits_{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 = \min\limits_{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\n", ans);
}

int main() {
	int T = read();

	while (T --)    work();

	return 0;
}

D1T3. 樹上的數

題目連接:Link

咕咕咕。

我是不會告訴您其實我不會作這道題的 233。

相關文章
相關標籤/搜索