2019-2020 XX Open Cup Grand Prix of Korea 虚拟参赛 暨 补题 @ Wings            分类 ACM
发布于 星期一, 十一月 2 日, 2020 年
更新于 星期二, 七月 20 日, 2021 年

自闭训练

记录

分组读题, 22min我过了签到题A.

1h2min蒋叶桢过另一个签到题G.

同时读完了所有题.

我做完A后想H, 张炀杰在想C, 蒋叶桢做完G后也开始想C.

H大致方向没错(转化为01序列, 逆序对), 可就是想不出来.

蒋叶桢C也卡了, 不会实现.

然后大家去想J, 不会.

时间一分一秒过去, 我们摸了3h的鱼. 最后1h, 大家都放弃了, 全员开始推箱子.

自闭 :(.

总结

我太菜了, 净给队友拖后腿.

UPD(2020-11-5): H其实很简单, 当时可能是因为没有休息好, 脑子不在状态.

部分题解(补题)

自闭, 不想补, 过几天再来

UPD 2020-11-5:

按难度排序(可能)

A. 6789

题目

给出一个$n \times m$的"扑克牌"矩阵, 其中扑克牌只有"6", “7”, “8”, “9”.

可以旋转任意位置的扑克牌, 使扑克牌矩阵中心对称. 求最小旋转次数.

($6$与$9$中心对称, $8$与$8$中心对称, $7$只能和中心旋转后的$7$中心对称)

$1 \le n, m \le 500$

题解

直接模拟即可, 判断$(i, j)$和$(n - i + 1, m - j + 1)$(中心对称位置)的牌是否可以通过旋转某一张来使得其对称. 注意中心点只有$8$可满足条件. 详见代码.

复杂度$O(n^2)$

代码
const int maxn = 510;

int n, m, ans = 0;
char str[maxn][maxn];

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)
		scanf("%s", str[i] + 1);
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			int ii = n - i + 1, jj = m - j + 1;
			int a = str[i][j] - '0', b = str[ii][jj] - '0';
			if (i == ii && j == jj) {
				if (a != 8) {
					ans = -1;
					break;
				}
			}
			else {
				if (a == 6) {
					if (b == 6)
						ans++;
					else if (b != 9) {
						ans = -1;
						break;
					}
				}
				else if (a == 9) {
					if (b == 9)
						ans++;
					else if (b != 6) {
						ans = -1;
						break;
					}
				}
				else if (a == 8) {
					if (b != 8) {
						ans = -1;
						break;
					}
				}
				else if (a == 7) {
					if (b != 7) {
						ans = -1;
						break;
					}
					else
						ans++;
				}
			}
		}
		if (ans == -1)
			break;
	}
	if (ans == -1)
		puts("-1");
	else
		printf("%d\n", ans/2);
	return 0;
}

F. Hilbert’s Hotel

题目

背景是希尔伯特的旅馆问题.

一个旅馆有无限个房间, 从$0$开始编号, 一开始都住满了人, 这些人设为第$0$组. 随后会有其他旅客组团入住旅馆. 如果下一组有$k$个旅客入住, 那么原先住在$x$号房间的人移至$x+k$号房间, 空出$0$到$k-1$号房间给新入住的旅客; 如果下一组有无限个旅客入住, 那么原先住在$x$号房间的旅客移至$2x$号房间, 空出$2x+1$号房间给新入住的旅客.

$Q (1 \le Q \le 3 \cdot 10^5)$次操作:

  • 1 k $k \ne 0$时, 有$k$个旅客入住; $k = 0$时, 有无现个旅客入住. $0 \le k \le 10^9$.
  • 2 g x 查询第$g$组旅客第$x$小房号模$10^9 + 7$. $1 \le x \le 10^9$, 保证存在解
  • 3 x 查询房号为$x$的房间住的是第几组的旅客. $0 \le x \le 10^9$

题解

询问$2, 3$是独立的, 先看看$2$怎么做吧.

由于给定了组号, 并且各组之间也是独立的, 每组第$x$小的房间号是否可以用一个函数写出来呢? 答案显然是可以! 一开始第$0$组的函数为$g_0(x) = x$, 如果新来了$k_1$个旅客, 那么原来的函数都加上一个$k_1$, 比如$g_0(x)$变为$g_0(x) = x + k_1, (0 \le x)$, 新来的这一组函数为$g_1(x) = x, (0 \le x < k_1)$. 如果新来了无限个旅客, 那么原来的函数都乘以$2$, 比如$g_0(x) = 2x + 2k_1, (0 \le x)$, $g_1(x) = 2x, (0 \le x < k_1)$, 新来的这一组函数为$g_2(x) = 2x + 1, (0 \le x)$. 这样对于查询$2$, 就可以很方便地代值计算得到答案了.

