二小姐的烟花

贪心 结论 排序

首先,如果我们选择了若干个烟花进行燃放,那么按照 从小到大的顺序燃放一定是最优的。因为 的和本身确定了,而这种构造方案能够获得最多的顺序对。

所以先对 从小到大排个序。现在,我们可以很显然地猜出一个结论:最优的燃放烟花的方式一定是燃放最后的若干个烟花。证明不会证,只能感性理解。

于是我们就从后往前遍历,计算贡献。假如当前的 出现了 次,那么它会增加 个逆序对。我们对这个值进行累加,再乘上 ,最后加上 本身的值,就能得到燃放 的烟花能获得的最大值了,最后求最值。

时间复杂度 ,瓶颈在排序。

#include <fstream>
#include <algorithm>
 
using namespace std;
using LL = long long;
 
const LL kMaxN = 1e6 + 5;
 
ifstream cin("flandre.in");
ofstream cout("flandre.out");
 
struct {
  LL x, id;
} a[kMaxN];
 
LL n, k, sum, cnt, eql, p, ans;
 
int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> k;
  for (LL i = 1; i <= n; i++) {
    cin >> a[i].x;
    a[i].id = i;
  }
  sort(a + 1, a + n + 1, [](auto a, auto b) {
    return a.x < b.x;
  });
  p = n + 1;
  for (LL i = n; i >= 1; i--) {
    if (i == n || a[i].x != a[i + 1].x) {
      eql = 1;
    } else {
      eql++;
    }
    sum += a[i].x;
    cnt += n - i + 1 - eql;
    LL val = sum + k * cnt;
    if (val > ans) {
      ans = val;
      p = i;
    }
  }
  cout << ans << ' ' << n - p + 1 << '\n';
  for (LL i = p; i <= n; i++) {
    cout << a[i].id << ' ';
  }
  return 0;
}

红美铃摸鱼

数学 前缀和

看到

这个一大坨式子,我们就可以考虑拆贡献。对于 ,如果它俩相乘的话,那么有 。显然我们不能枚举 进行计算,而 次操作每次都是对 进行区间相加。于是我们可以想到

  • [I] 预处理每个 所乘上的系数,即

这个式子有 ,不好搞,所以我们把 分成 的分别进行处理。

对于 ,式子变成 。我们从左往右枚举 ,每次添加 的一个新元素。不难发现 随着 变大而变小,具体是每次 时原来所有的 的贡献都会减掉

所以我们在增加 的贡献时,同时维护 ,每次累加这个值,从总贡献当中减去。因此,我们就成功完成了 的这一部分。

对于 同理,从后往前遍历,注意 互换了。这些预处理都是 的。

最后每个 多累加了一次,从贡献中删去 即可。这下我们就可以轻松地得出初始答案,每次查询对 值加上 也就相当于再答案上累加了 的系数之和乘上 。这个可以通过前缀和实现

时间复杂度 ,要注意常数。

#include <fstream>
 
using namespace std;
using LL = long long;
 
const LL kMaxN = 5e5 + 5, kMod = 1e9 + 7;
 
ifstream cin("meirin.in");
ofstream cout("meirin.out");
 
LL a[kMaxN], b[kMaxN], base[kMaxN], n, q, sum, sub, ans;
 
