## Happy Card #贪心 #二分 首先看到如此恶心的 T1 大脑先未响应半小时再说。接着不难发现 **炸** 和 **三带一** 可以合并成额外带的牌没有任意限制的 **三带一**。 考虑贪心。显然尽量使用 **三带一** 是最优的,因为这种方法可以一次去掉 $4$ 张牌,就算影响到了剩余牌的奇偶性,也能视作丢掉 $3$ 张牌。 所以我们可以尝试求 $\sum\limits_{i=1}^n \left\lfloor\cfrac{a_i}{3}\right\rfloor$,即可以打出 **三带一** 的最多次数。但是打完后剩余的牌不能小于这个数,否则带不上单牌。所以考虑二分 **三带一** 的次数,在满足次数的情况下剩余牌数量不能小于 **三带一** 的次数。 ```cpp bool check(LL x) { LL need = x, res = 0; for (LL i = 1; i <= n; i++) { LL add = a[i] / 3; if (add <= need) { need -= add; res += a[i] % 3; } else { res += a[i] - need * 3; need = 0; } } if (need || x > res) { return 0; } return 1; } ``` 现在我们得到了可以打出的 **三带一** 的最多次数,模拟一遍后得到打出 **三带一** 中的 **三个同样的牌** 的剩余牌。现在我们需要打出 **三带一** 对应的单牌。 另外两种出牌方式是 **单牌** 和 **对子**。对于 $x$ 张剩余的牌,它需要最少 $\left\lfloor\cfrac{x}{2}\right\rfloor$ 的次数才能打完。所以我们打完 **三带一** 所对应的单牌后,奇数的数量要尽量少。 贪心地考虑,可以先把所有奇数的牌打掉。如果还剩奇数张牌要丢,那么就只能额外加一个奇数了。所以分类讨论打的单牌能否打掉所有的奇数牌即可。 最后我们先计算出 $\sum\limits_{i=1}^n\left\lfloor\cfrac{a_i}{2}\right\rfloor$(如果最后打掉奇数的还剩下 $res$ 个需要打出,那么这个值可以减掉 $\left\lfloor\cfrac{res}{2}\right\rfloor$),而最后奇数的数量是可以计算出来的。加起来再加上最初 **三带一** 的数量即可得到答案。 > [!question] Q & A > > **Q**:既然我们是随便选的牌打三带一中的三张,那么有没有可能会导致剩余的牌当中奇数的数量达不到最优? > > **A**:把 $-3$ 从 $x$ 移到 $y$,$x$ 和 $y$ 的奇偶性会同时改变,所以剩下的奇数数量一定是一样的。 ```cpp int main() { cin.tie(0)->sync_with_stdio(0); for (cin >> t; t; t--) { cin >> n; LL sum = 0; for (LL i = 1; i <= n; i++) { cin >> a[i]; sum += a[i]; } LL tim = 0, need = 0; for (LL l = 0, r = sum; l <= r; ) { LL m = (l + r) / 2; if (check(m)) { tim = m; l = m + 1; } else { r = m - 1; } } for (LL i = 1; i <= n; i++) { LL add = a[i] / 3; if (need + add <= tim) { need += add; a[i] %= 3; } else { a[i] -= (tim - need) * 3; need = tim; } } LL cnt = 0, ans = 0; for (LL i = 1; i <= n; i++) { cnt += a[i] % 2; ans += a[i] / 2; } if (tim < cnt) { cout << tim + (cnt - tim) + ans << '\n'; // 减掉部分奇数的 } else { cout << tim + ans - (tim - cnt) / 2 << '\n'; // 减掉所有奇数的,再减掉额外的(两次可以减小一次答案) } } return 0; } ``` ## Speaker #树形dp #树链剖分 #线段树 #ST表 #最近公共祖先 #前缀和 #倍增 首先我们把 $x\rightarrow z\rightarrow y$ 的路线看成在 $x\rightarrow y$ 的路径上取一个点 $p$,使得 $x\rightarrow p$ 然后从 $p$ 往返到 $z$、最后从 $p\rightarrow y$ 的路线最近。 为什么一定是这样呢?不能从 $p$ 直接走到 $y$ 而不回到 $x\rightarrow y$ 路径上吗?当然不行,这样子两点间就会有超过一种走法,不符合树的结构。 于是问题就转化成了在 $x\rightarrow y$ 的路径上取一点 $p$,使得另外任意一点 $z$ 有 $a_x+2\times d_{(p,x)}$ 最大。答案最后再加上 $d_{(x,y)}$,因为这是一定要走的部分。 整个路径上每个点的信息并很好维护,但是我们需要求出对于每个点 $i$ 的值 ${} \max\limits_{j=i}^n \left(a_j+2\times d_{(i,j)}\right) {}$ 才可以维护。 这个值可以通过树形 DP 和换根解决。首先先考虑 $j$ 在 $i$ 子树中的情况,那么对于 $f_i$(初始化为 $a_i$),枚举 $i$ 的儿子 $j$,令 $f_i\gets\max(f_i,f_j+2\times w_{(i,j)})$。 ```cpp void dfs1(LL x, LL f) { down[x] = a[x]; for (auto i : e[x]) { if (i.first == f) { continue; } pre[i.first] = pre[x] + i.second; dfs1(i.first, x); down[x] = max(down[x], down[i.first] - 2 * i.second); } } ``` 然后考虑 $j$ 不在 $i$ 子树中的情况(为了方便规定 $j=i$ 合法),设最大值为 $g_i$。那么对于 $i$,$j$ 可以: - 存在于它的兄弟的子树当中。遍历父亲时,对儿子的 $f$ 值做前缀后缀最大值就可以对儿子转移时查询。 - 不存在于它的父亲的子树当中,即要向上至少两层。那么可以通过父亲的 $f$ 值转移过来。 这两个 DP 时间复杂度都是 $\mathcal O(n)$ 的。 ```cpp void dfs2(LL x, LL f) { up[x] = max(up[x], a[x]); vector<LL> son, val, pre, suf; for (auto i : e[x]) { if (i.first == f) { continue; } son.push_back(i.first); val.push_back(i.second); pre.push_back(down[i.first] - 2 * i.second); suf.push_back(down[i.first] - 2 * i.second); } for (LL i = 1; i < pre.size(); i++) { pre[i] = max(pre[i], pre[i - 1]); } for (LL i = (LL)suf.size() - 2; i >= 0; i--) { suf[i] = max(suf[i], suf[i + 1]); } for (LL i = 0; i < son.size(); i++) { up[son[i]] = max(i > 0 ? pre[i - 1] : (LL)-1e18, i < son.size() - 1 ? suf[i + 1] : (LL)-1e18) - 2 * val[i]; up[son[i]] = max(up[son[i]], up[x] - 2 * val[i]); dfs2(son[i], x); } } ``` 对于 $i$,$\max\left(f_i,g_i\right)$ 就是答案。 现在我们需要维护路径上的答案。首先可以想到重链剖分。由于不带修,用 ST 表维护 dfs 序所对应的序列的区间 $\max$。单次查询时间复杂度 $\mathcal O\left(\log_2 n\right)$。当然如果你大脑残废写了线段树把查询复杂度干到了 $\mathcal O\left(\log^2 n\right)$ 好像也可以过。 不过倍增也是可以的,额外维护每个节点向上跳 $2^k$ 步经过的点的最大值也是可以的。时间复杂度 $\mathcal O\left(\log_2 n\right)$。 维护 $1$ 到每个节点的路径长度,顺带求 LCA 来查询 $x\rightarrow y$ 的路程。 总时间复杂度 $\mathcal O\left(n+q\log_2 n\right)$。 ```cpp void prework1(LL x, LL f) { fa[x] = f; dep[x] = dep[f] + 1; siz[x] = 1; for (auto i : e[x]) { if (i.first == f) { continue; } prework1(i.first, x); siz[x] += siz[i.first]; if (siz[i.first] > siz[son[x]]) { son[x] = i.first; } } } void prework2(LL x, LL t) { top[x] = t; dfn[x] = ++cnt; if (!son[x]) { return; } prework2(son[x], t); for (auto i : e[x]) { if (i.first == fa[x] || i.first == son[x]) { continue; } prework2(i.first, i.first); } } LL query(LL l, LL r) { LL k = lg[r - l + 1]; return max(dp[l][k], dp[r - (1 << k ) + 1][k]); } LL solve(LL x, LL y) { LL res = 0, len = pre[x] + pre[y]; for (; top[x] != top[y]; x = fa[top[x]]) { if (dep[top[x]] < dep[top[y]]) { swap(x, y); } res = max(res, query(dfn[top[x]], dfn[x])); } if (dep[x] > dep[y]) { swap(x, y); } res = max(res, query(dfn[x], dfn[y])); return res - (len - 2 * pre[x]); } int main() { cin.tie(0)->sync_with_stdio(0); cin >> n >> q; for (LL i = 1; i <= n; i++) { cin >> a[i]; } for (LL i = 1, u, v, w; i < n; i++) { cin >> u >> v >> w; e[u].emplace_back(v, w); e[v].emplace_back(u, w); } dfs1(1, 0); dfs2(1, 0); prework1(1, 0); prework2(1, 1); for (LL i = 1; i <= n; i++) { dp[dfn[i]][0] = max(down[i], up[i]); } for (LL j = 1; j < kLog; j++) { for (LL i = 1; i + (1 << j) - 1 <= n; i++) { dp[i][j] = max(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]); } } for (LL i = 2; i <= n; i++) { lg[i] = lg[i / 2] + 1; } for (LL x, y; q; q--) { cin >> x >> y; cout << a[x] + a[y] + solve(x, y) << '\n'; } return 0; } ``` ## Monotonic Queue #序列dp #单调栈 不会,写的爆搜,甚至答案计算都是抄的题面上的。成功获得 $4$ 分。 ## XA-Game #矩阵 #快速幂 不会,暴力都没打,成功 $0$ 分。