结合题目和这些函数, 可以发现, 函数是一次函数. 又由于要对前缀进行区间修改, 可以考虑使用线段树, 那么找一个特定的组号就是单点查询. 进一步发现, 一次函数中, $x$的系数一定是$2$的次幂, 所以我们在保存节点信息时, 可以保存系数的指数, 以及截距. 答案需要取模, 所以截距可以取模保存. 因为是区间修改, 还需要打个lazy tag.

对于询问$3$, 先来考虑暴力的做法. 直接存哪个房价住的是哪些人是不现实的, 但是我们可以"回溯". 即给出的$x$, 去考虑上一个操作$1$, 如果是入住$k$个人, 那么这个查询就相当于没有上一个操作$1$, 然后查询3 (x-k). 当然如果$x-k$是负数, 那么显然这个$x$就是属于上一个操作$1$所修改的, 即答案为上一个操作$1$的组号. 入住无限个旅客也是同理, 让$x$除以$2$, 如果$x$是奇数, 那么就是属于上个操作$1$所修改的, 否则就可以看成在上一个操作$1$之前的查询3 x/2. 因为每个操作$1$, 对应每一组, 所以一直回溯, 便可找到相应的组了.

这样复杂度是$O(Q)$的, 加上查询就是$O(Q^2)$, 显然不行. 但是这个做法有优化的空间. 如果有一段连续的加$k$, 设他们的和为$sum$, 那么就可以直接$O(1)$判断答案是不是在这一段连续的加中. 即, 如果$x' = x - sum < 0$, 则在, 否则不在. 在的话可以二分, $O(logQ)$找到具体是哪一个点让他小于$0$. 不在的话, 说明需要再往前找. 连续一段加的前面一个是乘以$2$, 那么首先看$x'$的奇偶性, 像之前说的那样判断是找到了还是需要继续回溯.

记录乘以$2$出现的位置, 即可直接找到一段连续的加(见代码p2[]).

维护一段连续的加, 需要的是后缀和. 其实可以在操作的过程中$O(Q)$维护前缀和$pre[]$, 后缀和可以通过两个前缀和的差得到.

这样查询$3$的复杂度就只和乘以$2$的次数有关. 每次到乘以$2$的点都会让$x$减半, 那么查询$3$的复杂度就是$O(logx + logQ)$

需要特别注意的是$x = 0$(或者在回溯的过程中$x' = 0$)的情况. 如果此时前面都是乘以$2$的操作, 那么他会一直跑完这些操作, 这样复杂度会被卡成$O(Q^2)$, 所以我们需要特判$x = 0$(或$x' = 0$)的情况: 对于连续的乘以$2$, 直接跳到最前面第一个乘以$2$的操作即可. 可$O(Q)$在过程中预处理(见代码last_2[], gid[]).

总复杂度$O(Q(logQ + logx)) \approx O(QlogQ)$

代码

(过程中需要用到G-1, 所以我的G从$1$开始; 第$x$小是直接代入, 所以$x-1$, 当然也可以初始化一次函数截距b为$10^9 + 6$, $x$直接代入)

const int maxn = 3e5+10;
const int P = 1e9+7;

int Plus(LL a, LL b) {
	return a + b >= P ? (a + b) % P : a + b;
}

int Mult(LL a, LL b) {
	return a * b >= P ? a * b % P : a * b;
}

struct SegmentTreeNode {
	int l, r, mid, pk, b;
} tree[maxn << 2];

int n, pw[maxn], G = 1, last_2[maxn], cnt = 0, gid[maxn];
LL pre[maxn];
vector<int> p2;

void Build(int v, int l, int r) {
	tree[v] = SegmentTreeNode{l, r, (l+r)>>1, 0, 0};
	if (l == r)
		return;
	else {
		Build(LCH(v), l, tree[v].mid);
		Build(RCH(v), tree[v].mid+1, tree[v].r);
	}
}

void Init() {
	pw[0] = 1;
	for (int i = 1; i <= n; i++)
		pw[i] = Mult(pw[i-1], 2);
	Build(1, 1, n);
}

void UpdateNode(int v, int pk, int b) {
	tree[v].b = Mult(pw[pk], tree[v].b);
	tree[v].b = Plus(tree[v].b, b);
	tree[v].pk += pk;
}

void Down(int v) {
	UpdateNode(LCH(v), tree[v].pk, tree[v].b);
	UpdateNode(RCH(v), tree[v].pk, tree[v].b);
	tree[v].pk = tree[v].b = 0;
}

