24/04/16 樹

2huk發表於2024-04-16

\(\color{red}(-114514)\) P1424 小魚的航程(改進版)

  • 有一隻小魚,它平日每天游泳 \(250\) 公里,週末休息(實行雙休日),假設從周 \(x\) 開始算起,過了 \(n\) 天以後,小魚一共累計游泳了多少公里呢?

太難了,先咕咕咕。

\(\color{red}(1)\) UOJ387 To Do Tree

  • \(n\) 個任務,做第 \(i\) 個任務需要先做第 \(f_i\) 個任務。依賴關係形成了一棵樹,樹根為任務 \(1\)。每天你可以完成 \(m\) 個任務,這 \(m\) 個任務之間不能有依賴關係。求最少的完成所有任務的天數。

  • \(n \le 10^5\)

貪心策略是每次找子樹最大的任務做。

實現上維護一個堆,儲存當前哪些任務可以做但還沒做,按照子樹大小從大到小排序。每次取堆中前 \(m\) 大即可。

$\color{blue}\text{Code}$
struct Tree {
	vector<int> g[N];
	void add(int a, int b) { g[a].push_back(b); }
	vector<int> operator [](const int &u) const { return g[u]; }
}T;

int n, m, fa[N], sz[N];

void Luogu_UID_748509() {
	fin >> n >> m;
	fill(sz + 1, sz + n + 1, 1);
	for (int i = 2; i <= n; ++ i ) {
		fin >> fa[i];
		T.add(fa[i], i);
	}
	
	for (int i = n; i >= 2; -- i ) {
		sz[fa[i]] += sz[i];
	}
	
	priority_queue<PII> q;
	q.emplace(sz[1], 1);
	int tmp = 0;
	vector<vector<int> > ans;
	
	while (tmp < n) {
		vector<int> vec;
		for (int i = 1; i <= m && q.size(); ++ i ) {
			vec.emplace_back(q.top().second);
			q.pop();
			++ tmp;
		}
		ans.emplace_back(vec);
		for (int u : vec) {
			for (int v : T[u]) {
				q.emplace(sz[v], v);
			}
		}
	}
	
	fout << ans.size() << '\n';
	for (vector<int> t : ans) {
		fout << t.size() << ' ' << t;
	}
}

\(\color{red}(2)\) P4211 [LNOI2014] LCA

  • 給定一顆 \(n\) 的節點的樹。\(m\) 次詢問 \(\sum_{i=l}^r \operatorname{depth}_{\operatorname{lca}(i, z)}\)
  • \(n, m \le 5 \times 10^4\)

考慮幾個弱化版本:

  1. \(m\) 次詢問 \(\operatorname{depth}_{\operatorname{lca}(x, y)}\)

顯然可以用樸素做法。這裡的做法是這樣的:

  • \(x\) 到根上每個點加 \(1\),那麼 \(y\) 到根的點權和即答案。原因是 \(x, y\) 到根的公共路徑長度就是它們最近公共祖先的深度。
  • 實現用樹剖解決。
$\color{blue}\text{Code}$
while (m -- ) {
	int x, y;
	scanf("%d%d", &x, &y);
	
	modify(1, x, 1);		// 1 到 x 的路徑加一
	printf("%d\n", query(1, y));		// 1 到 y 的路徑和
	
	modify(1, x, -1);		// 清空 
}
  1. 單次詢問 \(\sum_{i=l}^r \operatorname{depth}_{\operatorname{lca}(i, z)}\)

顯然也可以用樸素做法。這裡我們延續上一問的做法:

  • 對於所有 \(i \in [l, r]\),將 \(i\) 到根上每個點累加 \(1\),那麼 \(z\) 到根的點權和即答案。
$\color{blue}\text{Code}$
int l, r, z;
scanf("%d%d%d", &l, &r, &z);
for (int i = l; i <= r; ++ i ) modify(1, i, 1);		// 1 到 i 的路徑加一
printf("%d\n", query(1, z));		// 1 到 z 的路徑和 
  1. \(m\) 次詢問 \(\sum_{i=\color{red}\mathbf1}^r \operatorname{depth}_{\operatorname{lca}(i, z)}\)

顯然不能用樸素做法了。做法是這樣的:

  • 考慮離線所有詢問。vector 以 \(r\) 做下標,儲存每個詢問的編號和 \(z\)。即 vec[r].push_back(make_pair(i, z))
  • 列舉 \(i = (1, 2, \dots, n)\),並每次將 \(i\) 到根上每個點累加 \(1\)
  • 然後訪問 vector 的 \(i\) 中的所有元素 \((j, z)\),我們將 \(z\) 到根的點權和累加到詢問 \(j\) 的答案中。
$\color{blue}\text{Code}$
int res[N];		// 第 i 問的答案 
vector<pair<int, int> > vec[N]; 

