P7353 解题报告

· · 题解

前言

模拟赛挂分挂完,直接来补 T4,花半天一路学了点双、圆方树(菜还有理了),但是 @Hoks 的思路对我来说不太直接,所以决定自己重新写一篇。

思路

赛时看到,最先想到割点,打算写个显然假的做法:Tom 在割点就是 Yes,否则就是 No。

但是割点挂了。 正解的确与割点有关。

初始在割点:

一种显然的策略:

  1. Tom 初始在割点。
  2. Tom 每一步走到另一个能使 Jerry 能活动的点双变少的割点。
  3. Tom 最后一步抓住 Jerry。

一些深入思考:

  1. 每个割点连接多个不同点双,策略的可行性取决于 Jerry 所在联通块能不能被 Tom 缩小。
  2. Tom 如果不能每一步都到达一个割点,Jerry 就可以逃跑,这个策略就会寄。

总结关键词:割点,到达,位置——圆方树

一直到这一步应该都是好理解的。那么在原图上建出圆方树。以 Tom 的位置为树根,向 Jerry 所在子树越过方点移动。每次到达的圆点和上一个圆点必须有边连接,不然 Jerry 就会逃跑。

钦定点 1 作为树根,f_ug_u 分别表示点 u 内外子树的可行行,考虑 dp:

内子树 f(第一遍 dfs)

对于每个圆点 u 之于其子树的可行性 f_{u} 如果为 1,那么它的儿子方点的儿子圆点(以下称为孙子圆点 sv)对于 u 必须都在原图上可达即 p(u, sv) = 1(不然 Jerry 跑到这个子树就寄),而且必须有 f_{sv} = 1 。就是说:

\forall sv, f_{sv} \land p(u, sv), f_u = 1

然而这样 dp 是存在缺陷的。考虑下图:

建出圆方树:

简单模拟一下可以得到 f=\{0, 1, 1, 1, 1\}。然而对于询问 1, 221 的子树中,如果直接从 f_1 就会得到答案为 No,不符合实际。究其原因在于 Jerry 只能在编号 7 的点双里移动而到不了 6 去,但 f_1 却是包括了 76 的答案。

解决方法也很简单,把 f 记录的对象从圆点扩大到方点。

对于每个方点 u 之于其子树的可行性 f_{u} 如果为 1,那么它的儿子圆点 v 对于父亲圆点 fa_u 必须都在原图上可达即 p(fa_u, v) = 1,而且必须有 f_{v} = 1 。就是说:

\forall v, f_{v} \land p(fa_u, v), f_u = 1

用这种 dp 得到的 f=\{0, 1, 1, 1, 1, 0, 1\},对于询问 1, 2,查询 2 所在 1 的子树的根(一定是 1 的一个儿子方点)也就是 7,得到 Yes,符合实际。

不过为了下面对 g 的递推方便一些,在这里修改一下 f 的定义:f_u 表示 u 的儿子或孙子圆点符合条件的数量,同时定义辅助数组 son 记录圆点的孙子数和方点的儿子数,dp:

f_u = \sum_{v \in sons_u}[p(u, v) \land(f_v = son_v)]

判断改成 f_u = son_u 即可。

外子树 g(第二遍 dfs)