void Update(int v, int l, int r, int pk, int b) {
	if (tree[v].l == l && tree[v].r == r)
		UpdateNode(v, pk, b);
	else {
		Down(v);
		if (l > tree[v].mid)
			Update(RCH(v), l, r, pk, b);
		else if (r <= tree[v].mid)
			Update(LCH(v), l, r, pk, b);
		else {
			Update(LCH(v), l, tree[v].mid, pk, b);
			Update(RCH(v), tree[v].mid+1, r, pk, b);
		}
	}
}

int Query(int v, int p, int x) {
	if (tree[v].l == tree[v].r)
		return Plus(Mult(pw[tree[v].pk], x), tree[v].b);
	Down(v);
	if (p > tree[v].mid)
		return Query(RCH(v), p, x);
	else
		return Query(LCH(v), p, x);
}

int main() {
	scanf("%d", &n);
	Init();
	pre[1] = 0;
	last_2[1] = -1;
	for (int i = 1; i <= n; i++) {
		int op;
		scanf("%d", &op);
		if (op == 1){
			int k;
			scanf("%d", &k);
			if (k) {
				Update(1, 1, G++, 0, k);
				pre[G] = pre[G-1] + k;
				last_2[G] = 0;
			}
			else {
				Update(1, 1, G++, 1, 0);
				Update(1, G, G, 1, 1);
				pre[G] = 0;
				last_2[G] = last_2[G-1] ? last_2[G-1] : G;
				gid[G] = cnt++;
				p2.push_back(G);
			}
		}
		else if (op == 2){
			int g, x;
			scanf("%d%d", &g, &x);
			printf("%d\n", Query(1, g+1, x-1));
		}
		//下面写的和狗屎一样丑, 能力有限, 见谅
		else {
			int x;
			scanf("%d", &x);
			int cur = G, res = 1, L = 2, R = G, flag = 1;
			for (int i = p2.size() - 1; ~i; i--) {
				// 等于0也可回溯上一步
				if (x - pre[cur] >= 0) {
					x -= pre[cur];
					if (x & 1) {
						res = p2[i];
						flag = 0;
						break;
					}
					else {
						if (x == 0) {
							cur = last_2[p2[i]];
							i = gid[cur];
						}
						x >>= 1;
						cur = p2[i] - 1;
						R = cur;
					}
				}
				else {
					L = p2[i] + 1, R = cur;
					break;
				}
			}
			if (flag) {
				while (L <= R) {
					int mid = (L + R) >> 1;
					if (x - (pre[cur] - pre[mid-1]) < 0) {
						L = mid + 1;
						res = mid;
					}
					else
						R = mid - 1;
				}
			}
			printf("%d\n", res-1);
		}
	}
	return 0;
}

G. Lexicographically Minimum Walk

题目

给出一张$n$个点$m$条边的有向图, 边权为互不相等的正整数. 找一条$s$到$t$的路, 使得路上边权依次组成的数组字典序最小.

如果路无限长, 输出"TOO LONG"

如果不存在$s$到$t$的路, 输出"IMPOSSIBLE"

$1 \le n \le 10^5, 0 \le m \le 3 \cdot 10^5$

题解

显然贪心.

但直接贪心可能会走到某个不通向$t$的点, 然后跑不到$t$. 但其实可以跑其他路到$t$.

为了避免这样的情况, 需要找一个子图, 保证子图上的每一条边都可以跑到$t$, 在子图上贪心即可.

如何寻找这样的子图? 先建一个"反图", 从$t$跑一遍, 记录能跑到的边, 这些边即是可以通向$t$的边.

然后依照这些边建立一张新的图, 边依据边权排序, 然后dfs第一条边即可.

注意有环就退出.

复杂度$O(n + mlogm)$

代码
const int maxn = 1e5+10;
const int maxm = 3e5+10;

int n, m, s, t, vis[maxn];
vector<int> G[maxm], G2[maxm], ans;

struct Edge {
	int u, v, c, flag;
} edges[maxm];

bool cmp( int i, int j) {
	return edges[i].c < edges[j].c;
}

void dfs(int v) {
	vis[v] = 1;
	for (auto i : G2[v]) {
		Edge &e = edges[i];
		e.flag = 1;
		if (!vis[e.u])
			dfs(e.u);
	}
}

bool dfs2(int u) {
	if (u == t)
		return true;
	vis[u] = 2;
	Edge &e = edges[G[u][0]];
	if (vis[e.v] == 2)
		return false;
	ans.push_back(e.c);
	return dfs2(e.v);
}

void Print() {
	for (auto x : ans)
		printf("%d ", x);
	puts("");
}