int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> q;
  for (LL i = 1; i <= n; i++) {
    cin >> a[i];
    sum = ((sum + (n - i + 1) * i % kMod * a[i] % kMod - sub) % kMod + kMod) % kMod;
    base[i] = (base[i] + sum + kMod) % kMod;
    sub = (sub + i * a[i] % kMod + kMod) % kMod;
  }
  for (LL i = 1; i <= n; i++) {
    cin >> b[i];
  }
  sum = sub = 0;
  for (LL i = n; i >= 1; i--) {    
    sum = ((sum + i * (n - i + 1) % kMod * a[i] % kMod - sub) % kMod + kMod) % kMod;
    base[i] = (base[i] + sum + kMod) % kMod;
    sub = (sub + (n - i + 1) * a[i] % kMod + kMod) % kMod;
  }
  for (LL i = 1; i <= n; i++) {
    base[i] = (base[i] - i * (n - i + 1) % kMod * a[i] % kMod + kMod) % kMod;
    ans = (ans + base[i] * b[i] % kMod + kMod) % kMod;
    base[i] = (base[i - 1] + base[i] + kMod) % kMod;
  }
  for (LL l, r, k; q; q--) {
    cin >> l >> r >> k;
    ans = (ans + (base[r] - base[l - 1] + kMod) % kMod * k % kMod + kMod) % kMod;
    cout << ans << '\n';
  }
  return 0;
}

红魔馆

概率与期望 树形dp 组合数学

  • [!] 以下规定 个关键点的行走方式为路线,任意两点间的为路径。

首先我们需要先求出初始答案。按照每一条遍历方式求总和肯定是不可取的,于是仍然考虑拆贡献。本题没有对走的路线进行任何规定,所以任何两个属于 个关键点的点都可以连起来,作为路线的一部分。

对于任意一条两个关键点连起来的路径,它会在 条路线当中出现过。即枚举路径前有 个关键点经过,则有 种排列方式,剩下 个点放在后面随便排。

  • [I] 事实上这个数等于 ,但是我赛时没管这么多,写的暴力求和。

现在我们考虑如何计算任意两个关键点之间的距离之和。考虑树形 dp,设 表示以 为起点,到 子树中所有关键点所需要的距离之和。若 子树中的关键点之和,那么有

其实就是将 子树内的关键点的距离拼上 。不过我们求的不是 子树内,而是 到任意关键点(除 本身)的距离和。

考虑换根,把每个 看作根,计算 到树内任意关键点的距离和。设其为 。如果要将根从 转移到 的话, 子树内的所有关键点到根的距离就会减去 ,其余的所有关键点到根的距离就会加上 ,所以有

此时我们就通过 计算出了每个点到任意关键点之间的距离之和,那么把所有关键点的这个值累加起来即可得到路径距离总和。乘上之前的系数 即可得到任意的路线距离总和的总和,除以总路线数量 即可得到初始答案。

现在考虑修改。如果我们修改一个点 邻边边权增加 ,而它的邻边被任意两个关键点之间的路径经过的次数为 ,那么答案会加上 (还要乘上系数和 )。

如何求 呢?如果 是关键点,那么它会被经过 次( 到其它关键点和其它关键点到 )。其次是作为路径上的点,那么可以遍历 的所有邻点 ,获取以 子树内关键点的数量(仅当 时有区别,其余都是 ),然后对于任意两个不同 的数量相乘再乘 (不是一正一反,而是经过了 的两条邻边)。当然肯定不能枚举,具体操作是求和后平方,再减去每个项的平方。

现在我们就成功写出这道题了。时间复杂度 ,也是需要注意常数的一道题。

#include <fstream>
#include <forward_list>
#include <utility>
 
using namespace std;
using LL = long long;
 
const LL kMaxN = 5e5 + 5, kMod = 998244353;
 
ifstream cin("sakuya.in");
ofstream cout("sakuya.out");
 
LL dp[kMaxN], res[kMaxN], ptr[kMaxN], mrk[kMaxN], fac[kMaxN], inv[kMaxN], a[kMaxN], fa[kMaxN], n, m, q, ans;
forward_list<pair<LL, LL>> e[kMaxN];
 
void dfs1(LL x, LL f) {
  ptr[x] = mrk[x];
  fa[x] = f;
  for (const auto &i : e[x]) {
    if (i.first == f) {
      continue;
    }
    dfs1(i.first, x);
    ptr[x] += ptr[i.first];
    dp[x] = (dp[x] + dp[i.first] + ptr[i.first] * i.second % kMod) % kMod;
  }
}
 