1. 祖父圆点 $ga$ 的外子树。要求 $ga$ 能到达 $u$ 且 $g_{ga} = 1$。 2. $u$ 的兄弟圆点 $br$。要求 $br$ 能到达 $u$ 且 $f_{br} = son_{br}$。 3. 父亲方点的兄弟方点 $uc$。这些方点不与 $u$ 直接连接,所以不用判断是否能到达。要求 $f_{uc} = son_{uc}$。 第一点,直接设定 $g_u$ 的初值为 $g_{ga} \land p(ga, u)$。 第二点,遍历并判断。 对于第三点中的 $uc$,不用遍历,因为 $\sum f_{uc}=f_{ga}-f_{fa}$,$\sum son_{uc}=son_{ga}-son_{fa}$,所以直接判断 $f_{ga} - f_{fa} = son_{ga} - son_{fa}$ 就行。 不用记录方点,因为外子树只有一个。 ### 初始不在割点 到目前为止都是对初始在割点情况的说明。而对与 Tom 初始不在割点的情况,可以遍历一遍所有圆点看是否有点 $u$ 满足 $f_u = son_u$ 且 $g_u = 1$。 如果有,那不管在不在割点,所有询问都输出 Yes 就可以。因为只要 Tom 到这个点,不管 Jerry 在内子树还是外子树,都可以采用上面的策略。 而如果没有,不管 Tom 移动到哪个点,Jerry 只要移动到 Tom 抓不到的部分去就可以赢得比赛,所以初始不在割点就是 No。 不用特意判断是否在割点。不是割点的点 $f$ 和 $g$ 一定不满足条件。 ## 代码实现 处理询问时会遇到一个问题,就是如何判断 $y$ 是否在 $x$ 的子树内。可以利用树剖的思想,一个子树内的 $dfn$ 一定是连续的,所以在 dfs 处理 $f$,$g$ 的时候顺手处理出 $in$ 和 $out$ 数组,$in$ 数组记录遍历到该点时的 $tim$,$out$ 数组记录离开该点时的 $tim$,那么只要满足 $in_x \le in_y \le out_x$ 就可以判定 $y$ 在 $x$ 的子树内。 而当 $b$ 在 $a$ 子树内时也可以利用这一点,遍历 $a$ 的所有儿子方点 $v$,若 $in_v \le in_b \le out_v$,那么 $v$ 就是可以得出答案的方点。 ```cpp #include <bits/stdc++.h> using namespace std; const string name = "P7353"; const int N = 2e5 + 2; int n, m, q, tim, top, cnt; int dfn[N], low[N], stk[N], fa[N], son[N], in[N], out[N]; int dp[N][2]; set <int> g[N]; vector <int> tr[N]; void tarjan(int u, int fa){ dfn[u] = low[u] = ++ tim; stk[++ top] = u; for(int v : g[u]){ if(!dfn[v]){ tarjan(v, u); low[u] = min(low[v], low[u]); if(low[v] >= dfn[u]){ int r = ++ cnt; tr[r].push_back(u); tr[u].push_back(r); while(stk[top + 1] != v){ int t = stk[top]; tr[r].push_back(t); tr[t].push_back(r); top --; } } }else if(v != fa) low[u] = min(dfn[v], low[u]); } } void dfs1(int u, int f){ in[u] = ++ tim; for(int v : tr[u]){ if(v == f) continue; fa[v] = u; in[v] = ++ tim; for(int vv : tr[v]){ if(vv == u) continue; fa[vv] = v, son[u] ++, son[v] ++; dfs1(vv, v); if(g[u].count(vv) && dp[vv][0] == son[vv]) dp[u][0] ++, dp[v][0] ++; } out[v] = tim; } out[u] = tim; } void dfs2(int u, int f){ if(!f) dp[u][1] = 1; else dp[u][1] = (g[u].count(f) & dp[f][1] & (dp[f][0] - dp[fa[u]][0] == son[f] - son[fa[u]])); for(int v : tr[fa[u]]){ if(v == u || v == f) continue; dp[u][1] &= (g[u].count(v) & (dp[v][0] == son[v])); } for(int v : tr[u]){ if(v == fa[u]) continue; for(int vv : tr[v]){ if(vv == fa[v]) continue; dfs2(vv, u); } } } bool check(int u, int v){ for(int t : tr[u]){ if(t == fa[u]) continue; if(in[t] <= in[v] && in[v] <= out[t]){ return dp[t][0] == son[t]; } } } int main(){ ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> m >> q; cnt = n; for(int i = 1; i <= m; i ++){ int u, v; cin >> u >> v; g[u].insert(v); g[v].insert(u); } tarjan(1, 0); tim = 0; dfs1(1, 0); dfs2(1, 0); for(int i = 1; i <= n; i ++){ if(dp[i][0] == son[i] && dp[i][1]){ while(q --) cout << "Yes\n"; return 0; } } while(q --){ int u, v; cin >> u >> v; if(in[u] <= in[v] && in[v] <= out[u]){ if(check(u, v)) cout << "Yes\n"; else cout << "No\n"; }else{ if(dp[u][1]) cout << "Yes\n"; else cout << "No\n"; } } return 0; } ```