int main() {
	scanf("%d%d%d%d", &n, &m, &s, &t);
	for (int i = 1; i <= m; i++) {
		int u, v, c;
		scanf("%d%d%d", &u, &v, &c);
		edges[i] = Edge{u, v, c, 0};
		G2[v].push_back(i);
	}
	dfs(t);
	for (int i = 1; i <= m; i++) {
		Edge &e = edges[i];
		if (e.flag) {
			G[e.u].push_back(i);
		}
	}
	if (!G[s].size())
		puts("IMPOSSIBLE");
	else {
		for (int i = 1; i <= n; i++)
			sort(G[i].begin(), G[i].end(), cmp);
		if (dfs2(s))
			Print();
		else
			puts("TOO LONG");
	}
	return 0;
}

H. Maximizer

题目

给出长度为$n$的两个全排列$a, b$, 一次操作能交换$a$中两个相邻的元素, 求最小交换次数, 使得$\sum_{i=1}^n \mid a_i - b_i \mid$最大.

题解

先来考虑$n$为偶数的情况. 要使得$\sum_{i=1}^n \mid a_i - b_i \mid$最大, 可以发现, $a, b$对应的元素$a_i, b_i$, 应该满足$a_i \le \frac{n}{2}, b_i > \frac{n}{2}$, 或者$a_i > \frac{n}{2}, b_i \le \frac{n}{2}$. 证明显然, 略(懒).

所以可以转化成$01$序列: $x \le \frac{n}{2}$的为$0$, 否则为$1$. 然后将$b$取反, 就得到了$a$的目标$a'$. “交换相邻元素"等价于求逆序对, 这个转化方法适用于所有序列, 包括有可重元素的数列, 当然包括$01$序列.

但是这里的$a'$不是按顺序排的, 所以直接求$a$的逆序对是不可行的.

下面有两种解决方法, 第一是构造一个最优的排列, 求逆序对.

将$a'$的下标数组$p_{a'}$取出, 按照贪心, 对$a$求一个排列$p$. 如:

$$ \begin{aligned} a = &\{1, 1, 0, 1, 0, 0\} \newline a' = &\{0, 1, 0, 0, 1, 1\} \end{aligned} $$

$a'$的下标取出

$$ \begin{aligned} a' = &\{0, 1, 0, 0, 1, 1\} \newline p_{a'} = &\{1, 2, 3, 4, 5, 6\} \end{aligned} $$

那么:

  • $a$的第一个数是$1$, 找到$a'$中第一个$1$的下标, 为$2$, 所以$p$的第一个元素为$2$;
  • $a$的第二个数是$1$, 找到$a'$中第二个$1$的下标, 为$5$, 所以$p$的第二个元素为$5$;
  • $a$的第二个数是$0$, 找到$a'$中第一个$0$的下标, 为$1$, 所以$p$的第二个元素为$1$

$\dots$

这样就得到了一个全排列$p$, 对$p$求逆序对, 即是交换次数.

奇数暂不讨论, 往下看有.

复杂度$O(nlogn)$

第二种方法:

只有$0$和$1$, 根据上述贪心方法, 可以发现, 其实我们是把$a$中第$i$个$1$和$a'$中第$i$个$1$“配对”. $0$同理.

简单证明一下:

不失一般性, 以$1$的配对为例. 如果不是这样, 那么在交换的过程中某一个$1$会"跨过"另一个$1$去配对, 被"跨过"的$1$位置已经改变, 如果改变以后朝着他的配对位置移动, 则答案不变; 如果朝着配对位置相反的方向移动, 则需要多移动一次, 不如直接让这个被跨过的$1$去配对当前$1$的配对位置.

证毕.

由于只有$01$序列, 我们只要把$1$配对了, $0$一定也是配对的. 并且这样的配对方式不会影响到其他$1$的移动次数, 所以我们只需要对每个一$1$, 计算他在两个数组中下标的差即可.

$n$是奇数的情况就很好考虑了. $\lceil \frac{n}{2} \rceil$既可以与小的配对, 也可以与大的配对, 因为和谁配对都不会改变答案. 所以我们让他分别是$1, 0$做两遍, 去最小答案即可.

复杂度$O(n)$

代码

(没写$O(nlogn)$的傻子做法)

const int maxn = 2.5e5 + 10;

int n, a[maxn], b[maxn];
LL ans;
vector<int> sa, sb;

