P9597 [JOI Open 2018] 猫或狗 题解
这道题的交互只是为了强制在线而已,算不上真正意义上的交互题。
动态 dp 板板题。
题意简述:
给定一棵树,刚开始每个点都没有被染色,每次操作可以将一个未染色结点染为红色或蓝色,或是将一个已染色的结点变为无色。每次操作后需要求出:在树上最少删去多少条边可以使得每一个联通块内不会同时出现红色和蓝色的结点。
先考虑暴力:
每次修改完后在树上跑一遍树形 dp。令 $dp_{i, 0/1/2}$ 表示在以编号为 $i$ 的结点为根结点的子树中,根节点所属联通块的颜色为无色、红色、蓝色的情况下,最少需要删多少条边可以使得当前子树满足条件。
转移方程很容易写出来:
$$ \begin{matrix} dp_{u, 0} = \begin{cases} \sum\limits_{v\in son_{u}}\min(dp_{v, 0}, dp_{v, 1} + 1, dp_{v, 2} + 1)&color_{u} = 0\\ \infty&color_{u} \not= 0 \end{cases}\\\\ dp_{u, 1} = \begin{cases} \sum\limits_{v\in son_{u}}\min(dp_{v, 0}, dp_{v, 1}, dp_{v, 2} + 1)&color_{u} = 1\\ \infty&color_{u} \not= 1 \end{cases}\\\\ dp_{u, 2} = \begin{cases} \sum\limits_{v\in son_{u}}\min(dp_{v, 0}, dp_{v, 1} + 1, dp_{v, 2})&color_{u} = 2\\ \infty&color_{u} \not= 2 \end{cases}\\ \end{matrix} $$
上面的式子中 $son_{u}$ 表示结点 $u$ 的子结点集合,$color_{u}$ 表示结点 $u$ 的颜色,$0$ 表示无色,$1$ 表示红色,$2$ 表示蓝色,若某个 $dp_{u, 0/1/2}$ 为 $\infty$ 则表示这种状态不可能出现。
观察这个式子,其实可以做出一些小优化,比如 $dp_{u, 0}$ 的转移一定不是最优的,可以合并到另外两个转移中。
重新定义 $dp_{u, 0/1}$ 表示在以编号为 $i$ 的结点为根结点的子树中,根节点所属联通块的颜色没有蓝色或没有红色的情况下,最少需要删多少条边可以使得当前子树满足条件。
转移方程就会简化为:
$$ \begin{matrix} dp_{u, 0} = \begin{cases} \sum\limits_{v\in son_{u}}\min(dp_{v, 0}, dp_{v, 1} + 1)&color_{u} \not= 2\\ \infty&color_{u} = 2 \end{cases}\\\\ dp_{u, 1} = \begin{cases} \sum\limits_{v\in son_{u}}\min(dp_{v, 0} + 1, dp_{v, 1})&color_{u} \not= 1\\ \infty&color_{u} = 1 \end{cases} \end{matrix} $$
这一点常数优化在正解里非常重要,因为这样做的的话单次广义矩阵乘法就可以从 $27$ 次运算降到 $8$ 次运算,大约优化了 $3$ 倍的常数。
上面的做法时间复杂度是 $\mathcal{O}(QN)$ 的,无法通过。
接下来就是动态 dp 的解释了:
我们定义广义矩阵乘法 $A \times B = C$ 满足:
$$ C_{i, j} = \min\limits_{1 \leqslant k \leqslant M}\left\{A_{i, k} + B_{k, j}\right\} $$
其中 $A$ 矩阵为 $N \times M$ 的矩阵, $B$ 矩阵为 $M \times R$ 的矩阵,$C$ 矩阵为 $N \times R$ 的矩阵。
可以发现这种运算仍然满足结合律。
我们可以把上面的转移方程改成广义矩阵乘法的形式:
$$ \begin{bmatrix} dp_{u', 0} & dp_{u', 0} + 1\\ dp_{u', 1} + 1 & dp_{u', 1} \end{bmatrix} \times \begin{bmatrix} dp_{v, 0}\\ dp_{v, 1} \end{bmatrix} = \begin{bmatrix} dp_{u, 0}\\ dp_{u, 1} \end{bmatrix} $$
上面的 $dp_{u', 0/1}$ 是表示还没有转移 $u$ 的子结点 $v$ 之前的 $u$ 的答案。或者可以理解为将 $v$ 从 $u$ 的子结点中去掉后,$u$ 的答案。
每次修改一个结点就相当于把从它到根结点的链上所有的转移矩阵修改了,暴力修改肯定是不行的,可以重链剖分过后做到每次最多修改 $\log N$ 个转移矩阵。
具体的话就是维护每个结点除开重儿子的转移矩阵,额外定义 $g_{u, 0/1}$ 表示不考虑重儿子的情况下的答案,跟上面的 $dp_{u', 0/1}$ 差不多。修改的时候把经过的重链末端的结点的转移矩阵更改了就行。
关于动态 dp 的部分不再细讲了,如果不会动态 dp 的话建议先去做模板题,里面的题解也有详细的解释。
注意每次修改节点颜色状态后,如果结点有颜色,一定要把转移矩阵中不可能出现的那一行全部设为 $\infty$,这样才能避免错误转移。
给出代码实现:
// #include "catdog.h"
#include <bits/stdc++.h>
using namespace std;
const int inf = 100000;
int n, m, u, v, add[2], sub[2], color[100005], f[100005][2], _g[100005][2];
int cnt, fa[100005], siz[100005], dep[100005], son[100005], dfn[100005], top[100005], rnk[100005], bot[100005];
vector<int> e[100005];
struct matrix {
int v[2][2];
matrix(int val = inf) {
v[0][0] = val, v[0][1] = inf;
v[1][0] = inf, v[1][1] = val;
}
matrix operator = (const matrix& _) {
v[0][0] = _.v[0][0], v[0][1] = _.v[0][1];
v[1][0] = _.v[1][0], v[1][1] = _.v[1][1];
return *this;
}
matrix operator * (const matrix& _) const {
matrix ret;
ret.v[0][0] = min(v[0][0] + _.v[0][0], v[0][1] + _.v[1][0]);
ret.v[0][1] = min(v[0][0] + _.v[0][1], v[0][1] + _.v[1][1]);
ret.v[1][0] = min(v[1][0] + _.v[0][0], v[1][1] + _.v[1][0]);
ret.v[1][1] = min(v[1][0] + _.v[0][1], v[1][1] + _.v[1][1]);
return ret;//循环展开后的广义矩阵乘法
}
} ans, before, after, g[100005];
struct Segment_Tree {//线段树维护转移矩阵
matrix val[400005];
int pos, L, R, l[400005], r[400005];
#define lc (k << 1)
#define rc ((k << 1) | 1)
#define mid ((l[k] + r[k]) >> 1)
void push_up(int k) {
val[k] = val[lc] * val[rc];
}
void build(int k) {
if(l[k] == r[k]) {
val[k] = g[rnk[l[k]]];
return;
}
l[lc] = l[k], r[lc] = mid, l[rc] = mid + 1, r[rc] = r[k];
build(lc), build(rc);
push_up(k);
}
void change(int k) {
if(l[k] == r[k]) {
val[k] = g[rnk[pos]];
return;
}
if(pos <= mid) change(lc);
else change(rc);
push_up(k);
}
matrix ask(int k) {
if(L <= l[k] && r[k] <= R) return val[k];
matrix ret(0);
if(L <= mid) ret = ret * ask(lc);
if(R > mid) ret = ret * ask(rc);
return ret;
}
void Change(int Pos) {
pos = Pos;
return change(1);
}
matrix Ask(int l, int r) {
L = l, R = r;
return ask(1);
}
} tree;
//树链剖分
void dfs1(int now) {
siz[now] = 1, dep[now] = dep[fa[now]] + 1;
for(const auto& i : e[now]) {
if(i != fa[now]) {
fa[i] = now;
dfs1(i);
siz[now] += siz[i];
if(siz[i] > siz[son[now]]) son[now] = i;
}
}
}
void dfs2(int now) {
++cnt; dfn[now] = cnt, rnk[cnt] = now;
if(now == son[fa[now]]) top[now] = top[fa[now]];
else top[now] = now;
if(son[now]) {
dfs2(son[now]);
bot[now] = bot[son[now]];
f[now][0] += min(f[son[now]][0], f[son[now]][1] + 1);
f[now][1] += min(f[son[now]][0] + 1, f[son[now]][1]);
}
else bot[now] = now;
for(const auto& i : e[now]) {
if(i != fa[now] && i != son[now]) {
dfs2(i);
f[now][0] += min(f[i][0], f[i][1] + 1);
f[now][1] += min(f[i][0] + 1, f[i][1]);
_g[now][0] += min(f[i][0], f[i][1] + 1);
_g[now][1] += min(f[i][0] + 1, f[i][1]);
}
}
g[now].v[0][0] = _g[now][0], g[now].v[0][1] = _g[now][0] + 1;
g[now].v[1][0] = _g[now][1] + 1, g[now].v[1][1] = _g[now][1];
}
//修改转移矩阵
void change(int now, int val) {
g[now].v[0][0] = _g[now][0], g[now].v[0][1] = _g[now][0] + 1;
g[now].v[1][0] = _g[now][1] + 1, g[now].v[1][1] = _g[now][1];
color[now] = val;
if(color[now]) g[now].v[color[now] - 1][0] = g[now].v[color[now] - 1][1] = inf;//把不可能出现的一行设为inf
while(now) {
before = tree.Ask(dfn[top[now]], dfn[bot[now]]);
tree.Change(dfn[now]);
after = tree.Ask(dfn[top[now]], dfn[bot[now]]);
now = fa[top[now]];
add[0] = min(after.v[0][0], after.v[0][1]), sub[0] = min(before.v[0][0], before.v[0][1]);
add[1] = min(after.v[1][0], after.v[1][1]), sub[1] = min(before.v[1][0], before.v[1][1]);
_g[now][0] += min(add[0], add[1] + 1) - min(sub[0], sub[1] + 1), _g[now][1] += min(add[0] + 1, add[1]) - min(sub[0] + 1, sub[1]);
g[now].v[0][0] = _g[now][0], g[now].v[0][1] = _g[now][0] + 1;
g[now].v[1][0] = _g[now][1] + 1, g[now].v[1][1] = _g[now][1];
if(color[now]) g[now].v[color[now] - 1][0] = g[now].v[color[now] - 1][1] = inf;//不可能出现的状态
}
}
//下面就是交互了
void initialize(int N, std::vector<int> A, std::vector<int> B) {
n = N;
for(int i = 2; i <= n; ++i) {
u = A[i - 2], v = B[i - 2];
e[u].push_back(v);
e[v].push_back(u);
}
dfs1(1);
dfs2(1);
tree.l[1] = 1, tree.r[1] = n;
tree.build(1);
}
int cat(int v) {
change(v, 1);
ans = tree.Ask(dfn[top[1]], dfn[bot[1]]);
return min(min(ans.v[0][0], ans.v[0][1]), min(ans.v[1][0], ans.v[1][1]));
}
int dog(int v) {
change(v, 2);
ans = tree.Ask(dfn[top[1]], dfn[bot[1]]);
return min(min(ans.v[0][0], ans.v[0][1]), min(ans.v[1][0], ans.v[1][1]));
}
int neighbor(int v) {
change(v, 0);
ans = tree.Ask(dfn[top[1]], dfn[bot[1]]);
return min(min(ans.v[0][0], ans.v[0][1]), min(ans.v[1][0], ans.v[1][1]));
}
本文来自博客园,作者:A_box_of_yogurt,转载请注明原文链接:https://www.cnblogs.com/A-box-of-yogurt/p/18016411