Happy Card

贪心 二分

首先看到如此恶心的 T1 大脑先未响应半小时再说。接着不难发现 三带一 可以合并成额外带的牌没有任意限制的 三带一

考虑贪心。显然尽量使用 三带一 是最优的,因为这种方法可以一次去掉 张牌,就算影响到了剩余牌的奇偶性,也能视作丢掉 张牌。

所以我们可以尝试求 ,即可以打出 三带一 的最多次数。但是打完后剩余的牌不能小于这个数,否则带不上单牌。所以考虑二分 三带一 的次数,在满足次数的情况下剩余牌数量不能小于 三带一 的次数。

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;
}

现在我们得到了可以打出的 三带一 的最多次数,模拟一遍后得到打出 三带一 中的 三个同样的牌 的剩余牌。现在我们需要打出 三带一 对应的单牌。

另外两种出牌方式是 单牌对子。对于 张剩余的牌,它需要最少 的次数才能打完。所以我们打完 三带一 所对应的单牌后,奇数的数量要尽量少。

贪心地考虑,可以先把所有奇数的牌打掉。如果还剩奇数张牌要丢,那么就只能额外加一个奇数了。所以分类讨论打的单牌能否打掉所有的奇数牌即可。

最后我们先计算出 (如果最后打掉奇数的还剩下 个需要打出,那么这个值可以减掉 ),而最后奇数的数量是可以计算出来的。加起来再加上最初 三带一 的数量即可得到答案。

Q & A

Q:既然我们是随便选的牌打三带一中的三张,那么有没有可能会导致剩余的牌当中奇数的数量达不到最优?

A:把 移到 的奇偶性会同时改变,所以剩下的奇数数量一定是一样的。

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表 最近公共祖先 前缀和 倍增

首先我们把 的路线看成在 的路径上取一个点 ,使得 然后从 往返到 、最后从 的路线最近。

为什么一定是这样呢?不能从 直接走到 而不回到 路径上吗?当然不行,这样子两点间就会有超过一种走法,不符合树的结构。

于是问题就转化成了在 的路径上取一点 ,使得另外任意一点 最大。答案最后再加上 ,因为这是一定要走的部分。

整个路径上每个点的信息并很好维护,但是我们需要求出对于每个点 的值 才可以维护。

这个值可以通过树形 DP 和换根解决。首先先考虑 子树中的情况,那么对于 (初始化为 ),枚举 的儿子 ,令

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);
  }
}

然后考虑 不在 子树中的情况(为了方便规定 合法),设最大值为 。那么对于 可以:

  • 存在于它的兄弟的子树当中。遍历父亲时,对儿子的 值做前缀后缀最大值就可以对儿子转移时查询。
  • 不存在于它的父亲的子树当中,即要向上至少两层。那么可以通过父亲的 值转移过来。

这两个 DP 时间复杂度都是 的。

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);
  }
}

对于 就是答案。

现在我们需要维护路径上的答案。首先可以想到重链剖分。由于不带修,用 ST 表维护 dfs 序所对应的序列的区间 。单次查询时间复杂度 。当然如果你大脑残废写了线段树把查询复杂度干到了 好像也可以过。

不过倍增也是可以的,额外维护每个节点向上跳 步经过的点的最大值也是可以的。时间复杂度

维护 到每个节点的路径长度,顺带求 LCA 来查询 的路程。

总时间复杂度

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 单调栈

不会,写的爆搜,甚至答案计算都是抄的题面上的。成功获得 分。

XA-Game

矩阵 快速幂

不会,暴力都没打,成功 分。