LL sol() {
	LL res = 0;
	sa.clear();
	sb.clear();
	for (int i = 1; i <= n; i++) {
		if (a[i])
			sa.push_back(i);
		if (b[i])
			sb.push_back(i);
	}
	for (int i = 0; i < sa.size(); i++)
		res += llabs(sa[i] - sb[i]);
	return res;
}

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%d", a + i);
	for (int i = 1; i <= n; i++)
		scanf("%d", b + i);
	if ((n&1) == 0) {
		for (int i = 1; i <= n; i++) {
			a[i] = (a[i] > n/2);
			b[i] = (b[i] <= n/2);
		}
		ans = sol();
	}
	else {
		int pa = -1, pb = -1;
		for (int i = 1; i <= n; i++) {
			if (a[i] == n/2 + 1)
				pa = i;
			if (b[i] == n/2 + 1)
				pb = i;
			a[i] = a[i] > n/2 + 1;
			b[i] = b[i] <= n/2;
		}
		a[pa] = 1, b[pb] = 1;
		ans = sol();
		a[pa] = 0, b[pb] = 0;
		ans = min(ans, sol());
	}
	printf("%lld\n", ans);
	return 0;
}

J. Parklife

题目

有一根长度为$10^6$的线段$[1, 10^6]$和$n$个小线段$[l_i, r_i]$, 他们要么有包含关系, 要么不相交(可以相接). 每个线段有一个权值$v_i$. 选择一些线段, 这些线段满足包含关系层次不超过$k$(即每一个点, 属于不超过$k$个小线段), 同时使选择的小线段的$\sum v_i$最大. 对于每个$k \in [1, n]$, 输出最大值.

$1 \le n \le 2.5 \cdot 10^5, 1 \le l, r \le 10^6, 1 \le v \le 10^9$

题解

很自然地(zzs原话), 发现线段只有包含关系, 可以构成一棵树. 我们把最大的线段当成根, 权值为$0$即可. 由于需要对每个$k$都输出答案, 所以可以考虑dp.

先来看这样的一个状态: $dp(i, j)$表示对于子树$i$, 层次不超过$j$的最大值, 显然不能用这样的方程, 他的复杂度是$O(n^2)$. 不过我们可以从中考虑一些问题.

当$j=1$时, 我们把方程写过一下: $f(i)$表示子树$i$选一层的最大值, 转移方程为:

$$f(u) = max \{ v(u), \sum_{v \in son_u} f(v) \} $$

发现没有, 其实我们在算$k(j) = 1$的时候, 已经"算到了”$k = 2$的值(不管最大值是哪一个, 次大值加上$k=1$的值[就是这里做出来的最大值]就是$k=2$的值了), 只不过我们把他"放弃"了. 实际上我们可能还放弃了$k=3$甚至更多的值(这里也指"增量").

如果把这些值合理利用, 是不是就能够只跑一次了呢?

不妨这样想, 对每一个节点, 开一个大根堆, 维护的是$k$递增取值时的"增量", 即:

  1. 堆中的第一个元素是取一层得到的值
  2. 第二个元素是取两层在取一层上多加上的值
  3. 以此类推

如何维护这样一个堆呢?

和$dp$一样, 把这个堆当成$dp$的值即可, 它也是从儿子转移过来的. 让儿子的堆的对应位置相加, 然后再把当前节点的$v$丢进去就行了.

证明:

先证儿子对应位置相加的正确性: 由于各个儿子的选择是互相独立的, 而且儿子的堆中元素是排好了序的, 那么对应位置相加以后得到的新序列也是排好了序的, 所以对应位置直接相加即可.

再证丢进去一个$v$可行: 每做一个节点, 都新增一层, 所以把当前的$v$丢进堆里是必要的. 由于子树中, 各深度节点的选择是独立的, 所以根据贪心, 无论$v$在哪一个地方, 只要比子树的某些值更优, 即可选上$v$, $k$递增的时候再去选其他值($v$最小当然最后选$v$即可).

做完以后, 根的堆的前缀和就是答案了. 当然堆中元素很可能不满$n$个, 超过堆大小的$k$, 就已经可以全部选上了. 这些$k$的答案就是堆中所有元素的和(其实也就是所有节点的和).

合并堆的时候记得用启发式合并.

还有最后一个问题: 如何建树?

将线段按$l$从小到大, 长度从大到小(实际上可看成$r$, 因为$l$相等才比较$r$)排序, 可以发现, 这个顺序就是树的前序遍历. 开一个栈维护一条链: 如果栈顶线段不包含当前线段, 证明这两个线段不是父子关系, 那么栈顶出栈, 直到栈顶和当前线段是父子关系, 连边. 当前点入栈. 这样就可以$O(n)$建树了.

复杂度小于$O(nlog^2 n)$, 因为启发式合并的$O(logn)$跑不满. (官方题解证明了复杂度其实是$O(nlogn)$, 看不懂)

(官方题解太阴间, O(n^2)的dp一顿数学分析, 结果和我的做法一样…)

据说还有长链剖分的算法?? 有空学, 咕咕咕.