for (int i = 1; i <= m; ++ i ) {
	scanf("%d%d", &a[i].r, &a[i].z);
	vec[a[i].r].push_back({i, a[i].z});
}

for (int i = 1; i <= n; ++ i ) {
	modify(1, i, 1);		// 1 到 i 的路徑加一
	for (pair<int, int> t : vec[i]) {
		int a = t.first, b = t.second;
		res[a] += query(1, b);		// 1 到 i 的路徑和 
	}
}

for (int i = 1; i <= n; ++ i ) printf("%d\n", res[i]); 

  1. \(m\) 次詢問 \(\sum_{i=l}^r \operatorname{depth}_{\operatorname{lca}(i, z)}\),即本題。

上一問差分即可。

$\color{blue}\text{Code}$
#include <bits/stdc++.h>

using namespace std;

#define int long long

const int N = 50010, M = N << 1;

int n, m, fa[N];
int	h[N], e[M], ne[M], idx;

void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

vector<pair<int, int> > vec[N];
int res[N]; 

int dep[N], top[N], son[N], id[N], cnt, sz[N];

void dfs1(int u) {
	dep[u] = dep[fa[u]] + 1;
	sz[u] = 1;
	for (int i = h[u]; ~i; i = ne[i]) {
		int v = e[i];
		dfs1(v);
		sz[u] += sz[v];
		if (sz[u] > sz[son[u]]) son[u] = v;
	}
}

void dfs2(int u, int t) {
	top[u] = t;
	id[u] = ++ cnt;
	if (son[u]) {
		dfs2(son[u], t);
		for (int i = h[u]; ~i; i = ne[i]) {
			int v = e[i];
			if (v != son[u]) dfs2(v, v);
		}
	}
}

struct Tree {
	int l, r, v, tag;
}tr[N << 2];

void pushup(int u) {
	tr[u].v = tr[u << 1].v + tr[u << 1 | 1].v;
}

void calc(int u, int k) {
	tr[u].tag += k;
	tr[u].v += k * (tr[u].r - tr[u].l + 1);
}

void pushdown(int u) {
	calc(u << 1, tr[u].tag), calc(u << 1 | 1, tr[u].tag);
	tr[u].tag = 0;
}

void build(int u, int l, int r) {
	tr[u] = {l, r, 0, 0};
	if (l != r) {
		int mid = l + r >> 1;
		build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
	}
}

void modify(int u, int l, int r) {
	if (tr[u].l >= l && tr[u].r <= r) calc(u, 1);
	else {
		int mid = tr[u].l + tr[u].r >> 1;
		pushdown(u);
		if (l <= mid) modify(u << 1, l, r);
		if (r > mid) modify(u << 1 | 1, l, r);
		pushup(u);
	}
}

int query(int u, int l, int r) {
	if (tr[u].l >= l && tr[u].r <= r) return tr[u].v;
	int res = 0, mid = tr[u].l + tr[u].r >> 1;
	pushdown(u);
	if (l <= mid) res = query(u << 1, l, r);
	if (r > mid) res += query(u << 1 | 1, l, r);
	return res;
}

void Tree_modify(int a, int b) {
	while (top[a] != top[b]) {
		if (dep[top[a]] < dep[top[b]]) swap(a, b);
		modify(1, id[top[a]], id[a]);
		a = fa[top[a]];
	}
	if (dep[a] > dep[b]) swap(a, b);
	modify(1, id[a], id[b]);
}

int Tree_query(int a, int b) {
	int res = 0;
	while (top[a] != top[b]) {
		if (dep[top[a]] < dep[top[b]]) swap(a, b);
		res += query(1, id[top[a]], id[a]);
		a = fa[top[a]];
	}
	if (dep[a] > dep[b]) swap(a, b);
	return res + query(1, id[a], id[b]);
}

signed main() {
	memset(h, -1, sizeof h);
	scanf("%lld%lld", &n, &m);
	for (int i = 2; i <= n; ++ i ) {
		scanf("%lld", fa + i);
		fa[i] ++ ;
		add(fa[i], i);
	}
	
	for (int i = 1; i <= m; ++ i ) {
		int l, r, z;
		scanf("%lld%lld%lld", &l, &r, &z);
		vec[1 + r].push_back({i, 1 + z});
		vec[l].push_back({-i, 1 + z});
	}
	
	dfs1(1), dfs2(1, 0);
	
	build(1, 1, n);
	
	for (int i = 1; i <= n; ++ i ) {
		Tree_modify(1, i);
		for (pair<int, int> t : vec[i]) {
			int a = abs(t.first), b = t.second;
			int k = t.first > 0 ? 1 : -1;
			res[a] += k * Tree_query(1, b);
		}
	}
	
	for (int i = 1; i <= m; ++ i ) printf("%lld\n", res[i] % 201314);
	
	return 0;
}