void dfs2(LL x, LL f) {
  for (const auto &i : e[x]) {
    if (i.first == f) {
      continue;
    }
    res[i.first] = res[x] - (ptr[i.first] * i.second) + (m - ptr[i.first]) * i.second;
    res[i.first] = (res[i.first] % kMod + kMod) % kMod;
    dfs2(i.first, x);
  }
}
 
LL pow(LL a, LL b) {
  LL res = 1;
  for (; b; b /= 2) {
    b % 2 && (res = res * a % kMod);
    a = a * a % kMod;
  }
  return res;
}
 
LL A(LL n, LL m) {
  if (m > n) {
    return 0;
  }
  return fac[n] * inv[n - m] % kMod;
}
 
int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> m;
  for (LL i = 1, u, v, w; i < n; i++) {
    cin >> u >> v >> w;
    e[u].emplace_front(v, w);
    e[v].emplace_front(u, w);
  }
  fac[0] = inv[0] = 1;
  for (LL i = 1; i <= n; i++) {
    fac[i] = fac[i - 1] * i % kMod;
  }
  inv[n] = pow(fac[n], kMod - 2);
  for (LL i = n - 1; i >= 0; i--) {
    inv[i] = inv[i + 1] * (i + 1) % kMod;
  }
  for (LL i = 1, x; i <= m; i++) {
    cin >> x;
    mrk[x]++;
  }
  dfs1(1, 0);
  res[1] = dp[1];
  dfs2(1, 0);
  for (LL i = 1; i <= n; i++) {
    ans = (ans + mrk[i] * res[i] % kMod) % kMod;
  }
  LL base = 0;
  for (LL i = 0; i <= m - 2; i++) {
    base = (base + A(m - 2, i) * fac[m - 2 - i] % kMod) % kMod;
  }
  for (LL i = 1; i <= n; i++) {
    mrk[i] && (a[i] = 2 * mrk[i] % kMod * (m - 1) % kMod);
    LL sum = 0, sub = 0;
    for (const auto &j : e[i]) {
      if (j.first == fa[i]) {
        LL x = m - ptr[i];
        sum = (sum + x) % kMod;
        sub = (sub + x * x % kMod) % kMod;
      } else {
        LL x = ptr[j.first];
        sum = (sum + x) % kMod;
        sub = (sub + x * x % kMod) % kMod;
      }
    }
    sum = (sum * sum % kMod - sub + kMod) % kMod;
    a[i] = (a[i] + 2 * sum % kMod) % kMod;
  }
  cin >> q;
  for (LL x, k; q; q--) {
    cin >> x >> k;
    ans = (ans + a[x] * k % kMod) % kMod;
    cout << ans * base % kMod * inv[m] % kMod << '\n';
  }
  return 0;
}

红楼

分治 分块 差分

我在考场上想出了根号分治,但是没有优化掉

首先题目中的修改操作可以视作以 为一个周期,每个周期前 个数加上

  • 对于 ,我暴力枚举每一个周期,用线段树做区间加法。修改时间复杂度 ,查询
  • 对于 ,我对于每个 都会开一颗 大小的线段树(维护和还有下标乘平方的和),然后对于 线段树的前 个数加上 。查询时枚举每一个 ,然后先把整块的给加上,最后剩的一些对于所有 的系数都会被限制在 。修改时间复杂度 ,查询时间复杂度

不难发现复杂度瓶颈在于第一种操作的修改。我们需要 对一个区间进行加法(这样单词修改就变成了 ),而查询时间复杂度可以变成

这是一个相当复杂的分块,需要使用复杂的差分+前缀和技巧实现。不详细说了。

对于第二种操作,修改时间复杂度太优了查询复杂度太劣,所以不开 的线段树了,直接 暴力修改前缀和数组,这样单块内查询就能做到 。总时间复杂度就可以达到