代码
const int maxn = 2.5e5+10;

struct Segment {
	int l, r, w;
	bool operator < (const Segment &S) const {
		return l == S.l ? r > S.r : l < S.l;
	}
} segments[maxn];

int n;

vector<int> son[maxn];
vector<int> stk;

void Build() {
	stk.push_back(0);
	for (int i = 1; i <= n; i++) {
		Segment &s = segments[i];
		int fa = stk.back();
		while (!(segments[fa].l <= s.l && s.r <= segments[fa].r)) {
			stk.pop_back();
			fa = stk.back();
		}
		son[fa].push_back(i);
		stk.push_back(i);
	}
#ifdef D
	printf("Tree:\n");
	for (int i = 0; i <= n; i++) {
		printf("%d:\n", i);
		for (auto v : son[i])
			printf("%d ", v);
		puts("");
	}
#endif
}

MaxHeap heap[maxn];
int idx = 0;

// 启发式合并
int Merge(int x, int y) {
	vector<LL> pool;
	if (heap[x].size() > heap[y].size())
		swap(x, y);
	while (!heap[x].empty()) {
		pool.push_back(heap[x].top() + heap[y].top());
		heap[x].pop(), heap[y].pop();
	}
	for (auto t : pool)
		heap[y].push(t);
	return y;
}

// 返回的是当前点的堆(的下标)
int dfs(int u) {
	if (!son[u].size()) {
		heap[++idx].push(segments[u].w);
		return idx;
	}
	int cur = dfs(son[u][0]);
	for (int i = 1; i < son[u].size(); i++) {
		int p = dfs(son[u][i]);
		cur = Merge(cur, p);
	}
	heap[cur].push(segments[u].w);
	return cur;
}

int main() {
	scanf("%d", &n);
	segments[0] = Segment{1, 1000000, 0};
	for (int i = 1; i <= n; i++) {
		int l, r, w;
		scanf("%d%d%d", &l, &r, &w);
		segments[i] = Segment{l, r, w};
	}
	sort(segments, segments + n + 1);
	Build();
	int p = dfs(0);
	LL ans = 0;
	for (int i = 1; i <= n; i++) {
		if (!heap[p].empty()) {
			ans += heap[p].top();
			heap[p].pop();
		}
		printf("%lld ", ans);
	}
	return 0;
}

K. Wind of Change

题目

给出两棵大小为$n$的树, 带边权, 节点标号为$1 ~ n$. 对于每一个节点$i$, 找到一个节点$j$, 使得在两棵树中, $i$到$j$的距离和最小. 求$j$.

$2 \le n \le 2.5 \cdot 10^5, 1 \le w \le 10^9$

题解

距离先转为和$lca$有关的问题.

对两棵树点分治, 得到两棵重构的点分树. 这样一个点的$lca$最多只有$O(logn)$个.

先形式化一下, 设$T1, T2$分别为两棵树, $dist(T, L, i)$为树$T$中$i$的祖先$L$到$i$的距离.

对于每一个$i$, 枚举其两棵树上的祖先的组合$(L1, L2), L1 \in T1, L2 \in T2$, 然后我们求两棵以$L1, L2$为根的子树$T1(L1), T2(L2)$中, 共有的节点$j$, 求出到$L1$及$L2$距离和最小的$j, j \ne i$. 这样就求得了最小的$dist(T1, L1, j) + dist(T2, L2, j)$. 然后再对每一个$i$求一下$\min \{ dist(T1, L1, i) + dist(T2, L2, i), L1 \in T1, L2 \in T2 \}$

可以发现, 我们要求的$\min \{ dis(T1, L1, i) + dist(T2, L2, i) \}$和$\min \{ dis(T1, L1, j) + dist(T2, L2, j) \}$的形式是一模一样的. 所以, 我们不妨一起做: 对每个$i$枚举两个祖先的组合$(L1, L2)$, 先计算$dist(T1, L1, i) + dist(T2, L2, i)$. 再找到$dist(T1, L1, j) + dist(T2, L2, j)$最小值和次小值以及他们所对应的点. 如果取得最小值的点不是$i$, 那就加上最小值; 如果是$i$, 那就加上次小值.

$L$的大小至少为$2$(否则他是叶子, 叶子不可能是某个节点的祖先), 所以一定有最小值和次小值, 上述做法可行.

复杂度$O(nlog^2n)$

具体实现:

点分学习笔记

上面的点分是没有边权的, 只是把一棵树的结构重新构造了一下(还没做过题, 不知道重构会影响什么, 不会影响什么). 但是这里有边权, 我们重构的过程中, 甚至连边都重构了. 那么怎么办呢?