\(\color{red}(3)\) P2680 [NOIP2015 提高組] 運輸計劃

  • 給定一棵 \(n\) 個點的樹,邊有邊權 \(w_i\)。給定 \(m\) 條路徑 \((u_i,v_i)\)。你可以選擇一條邊,將其邊權變為 \(0\)。最小化這 \(m\) 條路徑長度的最大值。
  • \(n, m \le 3 \times 10^5\)

二分答案 \(mid\)

對於原來路徑長度 \(\le mid\),我們無需考慮。換句話說,我們需要考慮的是長度 \(> mid\) 的路徑。

對於這些路徑而言,我們希望透過僅改變樹上一條邊,讓這些路徑的長度都變得 \(\le mid\)。顯然這條邊需要是這些路徑的交,而且是交中邊權最大的。

找路徑交可以用樹上差分的套路。

找到這條設為 \(0\) 的邊後簡單判斷一下即可。

$\color{blue}\text{Code}$
#include <bits/stdc++.h>

using namespace std;

const int N = 300010, M = N * 2, K = 19;

int n, m;
int h[N], e[M], ne[M], idx, w[M];
int fa[N][K], dep[N], dis[N];
int seq[N], cnt;

struct Path
{
	int a, b, p, d;
}q[N];

void add(int a, int b, int c)
{
	e[idx] = b, ne[idx] = h[a], w[idx] =c, h[a] = idx ++ ; 
}

void dfs(int u, int F, int D)
{
	seq[cnt ++ ] = u;
	dep[u] = D;
	for (int i = h[u]; ~i; i = ne[i])
	{
		int j = e[i];
		if (j == F) continue;
		fa[j][0] = u;
		for (int k = 1; k < K; ++ k )
			fa[j][k] = fa[fa[j][k - 1]][k - 1];
		dis[j] = dis[u] + w[i];
		dfs(j, u, D + 1);
	}
}

int lca(int a, int b)
{
	if (dep[a] < dep[b]) swap(a, b);
	for (int k = K - 1; ~k; -- k )
		if (dep[fa[a][k]] >= dep[b])
			a = fa[a][k];
	if (a == b) return a;
	for (int k = K - 1; ~k; -- k )
		if (fa[a][k] != fa[b][k])
			a = fa[a][k], b = fa[b][k];
	return fa[a][0];
}

int sum[N];

bool chk(int mid)
{
	memset(sum, 0, sizeof sum);
	int c = 0, mx = 0;
	for (int i = 0; i < m; ++ i )
	{
		int a = q[i].a, b = q[i].b, p = q[i].p, d = q[i].d;
		if (d > mid)
		{
			++ c;
			mx = max(mx, d);
			++ sum[a], ++ sum[b], sum[p] -= 2;
		}
	}
	
	if (!c) return true;
	
	for (int i = n - 1; ~i; -- i )
	{
		int j = seq[i];
		sum[fa[j][0]] += sum[j];
	}
	
	for (int i = 1; i <= n; ++ i )
		if (sum[i] == c && mx - dis[i] + dis[fa[i][0]] <= mid)
			return true;
	return false;
}

int main()
{
	memset(h, -1, sizeof h);
	cin >> n >> m;
	
	for (int i = 1; i < n; ++ i )
	{
		int a, b, c;
		cin >> a >> b >> c;
		add(a, b, c), add(b, a, c);
	}
	
	dfs(1, -1, 1);
	
	for (int i = 0; i < m; ++ i )
	{
		int a, b;
		cin >> a >> b;
		int p = lca(a, b);
		int d = dis[a] + dis[b] - dis[p] * 2;
		q[i] = {a, b, p, d};
	}
	
	int l = 0, r = 3e8;
	while (l < r)
	{
		int mid = l + r >> 1;
		if (chk(mid)) r = mid;
		else l = mid + 1;
	}
	cout << l;
	
	return 0;
}

\(\color{red}(4)\) P2486 [SDOI2011] 染色 - 洛谷 | 電腦科學教育新生態 (luogu.com.cn)

  • 給定一棵 \(n\) 個點的樹,每個點上有一個顏色。你需要支援兩種操作:
    1. 將一條鏈 \((x,y)\) 上的點全部染成顏色 \(c\)
    2. 詢問一條鏈 \((x,y)\) 上的點的顏色組成了幾個顏色段。
  • \(n \le 10^5\)

好題!

  • 16:09:寫完,RE

  • 2 min later:計算重兒子時 son[u] = sz[v],但輸出極大值 2088774347。

  • 2 min later:線段樹初始化沒有用樹剖後的編號 id[l] 而是 l

  • 114514 min later:tmd 不做了。

相關文章