Loading

[CF 1827B2] Range Sorting (Hard Version)

前言

被自己傻到了, 赛时想法完全就是三岁小孩都能 \(\rm{hack}\) 的, 不仅没有意识到还接着推了无比之久

关于一些考试技巧

考试的时候一定要按照策略走, 连保底都要确定正确性以后再吃, 不能无意义的浪费太多时间

思路一定一定要用数据验证, 不管怎么样你至少要找 33 组数据吧

思路

题意

对一个排列 pp 的一段区间 [l,r][l,r] 排序的代价为 rlr−l, 对整个数组 pp 排序的代价为选定若干区间并排序, 使得整个数组有序的代价之和

pp 的所有子段排序的代价之和

不难发现最差答案就是

\[\sum_{l = 1}^{n} \sum_{r = l}^{n} r - l = \frac{n(n + 1)(n - 1)}{6} \]

你不管他怎么推出来的, 不行你稍微转化以下简化成一个 \(\sum\) 也行, 反正我们记为 \(C\)

考虑什么时候可以减少答案
不难发现如果有一个间隔 \(i - 1 \sim i\) , 你只需要排序其前面一部分加上其后面一部分即可解决, 不用排他自己, 那么对答案贡献 \(-1\)

更形式化的就是, 如果对于位置 \(p = \{i - 1, i\}\) , 满足 \(\max a_{[l, i - 1]} < \min a_{[i, r]}\) , 那么贡献 \(-1\)
我们考虑换贡献主体, 看每个位置对多少个区间有贡献 \(-1\)

推推推
枚举间隔 \(\{i - 1, i\}\) , 区间左端点 \(l < i\) , 记 \(v = \max a_{[l, i - 1]}\)
\(p\)\(i\) 之后第一个 \(< v\) 的位置, 那么 \(r \in [i, p)\) 都行

真是暴暴力力的 \(\mathcal{O} (n^3)\)

维护递增单调栈, 不难发现我们可以通过单调递增的栈枚举出所有 \(v\) , 然后考虑分段
每次弹出的栈顶 \(p\) 和候补栈顶 \(q\) 确定了 \(\max _{[q + 1 \sim p, i - 1]} = p\)
因此, \(l \in q + 1 \sim p\) , 对每一段进行一个处理做到 \(\mathcal{O} (n^2)\)

瓶颈在求 \(p\)
也就是求 \(i\) 之后第一个小于 \(j\) 的位置

不难发现询问数是 \(\mathcal{O} (n)\) 级别的, 也就是说, 单调栈枚举出一段 \(\max _{[q + 1 \sim p, i - 1]} = p\) , 就可以进行一次询问
把询问离线下来, 然后我们可以用类似于扫描线的方法, \(\mathcal{O} (n \log n)\) 处理


现在分析赛时做法有没有救
赛时发现, 如果遇到两个点 (注意不是间隔) 都是 \(\rm{gap}\) , 会对贡献有影响
但是我们把贡献主体换成间隔之后, 好像不会有这个影响

确实不会, 告诉我们贡献主体的重要性

代码

参考

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using ull = unsigned long long;

constexpr int mod = 998244353;
void addt(int &x, int y) {x += y, x >= mod && (x -= mod);}
int add(int x, int y) {return x += y, x >= mod && (x -= mod), x;}
int ksm(int a, int b) {
  int s = 1;
  while(b) {
    if(b & 1) s = 1ll * s * a % mod;
    a = 1ll * a * a % mod, b >>= 1;
  }
  return s;
}

bool Mbe;
constexpr int N = 1e6 + 5;

ll n, a[N], d[N], stc[N];
vector<pair<int, int>> buc[N];

ll c[N];
void ad(int x, ll v) {while(x <= n) c[x] = min(c[x], v), x += x & -x;}
ll qu(int x) {ll s = n + 1; while(x) s = min(s, c[x]), x -= x & -x; return s;}

void solve() {
  cin >> n;
  for(int i = 1; i <= n; i++) {
    cin >> a[i], d[i] = a[i];
    buc[i].clear(), c[i] = n + 1;
  }
  sort(d + 1, d + n + 1);
  for(int i = 1; i <= n; i++) a[i] = lower_bound(d + 1, d + n + 1, a[i]) - d;
  ll ans = n * (n + 1) * (n - 1) / 6, top = 0;
  for(int i = 1; i <= n; i++) {
    while(top && a[stc[top]] < a[i]) {
      int coef = stc[top] - stc[top - 1];
      buc[i].push_back({coef, a[stc[top]]});
      top--;
    }
    stc[++top] = i;
  }
  for(int i = n; i; i--) {
    for(auto _ : buc[i]) {
      ans -= 1ll * _.first * (qu(_.second) - i);
    }
    assert(a[i] > 0);
    ad(a[i], i);
  }
  cout << ans << "\n";
}

bool Med;
int main() {
  fprintf(stderr, "%.3lf MB\n", (&Mbe - &Med) / 1048576.0);
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  int T= 1;
  while(T--) solve();
  cerr << 1e3 * clock() / CLOCKS_PER_SEC << " ms\n";
  return 0;
}

总结

换贡献主体是简化贡献的好方法
特殊的是 \(r - l\) 转化到间隔上这种传奇贡献转化

不好直接算最优答案时, 考虑先找出最差答案, 然后再看什么操作可以减少答案

单调栈处理 \(\min\max\) 的问题有奇效, 本质上是减少了无用的计算

把询问离线是常见的好方法

posted @ 2025-02-15 12:06  Yorg  阅读(12)  评论(0)    收藏  举报