实际上, 我们不需要每一条边的权值, 所以不需要保存整棵树的结构, 需要的是$dist(T, L, i)$, 也就是重构的树中, 节点$i$到其祖先的距离. 找到一个重心以后, 从他开始dfs, 可以得到这棵树中每一个点到重心的距离. 而重心一定后代的祖先, 所以保存所有后代及其到重心的距离即可. 然后深搜儿子进行点分的时候, 某些后代(除了儿子以外的后代)又会新增祖先(子树的重心)及距离. 这样虽然没有保存重构树的结构 —— 根本不需要全部的结构 —— 但是保存了真正需要的东西: 祖先

仔细思考上述内容, 又可以得出结论: 重心一定是所有后代有的祖先(没有后代就不用管他). 那么我们在对第二棵树进行点分的过程中, 实际上已经在枚举$L2$了, 也就是重心$c$. 然后, 对于$c(L2)$的所有子代$v$, 遍历他在$T1$中的祖先$a$, 这样就枚举了$(L1(a), L2(c))$. 可以对每一个$a$开一个小根堆, 把$dis(T1, L1, i) + dist(T2, L2, i)$压入堆中. 做完这一步, 就可以开始找最小和次小了.

重新枚举$v$和$a$, 先找最小, 如果就是$v$再找次小.

由于只需要小根堆的前两个值, 所以不必用堆 —— 可以直接维护最小值和次小值, 这样就把堆的$O(logn)$减下来了.

注意对每一个$c$开始更新之前, 要重置"堆".

然后就做完啦啦啦!!!

代码
typedef pair<int, LL> PIL;
typedef pair<LL, int> PLI;

const int maxn = 2.5e5+10;

struct HeapTwo {
	PLI val[2];
	void Reset() {
		val[0] = val[1] = make_pair(INF, -1);
	}
	void Push(PLI x) {
		if (x < val[0])
			val[1] = val[0], val[0] = x;
		else if (x < val[1])
			val[1] = x;
	}
	LL Top(int u) {
		return val[u == val[0].second].first;
	}
} heap[maxn];

int n, size[maxn], vis[maxn];
vector<PIL> T[2][maxn], descendant, anscentant[maxn];

LL ans[maxn];

// 维护size用来点分, 同时记录点到根的距离.
void dfs(int k, int u, int f, LL dis) {
	size[u] = 1;
	descendant.emplace_back(u, dis);
	for (auto e : T[k][u]) {
		int v = e.first;
		LL w = e.second;
		if (v != f && !vis[v]) {
			dfs(k, v, u, dis + w);
			size[u] += size[v];
		}
	}
}

int Center(int k, int u) {
	// 每一次求重心的时候, 要重新求子代, 注意清空
	descendant.clear();
	dfs(k, u, 0, 0);
	int tot_size = descendant.size();
	for (auto des : descendant) {
		int is_center = 1;
		int x = des.first;
		for (auto e : T[k][x]) {
			int v = e.first;
			if (vis[v])
				continue;
			// size[x] > size[v] == v 是 x 的儿子
			if (size[x] > size[v] && (size[v] << 1) > tot_size) {
				is_center = 0;
				break;
			}
			// size[x] < size[v] == v 是 x 的父亲
			if (size[x] < size[v] && ((tot_size - size[x]) << 1) > tot_size) {
				is_center = 0;
				break;
			}
		}
		if (is_center) {
			// 找到重心, 需要以重心为根, 求一下子代到根(这个祖先)的距离
			descendant.clear();
			dfs(k, x, 0, 0);
			return x;
		}
	}
	return -1;
}

void Decomp0(int k, int u) {
	int c = Center(k, u);
	for (auto des : descendant)
		// c 为所有子代的一个祖先
		anscentant[des.first].emplace_back(c, des.second);
	// 删除c, 做剩下的森林
	vis[c] = 1;
	for (auto e : T[k][c]) {
		int v = e.first;
		if (!vis[v])
			Decomp0(k, v);
	}
	// 深搜, 可回溯, 节省空间(虽然不是很必要)
	vis[c] = 0;
}

