0%

2025“钉耙编程”中国大学生算法设计暑期联赛(6)(2025杭电多校 6)——钥匙迷宫(dfs序)

题目大意

题目大意

给定一棵包含 2n2n 个节点的树,树上分布着 nn 种钥匙(编号 11nn)和 nn 种锁(编号 11nn)。每种钥匙和每种锁在树上恰好出现一次。

节点信息由数组 aa 给出:

  • ai>0a_i > 0,表示该节点拥有编号为 aia_i 的钥匙。

  • ai<0a_i < 0,表示该节点拥有编号为 ai-a_i 的锁。

移动与收集规则

  1. 出发点:必须选择一个拥有钥匙的节点作为起点。

  2. 钥匙获取:到达(或经过)拥有钥匙的节点时,自动获得该钥匙。钥匙永久保留。

  3. 通行限制:只有当手中拥有编号为 kk 的钥匙时,才能进入拥有编号为 kk 的锁的节点。

目标
对于每一种钥匙 ii1in1 \le i \le n),判断:如果从拥有钥匙 ii 的节点出发,是否能够遍历树的足够多部分,从而收集到全部 nn 种钥匙。

输入格式

  • 第一行包含整数 TT,表示测试数据组数。

  • 每组数据包含:

    • 整数 nn
    • 2n2n 个整数 a1,,a2na_1, \dots, a_{2n},表示节点上的物品。
    • 2n12n-1 行,每行两个整数 u,vu, v,表示树边。

输出格式
对于每组数据,输出一个长度为 nn01字符串

  • 字符串第 ii 个字符对应编号为 ii 的钥匙作为起点的结果。

  • 若能收集所有钥匙输出 1,否则输出 0

样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Input
4
1
1 -1
1 2
3
-3 2 1 -1 3 -2
1 2
1 3
2 4
2 5
3 6
3
1 -1 2 -2 -3 3
1 2
2 3
3 4
4 5
5 6
3
1 -3 2 -2 -1 3
1 2
2 3
3 4
4 5
5 6

Output
1
011
000
000

样例解释(以第二组数据为例)

  • 配置n=3n=3
    • 节点 1:锁 3 (-3)
    • 节点 2:钥匙 2 (2)
    • 节点 3:钥匙 1 (1)
    • 节点 4:锁 1 (-1)
    • 节点 5:钥匙 3 (3)
    • 节点 6:锁 2 (-2)
    • 边连接结构:4-2-1-3-6,且 2-5 相连。即 1 是中心,连接 2 和 3;2 连接 4 和 5;3 连接 6。

image

  • 判定过程

    1. 从钥匙 1 出发(位于节点 3)
    • 初始拥有:{钥匙 1}。
    • 相邻节点:节点 1(锁 3,无钥匙 3,不可入),节点 6(锁 2,无钥匙 2,不可入)。
    • 结果:困在节点 3,无法收集所有。对应字符串第 1 位为 0
    1. 从钥匙 2 出发(位于节点 2)
    • 初始拥有:{钥匙 2}。
    • 相邻节点:节点 1(锁 3,不可入),节点 4(锁 1,不可入),节点 5(钥匙 3,可入)。
    • 移动到节点 5,获得钥匙 3。现有:{钥匙 2, 钥匙 3}。
    • 回到节点 2,尝试进入节点 1(锁 3)。因有钥匙 3,可入
    • 到达节点 1 后,可前往节点 3(钥匙 1,可入)。获得钥匙 1。
    • 现有:{钥匙 1, 2, 3}。收集完毕。对应字符串第 2 位为 1
    1. 从钥匙 3 出发(位于节点 5)
    • 初始拥有:{钥匙 3}。
    • 前往节点 2(钥匙 2,可入)。获得钥匙 2。
    • 现有:{钥匙 2, 3}。后续过程同上,可收集完毕。对应字符串第 3 位为 1
  • 最终输出011

思路讲解

首先,我们想到暴力,但是跑一次只需要 O(n)O(n),也要 O(n2)O(n^2) 才能跑完。

于是面对这种复杂的问题,我们想到,需要分而治之。

我们先想,如果只有一个钥匙和一个门,那么合法的点在哪里那?

image

不难发现,肯定是在这边半颗树,也就是有钥匙的子树上的点才是合法的。

那么这个结论是非常显然的,那么现在问题在于我们有非常多的门,其实我们运用一下这个结论就会发现,我们只要不断往门上含有钥匙的子树上走,就行了,最后我们会得到一个联通块,检查一下这个联通块的联通性即可。

AC代码

https://vjudge.net/solution/64200130

心路历程(WA,TLE,MLE……)

那么一开始,我们选择的是纯 dfs 解决该问题,但是 WA 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vll valid;
void dfs(ll u, ll fa) {
ll dfsm;
if (A[u] < 0) {
if (dir[u] <= 0)
return;
valid.clear();
dfs(dir[u], u);
} else {
valid.PB(u);
for (auto v : g[u]) {
if (v == fa)
continue;
dfs(v, u);
}
}
}

主要原因是使用 dfs 时,没有一个全局的信息来参考。

比如下面这组 hack 数据,我的程序会错误地把 2,3 加入 valid 数组中,这个显然是有问题的。

这个其实是因为我们先走了节点 3 这条支路,又走了节点4。

image

于是我们继续在想,用树上差分固然可以解决此问题,但是总觉得有点杀鸡用牛刀了。使用纯 dfs 方案用办法解决此问题吗?

我们发现这个问题在于我们走完一条路了以后,我们需要知道这条路是封闭的,还是会继续往下走的?其实只要是会继续往下走的,其他路也不需要走了

知道了这个以后,我们发现,其实不用改动很多,仅仅只需要加一个返回值,子树返回值为 1 时,我们直接开摆即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// return 1 表示的是可以往下走
// return 0 表示不可以往下走
int dfs(ll u, ll fa) {
ll dfsm;
if (A[u] < 0) {
if (dir[u] <= 0)
return 0;
valid.clear();
dfs(dir[u], u);
return 1;
} else {
valid.PB(u);
for (auto v : g[u]) {
if (v == fa)
continue;
int ret=dfs(v, u);
if(ret){
return 1;
}
}
return 0;
}
}

然后就 AC 了。