void Decomp1(int k, int u) {
	int c = Center(k, u);
	// 先初始化堆
	for (auto des : descendant) {
		int v = des.first;
		for (auto ansc : anscentant[v]) {
			int a1 = ansc.first;
			heap[a1].Reset();
		}
	}
	// 对所有(L1, L2), 把所有 dist(T1, c, v) + dist(T2, a, v) 压入堆
	// 得到每个(L1, L2)的最小值和次小值
	for (auto des : descendant) {
		int v = des.first;
		LL dis2 = des.second;
		for (auto ansc : anscentant[v]) {
			int a1 = ansc.first;
			LL dis1 = ansc.second;
			heap[a1].Push(make_pair(dis1 + dis2, v));
		}
	}
	// 更新答案
	for (auto des : descendant) {
		int v = des.first;
		LL dis1 = des.second;
		for (auto ansc : anscentant[v]) {
			int a1 = ansc.first;
			LL dis2 = ansc.second;
			ans[v] = min(ans[v], dis1 + dis2 + heap[a1].Top(v));
		}
	}
	// 继续点分
	vis[c] = 1;
	for (auto e : T[k][c]) {
		int v = e.first;
		if (!vis[v])
			Decomp1(k, v);
	}
	vis[c] = 0;
}

int main() {
	scanf("%d", &n);
	for (int k = 0; k < 2; k++)
		for (int i = 1; i < n; i++) {
			int u, v;
			LL w;
			scanf("%d%d%lld", &u, &v, &w);
			T[k][u].emplace_back(v, w);
			T[k][v].emplace_back(u, w);
		}
	Decomp0(0, 1);
	for (int i = 1; i <= n; i++)
		ans[i] = INF;
	Decomp1(1, 1);
	for (int i = 1; i <= n; i++)
		printf("%lld\n", ans[i]);
	return 0;
}
留下昵称和邮箱, 可在第一时间获悉回复通知哦~

2021 FLAG

  • 找个妹子
  • 进计科
  • XCPC拿块金牌
  • 补全算法知识, 整全板子
  • 学会Web开发相关知识
  • 在服务器上搭建电子书库
  • 写个游戏并上线
  • 能弹一首曲子
  • 写首完整的曲子
  • 练习悠悠球

个人简介

我叫 Wings, 来自江西上饶, 目前人在西安, 是西电的一名学生.

常以 WingsWingsZengWingsWings的ID在各大小网站上游走, 一般来说, Wings不是我 😔, WingsZeng 一定是我 😊.

热爱算法, 喜欢钻研各种计算机技术.

业余爱好广泛, 只要不是文化课基本上都感兴趣😏.

开发/项目经历

  1. Android游戏 小墨滴的复仇 (弃坑)
  2. Android游戏 Circle Run (弃坑)
  3. Windows游戏 Snague (可能弃坑了吧)
  4. Python后端 Fathy' (可能弃坑了吧)

to be continued

教育经历

时间 学历 学校
2008-2014 小学 上饶市第十二小学
2014-2017 初中 上饶市第四中学
2017-2020 高中 上饶市第一中学
2020-2024 本科 西安电子科技大学
to be continued

比赛/竞赛经历

太久远太小的记不到了…

  1. 2017 国学竞赛初赛江西 没有分数或排名 二乙
  2. 2018 NOIP提高 258 省二
  3. 2019 CSP-S江西专场 145 省二
  4. 2019 数学竞赛初赛 70 没排名 (复赛打铁qaq)
  5. 2020 Gitee|Python贪吃蛇魔改大赛 可能是第四? 二等奖
  6. 2020 西电ACM训练基地熊猫杯 第四 银牌
  7. 2020 西安三校微软学生俱乐部Hackathon 和二等奖最后一名差0.5分 三等奖
  8. 2020 西电星火杯 三等奖
  9. 2020 西电ACM新生赛 第九 金牌
  10. 2020 ICPC 亚洲区域赛 济南站 132名 铜牌
  11. 2020-2021 第二届全国大学生算法设计与编程挑战赛(冬季赛) 924名 铜牌 (别骂了别骂了)
  12. 2020 ICPC 亚洲区域赛 昆明站 打星
  13. 2020 ICPC Asia-East Continent Final 签完到溜 打铁
  14. 西电"智能星"第一届自动驾驶小车比赛 第五 优胜奖|极速奖 本来可以冠军的别骂了别骂了
  15. 2021团体程序设计天体赛(CCCC) 个人二等奖
  16. 2021 西电 miniL CTF 优胜奖
  17. 2021 西电ACM校赛 第9名 金牌
  18. 2021 西电数模校赛 二等奖
  19. 2021 第15届IEEE 第48名
  20. 2021 CCPC 桂林站 打星

to be continued

爱好

技术

  • 算法
  • 独立游戏开发

游戏

  • Minecraft
  • Black Survival
  • I Wanna
  • Celeste
  • Life is Strange
  • Need for speed

运动

  • 篮球
  • 桌球
  • 乒乓球
  • 羽毛球
  • 慢跑

音乐

  • 吉他
  • 词曲
  • 流行

玩具

  • 魔方
    • 三阶速拧
    • 三阶盲拧
    • 高阶
  • yoyo球

追星

  • VAE
  • Benedict Cumberbatch